Run a two-tier app with Compose, safely

applied · 45 min · Objective 2.6

Task

Bring up an application and its database with Compose, wiring them together correctly: named volume for the data, a health-gated dependency, a loopback-bound port, and a secret that is not an environment variable. Then prove the down/up cycle keeps the data and that down -v destroys it.

Steps

  1. Write a compose file with a db service using a named volume for its data directory, and an app service that connects to it.
  2. Bind the app's published port to loopback -- "127.0.0.1:8080:8080" -- not to every interface. Confirm with ss after up.
  3. Wire depends_on with condition: service_healthy and give the db a healthcheck. Prove the app waits for the db to be ready, not merely started, by watching the startup order in docker compose logs.
  4. Supply the db password as a Compose secret backed by a file, not as an environment variable. Confirm it is not visible in podman inspect or the host process list.
  5. Bring it up with up -d, insert data, then down (without -v) and up again. Confirm the data survived, because named volumes outlive down.
  6. Now run down -v and up, and confirm the data is gone. State the difference between the two commands in one sentence.
  7. Read aggregated logs with docker compose logs -f app and identify a startup line that proves the db-readiness gate worked.

Verify

cd ~/lab-compose
ss -tlnp | grep ':8080'                                  # 127.0.0.1 only
docker compose exec db psql -U app -tAc 'select count(*) from items'   # survives down/up
docker compose config | grep -A2 secrets                 # secret, not env var
docker compose down -v && docker compose up -d
docker compose exec db psql -U app -tAc 'select count(*) from items'   # 0 after -v

The two counts are the lesson: data survives down and does not survive down -v. The loopback bind in the first check is the exposure control -- a database published on 0.0.0.0 is reachable from anything that can reach the host.

Notes

depends_on alone means "start after", not "wait until ready". Without the service_healthy condition an app that connects to its database on startup will still fail the race, intermittently, in a way that looks like an application bug rather than a compose configuration gap.