CertGrid CertGrid
Hands-on Lab·Docker

Docker Compose Multi-Service Applications

The shape almost every real project takes: an application talking to a database over the project network, with the data on a named volume that survives a teardown. Built and verified against PostgreSQL 17.

Docker Compose Guide 25 of 46 Intermediate

Tested on the versions above. Container names derive from the directory name and timings vary. Match the shape of the output, not the exact strings.

One Docker host is all this guide needs. Nothing here depends on a second machine, and the hardware above is modest on purpose - a 2 core, 4 GB VM runs everything in this path.
Server NameIP AddressOSRolesCPURAMHDD
DOCKER01192.168.0.21Ubuntu 26.04 LTSDocker Host2 Core4 GB50 GB

Before you start

  1. The file

    Two services. The database gets a named volume for its data directory and a healthcheck; the application waits for that healthcheck rather than for the container merely to exist. Nothing is published to the host - the app reaches the database over the project network, and the database is unreachable from outside it. That is the default worth keeping.

    bash
    cat compose.yamlservices:  db:    image: postgres:17-alpine    environment:      POSTGRES_PASSWORD: example      POSTGRES_DB: appdb    volumes:      - dbdata:/var/lib/postgresql/data    healthcheck:      test: ["CMD-SHELL", "pg_isready -U postgres -d appdb"]      interval: 3s      retries: 10  api:    image: alpine:3.22    depends_on:      db:        condition: service_healthy    command: sh -c "nc -z db 5432 && echo reached db:5432 by service name; sleep 300" volumes:  dbdata:

    Expected resultTwo services and a top-level volumes block declaring the named volume.

    Success conditionThe volume is declared at the top level as well as mounted in the service. Omitting the top-level declaration is the most common error in this file.

  2. Bring it up and watch the ordering

    Read the sequence carefully, because it is the whole point of the file. The database starts, then Compose reports Waiting, then Healthy, and only then does the application start. Without the healthcheck condition the app would have started immediately and failed to connect - the database process needs a moment after the container exists.

    bash Example session
    docker compose up -d Network cg-stack_default Created Container cg-stack-db-1 Created Container cg-stack-api-1 Created Container cg-stack-db-1 Starting Container cg-stack-db-1 Started Container cg-stack-db-1 Waiting Container cg-stack-db-1 Healthy Container cg-stack-api-1 Starting Container cg-stack-api-1 Started

    Expected resultWaiting and Healthy for the database BEFORE the application starts.

    Success conditionYou see Waiting then Healthy. If the app starts immediately after the db Started line, your condition: is missing or misspelled.

  3. Confirm the health state

    ps reports the health state alongside the status, and it is the fastest read on whether a dependency is genuinely ready. Only db shows a port, and only inside the network - there is no host mapping.

    bash Example session
    docker compose ps --format "table {{.Service}}\t{{.Status}}\t{{.Ports}}"SERVICE   STATUS                   PORTSapi       Up Less than a seconddb        Up 3 seconds (healthy)   5432/tcp

    Expected result(healthy) in the status column, and a health state of healthy on inspect.

    Verify it worked

    bash Example session
    docker inspect cg-stack-db-1 --format "health={{.State.Health.Status}} checks={{len .State.Health.Log}}"health=healthy checks=1

    Success conditionThe database reports healthy. A service with no healthcheck shows no such annotation at all - absence is not the same as unhealthy.

  4. Prove the app reached the database by name

    The application connected to the host db - the service name - with no address, no link and no published port. Compose put both containers on a project network and registered each service name in its DNS, which is exactly the user-defined bridge behaviour from guide 10 set up on your behalf.

    bash
    docker compose logs apiapi-1  | API startingapi-1  | reached db:5432 by service name

    Expected resultThe application reporting a successful connection, and one project network.

    Verify it worked

    bash Example session
    docker network ls --filter name=cg-stackNETWORK ID     NAME               DRIVER    SCOPE5495fe0ddd42   cg-stack_default   bridge    local

    Success conditionThe service name resolved. This is why you should never hardcode container IPs in a Compose project.

  5. Talk to the database directly

    compose exec gets you a session inside the database container. Useful for a schema check or a quick query - and note you did not need the database port published on the host to do it.

    bash Example session
    docker compose exec db psql -U postgres -d appdb -c "select version();" | head -3                                         version------------------------------------------------------------------------------------------ PostgreSQL 17.11 on x86_64-pc-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit

    Expected resultA version banner from the running server.

    Success conditionYou get a real answer from the database engine. If psql reports it cannot connect, the server is still starting - which is what the healthcheck exists to prevent.

  6. Data survives the teardown - unless you ask otherwise

    This is the distinction that catches people out. down removes the containers and the network but leaves named volumes alone, so bringing the project back up finds its data intact. down -v deletes those volumes too, and there is no confirmation prompt. Learn the difference on a project you do not care about.

    bash
    # keeps the dbdata volume - the database comes back with its data:docker compose down# -v DELETES the dbdata volume and everything in the database:docker compose down -v

    Expected resultThe first form leaves the volume in docker volume ls; the second removes it.

    Success conditionYou can state which of the two you want before typing it. For anything holding real data, back it up first - see guide 13.

Troubleshooting

Official sources