Code Safari

Chapter 122·Intermediate·9 min read

Docker Compose: Your Whole App in One File

A plain-English guide to Docker Compose — why juggling many docker run commands doesn't scale, describing a whole multi-container app in one compose.yaml, how services, ports, volumes, and networks fit together, and the handful of commands (up, down, logs) that run it all.

July 19, 2026

By the end of the last chapter you could run a real multi-container system — a network, a database with a volume, a web app with published ports — but only by typing a string of long, order-sensitive docker run commands, perfectly, every single time. That doesn't scale to a real project, and it certainly doesn't scale to a team. Docker Compose is the answer: describe your entire application — every container, connection, and setting — in one file, and run the whole thing with one command. It's where containers go from a neat trick to a genuinely pleasant daily workflow.

The problem: apps are more than one container

Picture the modest system from last chapter: a web app, a PostgreSQL database, and a Redis cache. To run it by hand you'd need something like:

$ docker network create appnet
$ docker run -d --name db --network appnet -v pgdata:/var/lib/postgresql/data postgres:16
$ docker run -d --name redis --network appnet redis:7
$ docker run -d --name web --network appnet -p 8080:80 myapp

Four commands, in a specific order, each with flags you have to remember exactly. Now imagine doing that every morning, keeping it in sync as the app grows, and — the real killer — explaining it to a new teammate. This imperative, by-hand approach is fragile: one forgotten flag or wrong order and the system won't come up, and there's no single place that records "here is how this app is wired together."

One file to describe it all

Compose reads a file named compose.yaml (you'll also see the older name docker-compose.yml — same thing). It's written in YAML, a simple indentation-based format for structured data. Here's that entire four-command system, captured as one file:

services:
  web:
    build: .
    ports:
      - "8080:80"
    depends_on:
      - db
      - redis
    environment:
      DATABASE_URL: postgres://db:5432/app
 
  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: secret
 
  redis:
    image: redis:7
 
volumes:
  pgdata:

Read it top to bottom and it's almost plain English: there are three servicesweb, db, and redis. web is built from the local Dockerfile, publishes port 8080, and depends on the other two. db uses the Postgres image and stores its data in a named volume. redis uses the Redis image as-is. And a named pgdata volume is declared at the bottom. Everything the four docker run commands did, now legible in one place — and, crucially, something you can commit to your repository so the whole team runs the identical setup.

The pieces of a service

Each entry under services: maps almost directly to the docker run flags you already know — which is the nicest thing about learning Compose after the raw commands: there's nothing genuinely new, just a tidier home for each idea.

  • image: — which image to run (postgres:16, redis:7), exactly like the image name in docker run.
  • build: . — instead of a pre-made image, build one from a Dockerfile in this directory. This is the docker build step, folded in.
  • ports: — publish ports, the -p host:container mapping from the last chapter, written as "8080:80".
  • volumes: — attach volumes, the -v flag, for persistent data.
  • environment: — set environment variables inside the container (database passwords, config, connection URLs).
  • depends_on: — declare that this service needs another started first, so web waits for db.
image / build → what to run
ports → -p publishing
volumes → -v persistence
environment → config
depends_on → start order
A Compose service is just docker run, organised

Networking, handled for you

Here's the payoff that makes Compose feel magical after last chapter's manual work: Compose automatically creates a shared network for all the services in the file, and puts every service on it. That means the name-based networking you set up by hand — docker network create, --network, --name — happens for free, and each service is reachable by its service name.

Look back at the web service's config: DATABASE_URL: postgres://db:5432/app. That db is simply the service name from the file, and Compose's networking resolves it to the database container automatically. No docker network create, no chasing IPs, no localhost trap — you refer to other services by the names you gave them in the file, and it just works. Everything the previous chapter taught you to do deliberately, Compose does silently.

The commands: up, down, and friends

With the file written, the entire system runs with a handful of commands. The two you'll use constantly:

$ docker compose up        # build (if needed) and start every service
$ docker compose up -d     # same, but detached (in the background)
$ docker compose down      # stop and remove all the containers & network

docker compose up reads the file, creates the network, builds or pulls each image, and starts every service in dependency order — the whole stack, alive, from one command. docker compose down tears it all back down cleanly (leaving named volumes intact, so your data survives). The rest you'll reach for often:

$ docker compose logs -f web    # follow one service's logs (like tail -f)
$ docker compose ps             # list this app's running services
$ docker compose exec web bash  # open a shell inside a running service
$ docker compose build          # rebuild images after a Dockerfile change

Notice logs -f and exec ... bash are the container-world versions of skills straight from the Linux guide — following logs with tail -f, and opening a shell to poke around inside. Compose scopes them neatly to your app's services.

What Compose is and isn't

One honest boundary to set, because it matters for the final chapter. Docker Compose is built for a single machine — your laptop in development, or a small deployment on one server. It's brilliant there. But it does not handle running your app across many machines, automatically restarting containers that crash across a cluster, or scaling a service to fifty copies spread over a fleet. That job — orchestration at scale — belongs to bigger tools like Kubernetes, which the next chapter introduces. Compose is the right tool for development and simple setups; it's not trying to be the production-cluster tool, and knowing that line keeps you from misusing it.

The local story is complete

With Compose, you've reached a genuinely capable place: you can describe an entire multi-service application declaratively, bring it up or down with a single command, and hand it to a whole team who all get the identical environment. For local development, that's the full picture — everything from the last five chapters, assembled into a workflow you'd actually enjoy using.

Two chapters remain, and they're about leaving your laptop. First: how do images get shared — pushed to a registry so a teammate or a server can pull them? And then the big one: what genuinely changes when containers go to production — the real internet, real traffic, real uptime — and where tools like Kubernetes enter the story. That's where the guide has been heading all along.

Docker Compose: Your Whole App in One File | Code Safari