Docker Compose Basics
A foundational guide to Docker Compose, covering its architecture, `docker-compose.yml` structure, common commands, and best practices for multi-container application development.
Docker Compose simplifies the orchestration of multi-container Docker applications. Instead of managing individual containers via docker run commands, Compose allows you to define your entire application stack in a single YAML file, streamlining deployment, scaling, and management. This guide covers the essential aspects of Docker Compose, from its core concepts to practical examples and troubleshooting.
Docker Compose is typically installed alongside Docker Desktop or as a standalone Python package. As of Docker Desktop 3.0+, it's often included as the docker compose (without a hyphen) plugin, superseding the older docker-compose (with a hyphen) command. This article will primarily use the newer docker compose syntax.
Understanding the docker-compose.yml Structure
The heart of any Compose project is the docker-compose.yml file. This YAML file describes the services that make up your application, their configurations, networks, and volumes. A typical structure looks like this:
version: '3.8'
services:
web:
image: nginx:alpine
ports:
- "8080:80"
depends_on:
- db
networks:
- app_net
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
POSTGRES_DB: mydatabase
volumes:
- dbdata:/var/lib/postgresql/data
networks:
- app_net
volumes:
dbdata:
networks:
app_net:
driver: bridge
Top-Level Keys
version: Specifies the Compose file format version. Using'3.8'or higher is recommended for access to the latest features.services: Defines the individual containers that constitute your application. Each service entry corresponds to one container configuration.volumes: Declares named volumes that can be shared between services and persist data beyond container lifecycles.networks: Defines custom networks for inter-container communication, offering better isolation and naming.
Service-Specific Directives
Each service within the services section can have numerous configuration options:
image: The Docker image to use for the service (e.g.,nginx:alpine,my-custom-app:1.0).build: Specifies a path to a Dockerfile if you need to build a custom image (e.g.,build: ./app). Can also take an object withcontextanddockerfilekeys.ports: Maps host ports to container ports (e.g.,"8080:80"maps host port 8080 to container port 80).environment: Sets environment variables within the container (e.g.,POSTGRES_PASSWORD: example). Can also reference an.envfile or host environment variables.volumes: Mounts host paths or named volumes into the container (e.g.,dbdata:/var/lib/postgresql/data).networks: Attaches the service to one or more defined networks.depends_on: Expresses dependency between services, ensuring they start in a specific order. Note: This only ensures startup order, not service readiness.restart: Defines the restart policy (e.g.,no,always,on-failure,unless-stopped).command: Overrides the default command specified by the image.entrypoint: Overrides the default entrypoint specified by the image.env_file: Specifies one or more files to load environment variables from.
Common Docker Compose Commands
The docker compose command is your primary interface for managing your application stack.
docker compose up
This command builds, (re)creates, starts, and attaches to containers for your services.
docker compose up -d: Starts services in detached mode (in the background). This is the most common way to deploy.docker compose up --build: Forces Compose to rebuild images for services that have abuildinstruction, even if they haven't changed.docker compose up --force-recreate: Recreates all containers, even if their configuration hasn't changed. Useful for ensuring a fresh start.docker compose up [SERVICE_NAME]: Starts only a specific service and its dependencies.
docker compose down
Stops and removes containers, networks, and volumes created by up.
docker compose down: Stops and removes containers and default networks.docker compose down -v: Also removes named volumes declared in thevolumessection ofdocker-compose.yml. Use with caution as this deletes persistent data!docker compose down --rmi all: Removes images used by any service, in addition to containers, networks, and volumes.
docker compose ps
Lists all running services defined in the docker-compose.yml.
$ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
myapp-db-1 postgres:16 "docker-entrypoint.s..." db 5 seconds ago running 5432/tcp
myapp-web-1 nginx:alpine "/docker-entrypoint.s..." web 5 seconds ago running 0.0.0.0:8080->80/tcp
docker compose logs
Displays log output from services.
docker compose logs: Shows logs from all services.docker compose logs -f: Follows log output in real-time (liketail -f).docker compose logs web: Shows logs only for thewebservice.docker compose logs --tail 100: Displays the last 100 lines of logs.
docker compose exec
Executes an arbitrary command inside a running service container.
$ docker compose exec db psql -U postgres mydatabase
Password for user postgres:
psql (16.2 (Debian 16.2-1.pgdg120+1))
Type "help" for help.
mydatabase=#
docker compose build
Builds or rebuilds services defined with a build instruction.
docker compose build web
Networks and Volumes
Proper management of networks and volumes is crucial for robust Compose applications.
Networks
By default, Compose creates a single default bridge network for your application. All services are attached to this network and can reach each other by their service names. For more complex setups, defining custom networks offers better control and isolation.
For example, to create a custom bridge network named app_net:
networks:
app_net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
Then, attach services to it:
services:
web:
networks:
- app_net
Volumes
Volumes are the preferred mechanism for persisting data generated by Docker containers. Named volumes are managed by Docker and are typically created in /var/lib/docker/volumes/ on Linux hosts. Bind mounts, which map a host directory directly into a container, are also supported but less portable.
volumes:
dbdata:
driver: local # default
driver_opts:
type: 'none'
o: 'bind'
device: '/path/to/host/data/for/db'
The above example shows how to configure a named volume to use a specific host path as a bind mount for more direct control, though this is less common than letting Docker manage the named volume completely.
Common Pitfalls and Troubleshooting
- Port Conflicts: If
docker compose upfails with an error likeError starting userland proxy: listen tcp 0.0.0.0:8080: bind: address already in use, it means a process on your host machine (or another container) is already using the specified host port. Change the host port in yourportsmapping (e.g.,"8081:80") or stop the conflicting process. - Service Readiness vs. Startup Order:
depends_ononly ensures that dependent services start in order, not that they are fully ready to accept connections (e.g., a database might be starting but not yet listening on its port). For production, consider using health checks (Compose v3.4+) or application-level retry logic. - Data Persistence: Forgetting
-vwithdocker compose downmeans your named volumes (and thus your data) will persist. This is usually desired for databases. However, if you want a clean slate, remember to add-v. - YAML Indentation: YAML is whitespace-sensitive. Incorrect indentation will lead to parsing errors. Use a linter or a good editor with YAML support.
- Environment Variables: Ensure environment variables are correctly loaded. If using an
.envfile, it must be in the same directory asdocker-compose.ymlby default, or specified with--env-file. Remember that variables defined inenvironmentblock within the service take precedence overenv_file. - Image Pull Errors: If an image fails to pull (e.g.,
repository does not exist or may require 'docker login'), verify the image name and tag, and ensure you are logged into any necessary private registries.