Catching Missing Env Vars Before Container Startup
An app crashes deep into boot because a required env var was unset. Fail fast with a compose ${VAR:?err} guard, an entrypoint check, and a CI schema gate.
A container starts, runs for ten seconds, then crashes with a null-pointer error deep in application code — all because DATABASE_URL was never set. This guide is part of environment variable validation within the environment sync, secrets and CI parity baseline.
Diagnostic
Reproduce the late failure by starting the service with a required variable unset.
#!/usr/bin/env bash
set -euo pipefail
unset DATABASE_URL
docker compose up app
Expected BAD output (failure happens late and the message hides the real cause):
app-1 | Server starting on :8080
app-1 | Connecting to database...
app-1 | TypeError: Cannot read properties of undefined (reading 'replace')
app-1 | at parseConnectionString (/app/db.js:14:21)
app-1 exited with code 1
The crash is a generic runtime error, not "DATABASE_URL is required", so the root cause is obscured.
Root cause
By default an unset environment variable is simply absent. The application reads undefined, carries it through initialization, and only fails when it finally dereferences it — far from where the variable should have been validated. Nothing between the unset value and the crash asserts that the variable is present, so the failure is both late and misattributed.
Resolution
- Make Compose itself refuse to start when a required variable is missing, using the
${VAR:?error}mandatory-variable syntax.
# docker-compose.yml
services:
app:
image: app:local
environment:
DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required}
API_PORT: ${API_PORT:?API_PORT is required}
- Add an entrypoint guard so the check also fires when the container is run outside Compose (CI,
docker run).
#!/usr/bin/env bash
# docker-entrypoint.sh
set -euo pipefail
REQUIRED=("DATABASE_URL" "API_PORT" "JWT_SECRET")
missing=()
for key in "${REQUIRED[@]}"; do
[ -n "${!key:-}" ] || missing+=("$key")
done
if [ "${#missing[@]}" -gt 0 ]; then
echo "FATAL: missing required env vars: ${missing[*]}" >&2
exit 1
fi
exec "$@"
- Wire the entrypoint and a CI schema check so the gate runs before the image is ever deployed.
# docker-compose.yml (entrypoint wiring)
services:
app:
image: app:local
entrypoint: ["/app/docker-entrypoint.sh"]
command: ["node", "server.js"]
#!/usr/bin/env bash
# CI: validate .env against the schema before tests run
set -euo pipefail
ajv validate -s env-schema.json -d .env --strict-types
Expected output
$ docker compose up app
app The DATABASE_URL variable is required but not set.
$ echo $?
1
With the entrypoint guard and a missing variable supplied at docker run time:
FATAL: missing required env vars: JWT_SECRET
The container exits immediately with a precise message, before any application code runs.
Prevention
- Keep the required list in one machine-readable schema (
env-schema.json) and drive both the entrypoint and CI from it, so there is a single source of truth. - Add a pre-commit hook running
ajv validateagainst.envto catch omissions before push. - Use
${VAR:?}for every truly required variable in Compose so the failure surfaces atuptime, not at runtime.
WSL2: normalize line endings (
core.autocrlf=input) so a trailing\ron a value does not make a present variable read as malformed. macOS (Docker Desktop): avoid relying on host shell inheritance; pass--env-fileso the guard validates the file the container actually receives.
Rollback
The guard only blocks genuinely incomplete environments, so the fix is to supply the variable, not to remove the guard. To unblock temporarily while debugging, export the value for one run: DATABASE_URL=postgres://localhost/dev docker compose up app.