Orchestrating Multi-Service Apps with Docker Compose (Advanced)

One compose.yaml to run web, database, and cache together: custom networks, health checks, env vars, volumes, and restart policies.

Starting a single container is easy, but real apps are usually a bundle of "web + database + cache." Wiring them up one docker run at a time is tedious and it's easy to forget a network flag. Docker Compose lets you describe every service, network, and volume in one declarative compose.yaml, then bring the whole stack up with a single command. This guide assumes you're on your own server/VPS running Ubuntu or Debian, with Docker Engine and the Compose plugin already installed (a working docker compose version is all you need).

A web + db + redis example

Create compose.yaml in your project directory:

services:
  web:
    build: .
    ports:
      - "8080:8080"
    environment:
      DATABASE_URL: "postgres://app:${DB_PASSWORD}@db:5432/appdb"
      REDIS_URL: "redis://cache:6379"
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    networks:
      - backend
    restart: unless-stopped

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: appdb
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - backend
    restart: unless-stopped

  cache:
    image: redis:7
    networks:
      - backend
    restart: unless-stopped

volumes:
  db-data:

networks:
  backend:

Custom networks let services talk

The networks block declares a custom bridge network, backend, that all three services join. Within a shared network, Compose provides service-name DNS: web reaches the others simply as the hostnames db and cache, with no need to publish ports to the host. Only web, which actually faces the outside world, maps 8080 via ports; keeping db and cache internal-only is safer.

dependson and healthcheck

dependson controls start order, but by default it only waits for a container to start, not to be ready. There's a gap between a database process launching and it accepting connections, so give db a healthcheck (probing with pgisready) and have web wait on condition: servicehealthy. That way web starts only once the database is genuinely up, avoiding a rejected first connection.

Environment variables and .env

Pull secrets out of the file. Create a .env in the same directory and Compose reads it automatically:

DB_PASSWORD=change-me-in-prod

Every ${DBPASSWORD} in compose.yaml gets substituted. Add .env to your .gitignore so it never lands in the repo.

Persisting data with volumes

Delete a container and its filesystem is gone, so a database needs a named volume: db-data:/var/lib/postgresql/data. Docker manages the volume, and even docker compose down leaves it intact, so recreating the container keeps your data (unless you explicitly run down -v).

Restart policy

restart: unless-stopped brings a container back after a crash or a server reboot, but respects the times you stopped it yourself. It's the common production choice; use on-failure if you only want retries on crashes.

Everyday commands

docker compose up -d          # bring the whole stack up in the background
docker compose ps             # check status
docker compose logs -f web    # tail one service's logs
docker compose up -d --scale web=3   # scale web out to 3 replicas
docker compose down           # stop and remove containers (named volumes kept)

Note that when using --scale you can't pin a single host ports mapping on that service (they'd collide) — typically you front it with a reverse proxy or use a port range.

Summary

The point of Compose is turning "how these containers cooperate" into one version-controlled file: custom networks let services find each other by name, healthcheck plus dependson enforce start order, .env centralizes config, named volumes protect your data, and restart adds self-healing. Get comfortable with up -d, logs -f, and scale, and you can run a solid multi-service app on any VPS.