Skip to content

2. Environment sealing

Stage 2 turns your local .env into the values the deployment will use.

  1. Read every key and value in .env.
  2. Classify each value: a real value, a placeholder, or plain configuration.
  3. Check that .env.example declares every key the code reads.
  4. Ask Git whether .env.production is ignored.
  5. Only then write .env.production, with real values only.

A project with no .env has nothing to seal, and the stage is recorded as not applicable rather than failed.

Values are classified with the same placeholder patterns, provider prefixes and entropy thresholds the audit’s secret rules use, and key coverage is the same comparison ENV-001 makes. The audit and the deploy cannot disagree about what a secret is or which keys are missing.

A value such as CHANGEME is reported for you to supply and left out of the generated file. Deploying it would configure a live service with the word CHANGEME, which fails later in a way that is much harder to trace.

.env.production holds real values. If Git would not ignore it, it is one git add away from being committed, so the check runs before anything is written and nothing is written at all. The question goes to git check-ignore, the only thing that reads nested ignore files, negations and global excludes the way Git itself does. If Git cannot answer, the check fails rather than guessing.

A copy of the demo project whose .gitignore does not list .env.production, and whose .env holds a placeholder:

{
"project": "seal-bad",
"ok": false,
"reason": ".env.production is not covered by .gitignore, so it was not written",
"ignored": false,
"written": "",
"sealed": ["PORT"],
"blocked": ["CORS_ORIGIN"],
"uncovered": [],
"values": [
{ "key": "PORT", "kind": "plain",
"reason": "too short to be a credential, read as configuration", "value": "10000" },
{ "key": "CORS_ORIGIN", "kind": "placeholder",
"reason": "the value looks like a stand-in, not a real credential", "value": "CHANGEME" }
]
}

With the demo’s own .gitignore and real values, the stage passes and writes:

# Generated by ProdPilot. Real values, never commit this file.
# Regenerate it rather than editing it by hand.
PORT=10000
CORS_ORIGIN=https://app.example.com

A value that looks like a credential is masked in the result, the way the audit masks a finding. The sealed values then go to the Docker build test and to Render, and nowhere else.