Docker Compose Introduction

Docker Compose lets you describe a multi-container application in one YAML file and manage it with one command. Instead of remembering several long docker run commands for a web app, database, cache, and network, you declare the desired setup once in compose.yaml. Compose matters because most real applications are not one container; they are a small system of services that must start together and communicate predictably.

Overview: How Docker Compose Works

Compose is a client-side orchestration tool built into modern Docker as docker compose. The old standalone docker-compose command was Compose V1 and is legacy; new lessons and new projects should use the space-separated Compose V2 command. Compose reads a project file, usually named compose.yaml, converts it into Docker API calls, and asks the Docker daemon to create containers, networks, volumes, and images.

A Compose file is not an image and it is not a container. It is a declarative description of services. A service is a template for one or more containers. For example, a service named web might use the image nginx:1.27-alpine, publish port 8080, and mount a local file into the container. A service named db might use postgres:16-alpine and store data in a named volume.

When you run docker compose up, Compose chooses a project name, reads the service definitions, creates a default network for that project, creates named volumes if needed, pulls missing images, builds images that use build:, and starts containers. Service names become DNS names on the project network. If a web container needs Redis, it can connect to host redis on the container port, not to localhost. Inside a container, localhost means the same container.

Compose still uses normal Docker internals. Images are read-only stacked layers. Each container gets a thin writable layer. Named volumes are managed by Docker and survive container removal. Bind mounts map a specific host path into a container and are common for local development. Published ports are created only when you use Compose ports:; an image’s EXPOSE line is metadata and does not publish anything by itself.

Syntax

The modern command form is:

docker compose [OPTIONS] COMMAND [ARGS]
Part Meaning
docker compose The Compose V2 CLI plugin included with current Docker installations.
-f compose.yaml Use a specific Compose file. If omitted, Compose searches for common names such as compose.yaml.
-p myproject Set the project name used in generated container, network, and volume names.
up Create and start services. Add -d for detached mode.
down Stop and remove project containers and networks. Add --volumes only when you also want named volumes removed.
ps List containers in the project.
logs Show service logs. Add -f to follow.
exec Run a command inside an already running service container.

A small Compose file usually has this shape:

services:
  service-name:
    image: image-name:tag
    ports:
      - "8080:80"
    volumes:
      - volume-name:/path/in/container
volumes:
  volume-name:
Key Use
services The top-level map of service names to container settings.
image The image to run. Prefer specific tags such as nginx:1.27-alpine.
build Build an image from a Dockerfile before starting the service.
ports Publish host ports to container ports, like 8080:80.
environment Set runtime environment variables. Do not put real secrets in source-controlled files.
volumes Attach named volumes or bind mounts.
depends_on Start one service before another. It does not prove the dependency is ready to accept traffic.

Examples

Run One Web Service

services:
  web:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"
docker compose up -d
docker compose ps
docker compose down

Output:

[+] Running 2/2
 ✔ Network demo_default  Created
 ✔ Container demo-web-1  Started
NAME         IMAGE               COMMAND                  SERVICE   STATUS         PORTS
demo-web-1   nginx:1.27-alpine   "/docker-entrypoint..."   web       Up 5 seconds   0.0.0.0:8080->80/tcp
[+] Running 2/2
 ✔ Container demo-web-1  Removed
 ✔ Network demo_default  Removed

This file defines one service named web. Compose creates a project network, starts one Nginx container, and publishes host port 8080 to container port 80. Visiting http://localhost:8080 reaches Nginx. The down command removes the project container and network, but it does not remove the pulled image.

Add a Persistent Database

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: changeme
      POSTGRES_USER: appuser
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"
volumes:
  pgdata:
docker compose up -d
docker compose logs db
docker compose down

Output:

[+] Running 3/3
 ✔ Network demo_default  Created
 ✔ Volume demo_pgdata    Created
 ✔ Container demo-db-1   Started
db-1  | database system is ready to accept connections
[+] Running 2/2
 ✔ Container demo-db-1   Removed
 ✔ Network demo_default  Removed

The named volume pgdata stores PostgreSQL data outside the container writable layer. Removing the container with docker compose down does not delete that volume, so the database files are still available next time. Use docker compose down --volumes only when you intentionally want to delete the project volumes too. The password shown here is an obvious local-development placeholder; real secrets should come from a secret manager, an ignored environment file, Docker secrets, or your deployment platform.

Build an App and Connect It to Redis

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      REDIS_HOST: redis
    depends_on:
      - redis
  redis:
    image: redis:7.2-alpine
docker compose up --build -d
docker compose exec app node --version
docker compose down

Output:

