Skip to main content

Assignment 2: Compose a multi-service stack

Goal: Define and run a multi-service application with Docker Compose — your app from Assignment 1 plus a database — wired together on a user-defined network, with persistent data and proper readiness ordering, all from a single compose.yaml.

Where: Any lab VM with Docker installed. Your app image should be the one you pushed to the lab registry at 10.100.100.6 in Assignment 1.

Tasks

  1. Extend (or reuse) your app so it connects to a database (e.g. Postgres) using a connection string read from an environment variable like DATABASE_URL, and reaches the DB by the service name (e.g. db), not an IP.
  2. Create a compose.yaml with two services:
    • db: an official database image, pinned to a version tag, with a named volume mounted at its data directory so data survives restarts.
    • api: your image pulled from 10.100.100.6/<yourname>/<app>:sha-<shortsha>, publishing its port to the host with ports:.
  3. Put both services on a user-defined network (or rely on Compose's default network) so the api resolves db by name.
  4. Add a HEALTHCHECK to the db service (e.g. pg_isready) and make api use depends_on: with condition: service_healthy so it starts only after the DB is ready.
  5. Keep secrets out of the committed file: put the DB password in an .env file (and add .env to .gitignore / .dockerignore).
  6. Bring it up: docker compose up -d. Verify with docker compose ps that both services are running and db is healthy.
  7. Confirm the app can talk to the database (hit an endpoint that reads/writes the DB, or docker compose logs api showing a successful connection).
  8. Test persistence: write some data, docker compose down (WITHOUT -v), docker compose up -d again, and confirm the data is still there. Then explain (in your writeup) what docker compose down -v would have done differently.

Deliverable

Your compose.yaml, your .env.example (with the password value as <REDACTED>), and a short writeup showing: the docker compose ps output with db healthy, evidence the api reached db by name, and your persistence test result (data survived a down/up cycle).

Acceptance criteria — you're done when:

  • A single compose.yaml defines both the db and api services.
  • The api reaches the database by service name (e.g. db:5432), with no hard-coded container IP.
  • Both image references are pinned (db version tag; api is your sha- tagged image from 10.100.100.6).
  • A named volume persists the database data directory.
  • The db service has a working HEALTHCHECK and docker compose ps shows it as healthy.
  • api uses depends_on with condition: service_healthy.
  • The DB password is in an .env file, not hard-coded in compose.yaml, and .env is git-ignored.
  • docker compose up -d brings the whole stack up; the app successfully reads/writes the database.
  • Data survives a docker compose down followed by docker compose up -d.

Hints

  • Re-read Chapter 4 section 5 for the depends_on + condition: service_healthy pattern — plain depends_on only waits for start, not readiness.
  • Inside the api container you can test name resolution: docker compose exec api sh, then ping db or wget -qO- http://db:5432.
  • docker compose config prints the fully resolved file — use it to catch YAML indentation errors before running.
  • If data vanished after down, you probably ran down -v (which deletes named volumes) or used a bind mount you cleaned up — check your volumes: block.
  • If api cannot pull, re-run docker login 10.100.100.6 and double-check the image tag matches Assignment 1.
  • Even with service_healthy, have your app retry its DB connection — that is realistic and avoids race conditions.

Blocked for >~30 min after re-reading the lessons? Bring what you've tried to your mentor.