A real application is rarely just one container. A typical setup might need a backend API, a database, and maybe a cache — each running in its own container, but needing to talk to each other. Running each one by hand with long docker run commands gets old fast, and remembering the right flags every time is error-prone. Docker Compose solves this with a single YAML file describing every service an app needs.

A backend plus a database

version: "3.9"
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgres://appuser:secret@db:5432/appdb
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      - POSTGRES_USER=appuser
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=appdb
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Reading the file

  • services — each key under here (app, db) becomes its own container.
  • build: . — for the app service, build an image from the Dockerfile in the current directory, rather than pulling a pre-built one.
  • image: postgres:16 — for the db service, just pull this exact, ready-made image from Docker Hub instead of building one.
  • depends_on — starts db before app. Note this only controls start order, not whether Postgres has finished initializing — a well-written app should retry its database connection on startup rather than assume it is instantly ready.
  • volumes — the named volume db-data keeps the database's files on disk even if the container is recreated.

The magic of service names as hostnames

Notice the database URL uses db as the hostname, not localhost. Compose creates a private network for the whole file, and every service can reach every other service by its service name, exactly as if it were a real DNS hostname. This is one of the most useful things Compose gives you for free.

Running it

docker compose up            # build/pull and start everything, logs in the foreground
docker compose up -d         # same, but detached in the background
docker compose logs -f app   # follow just the app service's logs
docker compose down          # stop and remove all the containers
docker compose down -v       # also remove the named volumes (careful — deletes data)

One command spins the entire stack up in the right order with the right network wired together, which is a huge improvement over juggling several long docker run commands by hand.

When you would reach for this

  • Local development, so every teammate runs the exact same backend-plus-database setup with one command.
  • CI pipelines, spinning up a real database for integration tests.
  • Small, single-machine deployments where a full orchestrator like Kubernetes would be overkill.

With the fundamentals and Compose in place, the last two tutorials build two genuinely real Dockerfiles: one for a Node.js API, and one for a Spring Boot app using the exact multi-stage pattern this very site is deployed with.