Docker Compose Basics
Docker Compose is a YAML-based tool to define and run multi-container Docker applications.
Concept explained
At its simplest, a docker-compose.yml lists services. Each service maps to an image or a build context, optional ports, environment variables, volumes and dependencies.
Key pieces:
- services: named containers that make up the app (web, db, redis).
- volumes: persistent storage (databases, uploads).
- networks: how services talk to one another (default network is created automatically).
- environment / env_file: configure services.
- depends_on: controls start order but not readiness – use healthchecks if you need service readiness.
Prefer the modern CLI command docker compose (no hyphen) when using Docker Compose v2. Some systems may still support docker-compose.
Step-by-step example
Below is a minimal app with a simple Node web service and Postgres. Put this docker-compose.yml in your project root.
docker-compose.yml:
services:
web:
build: . # builds using ./Dockerfile
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://postgres:password@db:5432/app
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: password
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
Minimal Dockerfile for the web service (Node example):
FROM node:18-slim
WORKDIR /app
COPY package.json ./
COPY app.js ./
EXPOSE 3000
CMD ["npm", "start"]
Common commands:
- Build and start in foreground:
docker compose up --build - Start in detached mode:
docker compose up -d - Stop and remove:
docker compose down - View logs:
docker compose logs -f web - Run a one-off command:
docker compose exec web sh
docker compose up flags explained
--build: rebuild images before starting.-d: run in detached mode.--remove-orphans: remove services not defined in the compose file.--force-recreate: recreate containers even if config unchanged.-por--project-name: specify an alternate project name (default is directory name).
Setting the Working Directory
You can override the working directory inside the container using working_dir (or workdir in older versions).
services:
app:
image: node:18
working_dir: /app/src
command: node index.js
This is equivalent to WORKDIR /app/src in a Dockerfile or -w /app/src in docker run.
docker compose up --build (what it does)
docker compose up --build rebuilds images if the Dockerfile or context changed, then starts containers.
This is the recommended command during active development.
docker compose up vs docker run
docker runlaunches one container.docker compose uplaunches many, wires networks, volumes, environment, and ensures dependency order. Use run for single tests; Compose for app stacks.
docker compose up -d (detached mode)
-d starts services in the background.
Use docker compose logs -f to stream logs afterward.
Variations & gotchas
- Healthchecks: depends_on does not wait for app readiness. Add a
healthcheckto the DB and usecondition: service_healthyin older compose specs or implement a retry in the app. - .env files: Compose supports an
.envfile; avoid committing secrets. See Environment variables & Secrets for patterns. - Bind mounts vs named volumes: bind mounts are handy for local development; use named volumes for persistent production data.
- Compose file version: modern versions of Docker Compose automatically use the latest schema, so you don't need to specify a version in your compose file.
- Scaling:
docker compose up --scale worker=3works for stateless services, not for stateful DBs.
Don't rely on Compose as a production orchestrator for large, multi-host clusters. For small apps Compose is often sufficient.
Common mistakes
- Exposing databases publicly by publishing DB ports (e.g.,
- "5432:5432"). Avoid unless needed. - Forgetting volumes for databases – leads to data loss when containers are removed.
- Using
latestimage tags in production – makes rollbacks and reproducibility hard. - Expecting
depends_onto guarantee the dependent service is ready (it only controls start order). - Committing
.envor credentials to version control.
Best practices
- Pin image versions (e.g.,
postgres:15) for reproducible runs. - Use named volumes for stateful services and bind mounts for local dev.
- Keep small services and one responsibility per container.
- Add a sensible
restartpolicy (e.g.,unless-stopped). - Use healthchecks for services that other services depend on.
- Put secrets in a secret manager or use environment files kept out of VCS.
When to use / when not to use
When to use:
- Local development, CI tasks, small staging or production apps.
- Single-host deployment or simple multi-container stacks.
- Fast iteration workflows where Docker Compose reduces infra overhead.
When not to use:
- Large, multi-node clusters requiring service discovery, autoscaling, or advanced scheduling – use orchestration (e.g., Kubernetes).
- When you need multi-region deployments and complex network policies.
Key takeaways
- Docker Compose ties services, networks and volumes in a single file so you can run multi-container apps with one command.
- Use named volumes for persistent data, pin image versions, and add healthchecks for readiness.
docker compose up --buildstarts the app;docker compose downremoves it – remember volumes persist unless removed explicitly.- Compose is great for local and small-scale deployments but not a drop-in replacement for full orchestrators.