Configuration through the environment
Passing DATABASE_URL as an environment variable instead of hardcoding it exists for one reason: the same image from lesson 3-1 must run everywhere, on your laptop, on the CI server, and in production. CI is short for continuous integration, an automated machine that tests every push, and unit 7 is devoted to it.
The code stays identical and only the environment changes per machine. This convention is strong enough to have a name, the twelve-factor config rule.
The database side of the pair needs its own env vars. The official postgres image reads these on first boot:
db:
image: postgres:16
environment:
POSTGRES_USER: shop
POSTGRES_PASSWORD: secret
POSTGRES_DB: shop
volumes:
- dbdata:/var/lib/postgresql/data| Variable | Must match |
|---|---|
POSTGRES_USER | the user in DATABASE_URL |
POSTGRES_PASSWORD | the password in DATABASE_URL |
POSTGRES_DB | the database name in DATABASE_URL |
User, password, and database name here must match what the app's DATABASE_URL claims, or connections fail. When a compose stack is broken, comparing these two blocks is the first debugging move.
Assembling the URL from its pieces
A connection URL is built from exactly the pieces sitting in the env blocks. Holding each in a variable makes the shape obvious.
user="shop" password="secret" host="db" port="5432" dbname="shop" echo "postgres://$user:$password@$host:$port/$dbname"
Output
postgres://shop:secret@db:5432/shop| URL position | Comes from |
|---|---|
| before the colon | POSTGRES_USER |
between colon and @ | POSTGRES_PASSWORD |
after @ | the compose service name |
| after the last slash | POSTGRES_DB |
Every piece is plain variable expansion inside one echo string, which works because double quotes allow expansion while keeping the whole thing a single argument. Writing it out this way is a useful habit when a URL is rejected, since it forces each component to be checked separately.
What depends_on does and does not do
depends_on:
- dbThis controls start order only, so compose launches db before app. It does not wait for Postgres to become ready to accept connections, which takes a few seconds after the container starts.
| Guarantee | Provided by plain depends_on |
|---|---|
db container starts first | yes |
| Postgres accepts connections | no |
So the app can still boot, try to connect, and fail while the database is warming up. There are two real fixes.
- The app retries its database connection on startup, which is good practice regardless.
- A healthcheck on the db service, plus
depends_onwithcondition: service_healthy, so compose waits for genuine readiness.
Healthchecks get their own treatment in lesson 9-2. The trap to remember is that started is not ready.
A start-order guarantee that is not a readiness guarantee
With plain depends_on: [db], compose starts db first and the app can still crash with "connection refused" on a slow machine, because depends_on starts the db container without waiting for Postgres inside it to be ready for connections.
depends_on sequences container startup, not application readiness, and Postgres needs a few seconds after its container starts.
| Fix | Nature |
|---|---|
| retry the connection in the app | application-level, always worth having |
healthcheck plus condition: service_healthy | compose-level, waits for real readiness |
The retry loop is the more robust of the two, because it also survives a database restart in the middle of the day rather than only at boot. The healthcheck route is covered in lesson 9-2.
A password mismatch between blocks
With POSTGRES_PASSWORD set to secret and the app's DATABASE_URL reading postgres://shop:hunter2@db:5432/shop, the connection fails.
The database accepts the password from its own environment, which is secret, and the app presents hunter2, so Postgres rejects the authentication.
| Side | Password |
|---|---|
| db service environment | secret |
| app connection URL | hunter2 |
Mismatched env blocks like this are the most common broken-compose-stack bug, which makes diffing them the first thing to do. The error message says authentication failed rather than anything about compose, so the cause is easy to look past.