[+] Building 8.4s (10/10) FINISHED
[+] Running 3/3
 ✔ Network demo_default    Created
 ✔ Container demo-redis-1  Started
 ✔ Container demo-app-1    Started
v20.20.2
[+] Running 3/3
 ✔ Container demo-app-1    Removed
 ✔ Container demo-redis-1  Removed
 ✔ Network demo_default    Removed

The Dockerfile uses a pinned base image tag and installs dependencies before copying the full source tree. That ordering protects the dependency layer from being invalidated every time application source changes. The Compose file builds the app image, starts Redis from a registry image, and places both services on the same project network. The app should connect to Redis using host redis, because Compose provides service-name DNS on that network.

How It Works Step by Step

  1. Compose finds and parses compose.yaml, then merges any files you explicitly pass with -f.
  2. Compose determines a project name from the directory name unless you set one with -p or a project configuration.
  3. For each service, Compose checks whether it needs to pull an image or build one from a Dockerfile.
  4. The Docker daemon stores pulled or built images as read-only layers. For built images, normal build-cache rules apply: changing one Dockerfile instruction invalidates that layer and every later layer.
  5. Compose creates the project’s default network. Containers attached to that network can resolve each other by service name.
  6. Compose creates named volumes declared under top-level volumes: and attaches them to the requested container paths.
  7. Compose creates and starts containers with labels that record their project and service membership.
  8. When you run docker compose down, Compose finds resources with those labels, stops containers, removes containers and project networks, and leaves named volumes unless you request --volumes.

Compose does not turn Docker into a full production orchestrator by itself. It is excellent for local development, demos, integration tests, and small single-host deployments. Larger production systems usually need health-aware orchestration, rolling updates, secret management, scheduling, and monitoring from a platform such as Kubernetes, Docker Swarm, or a cloud service.

Common Mistakes

Using localhost Between Containers

services:
  app:
    image: myapp:1.0
    environment:
      REDIS_HOST: localhost
  redis:
    image: redis:7.2-alpine

This is wrong because localhost inside the app container points back to the app container itself. Use the Compose service name instead:

services:
  app:
    image: myapp:1.0
    environment:
      REDIS_HOST: redis
  redis:
    image: redis:7.2-alpine

Confusing EXPOSE with Published Ports

FROM nginx:1.27-alpine
EXPOSE 80

EXPOSE documents that the image expects port 80, but it does not publish that port to the host. In Compose, publish with ports::

services:
  web:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"

Deleting Database Data by Removing Volumes

docker compose down --volumes

This command removes named volumes for the project. That is useful for resetting local state, but dangerous if the volume contains data you wanted to keep. Use plain docker compose down when you only want to remove containers and networks.

Expecting depends_on to Mean Ready

depends_on controls startup order, not application readiness. A database container can be started while the database process is still initializing. Real applications should retry connections, and more advanced Compose files can add health checks when startup readiness matters.

Best Practices

  • Name services after their role, such as web, api, db, or redis; those names become useful DNS names.
  • Use specific image tags like postgres:16-alpine and redis:7.2-alpine instead of latest.
  • Commit compose.yaml, but do not commit real secrets. Use obvious placeholders in examples and project-specific secret handling in real work.
  • Use named volumes for persistent service data and bind mounts for editing local source code during development.
  • Keep host port publishing minimal. Services that only need to talk to each other can stay on the Compose network without a ports: entry.
  • Use docker compose logs -f, docker compose ps, and docker compose exec as your first debugging tools.
  • Prefer docker compose down for cleanup; add --volumes only for an intentional data reset.
  • When using build:, keep Dockerfile cache behavior in mind and include a .dockerignore in real projects.

Practice Exercises

  1. Create a compose.yaml with one web service using nginx:1.27-alpine and publish it on host port 8090. Expected end state: docker compose ps shows the service running and http://localhost:8090 reaches Nginx.
  2. Add a redis service using redis:7.2-alpine. Do not publish Redis to the host. Hint: another service on the same Compose network can still connect to hostname redis.
  3. Create a Compose file for PostgreSQL with a named volume. Start it, stop it with docker compose down, then start it again. Expected end state: the named volume is reused rather than recreated.

Summary

  • Docker Compose describes multi-container applications in compose.yaml.
  • The modern command is docker compose, not the legacy docker-compose.
  • Services are container templates; Compose creates containers, networks, and volumes from them.
  • Service names act as DNS names on the Compose project network.
  • ports: publishes host ports; EXPOSE alone does not.
  • Named volumes persist data beyond container removal unless you remove volumes explicitly.
  • Compose is a practical default for local development and integration testing of multi-service apps.