Docker Compose lets you define and run multi-container applications with a single YAML file, where services are the containers, volumes provide persistent storage, and networks enable communication between them. This article explains how these three core concepts work together, how to configure them, and how they map to Docker’s underlying primitives.
Background: What Docker Compose Solves
Running a modern application often requires multiple containers: a web server, a database, a cache, a message queue. Managing them individually with docker run commands becomes tedious and error-prone. Docker Compose, a tool included with Docker Desktop and installable separately on Linux, lets you declare the entire stack in a docker-compose.yml file. One command, docker compose up, creates all the necessary containers, networks, and volumes. This declarative approach makes setups reproducible and shareable—commit the YAML file to version control, and any teammate can spin up the same environment.
Services: The Building Blocks
In Compose, a service is a definition of a container to run. Each service specifies an image (or a build context), environment variables, ports to expose, dependencies, and other runtime options. For example:
services:
web:
image: nginx:latest
ports:
- "8080:80"
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
Here, two services are defined: web and db. Compose will create one container per service by default, but you can scale a service to multiple replicas with docker compose up --scale web=3. Services can also depend on each other using the depends_on attribute, which controls startup order (though it does not wait for the dependency to be ready—only for it to start). For readiness checks, you would use a health check and a tool like wait-for-it.
Volumes: Persistent Data Storage
Containers are ephemeral by design—when a container is removed, its writable layer is lost. Volumes are the recommended way to persist data across container restarts and recreations. Compose supports two types of volumes: named volumes and bind mounts.
Named volumes are managed by Docker and stored in a dedicated directory on the host (e.g., /var/lib/docker/volumes/ on Linux). They are declared in a top-level volumes: section and referenced in a service with a mount path. For example:
services:
db:
image: postgres:15
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
This creates a volume named db-data and mounts it at the container’s data directory. Even if the container is removed, the volume persists. Bind mounts, on the other hand, map a host directory directly into the container, e.g., ./src:/app. They are useful for development (live code reload) but are less portable because they depend on the host filesystem path.
Networks: How Containers Talk
By default, Compose creates a single network for your project, and all services join it. Containers on the same network can communicate using their service name as the hostname. For instance, a web service can connect to a database using db:5432. This internal DNS resolution is automatic and isolated from the host network.
You can define custom networks in the top-level networks: section and attach services to them. This is useful for segmentation—for example, a backend network for database and API, and a frontend network for the web server. Services can be attached to multiple networks, but a container cannot be on two networks with the same name. To expose a service to the outside world, you map ports with ports: (e.g., "8080:80"), which binds the container port to a host port. Alternatively, you can use expose: to make a port available only to linked services on the same network, without publishing it to the host.
How Compose Implements These Under the Hood
When you run docker compose up, Compose translates the YAML into Docker API calls. It creates the networks first, then volumes, then containers. It assigns each container a static IP address within the network and sets up DNS aliases based on service names. The depends_on option is implemented by starting containers in dependency order, but it does not wait for the dependency to be healthy—that requires a health check condition (available in Compose spec).
Compose also sets a project name (defaults to the directory name) that prefixes all resources: myproject_db, myproject_default. This prevents collisions when multiple projects run on the same host. You can override the project name with -p or the COMPOSE_PROJECT_NAME environment variable.
Choosing Between Volumes and Bind Mounts
Choosing between named volumes and bind mounts depends on your use case. Named volumes are easier to back up and migrate, and they work seamlessly across different host environments. They are the recommended choice for database data, uploads, and other persistent application state. Bind mounts are convenient for development because they reflect changes from your host editor immediately. However, they introduce host-specific paths, which can break portability. A common pattern is to use bind mounts for source code and named volumes for database data.
Another option is tmpfs mounts, which store data in memory and are wiped when the container stops—useful for temporary files that should never be persisted.
Common Problems and Troubleshooting
One frequent issue is the depends_on not waiting for the database to be ready. The solution is to add a health check to the database service and use the condition: service_healthy syntax in depends_on. For example:
services:
db:
image: postgres:15
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
web:
build: .
depends_on:
db:
condition: service_healthy
Another common problem is port conflicts when two services try to bind the same host port. Compose will fail with an error; you can change the host port or use different ports for each service.
Networking issues often arise when a service is not on the same network as the one it needs to talk to. Check that both services are attached to the same network, and use service names (not localhost) in connection strings.
Frequently Asked Questions
What is the difference between docker-compose and docker compose?
The hyphenated docker-compose is the older standalone Python tool, while docker compose (with a space) is the newer plugin integrated into Docker CLI. They are largely compatible, but the plugin version is recommended for new projects and receives more frequent updates.
How do I persist data with Docker Compose?
Use a named volume in the service definition and declare it in the top-level volumes: section. For example, db-data:/var/lib/postgresql/data will keep the database data even after docker compose down (without -v).
Can I use Docker Compose in production?
Yes, Compose can be used in production, but for large-scale deployments, orchestration tools like Kubernetes or Docker Swarm offer better scaling, self-healing, and rolling updates. Compose is ideal for single-host deployments and development environments.
How do I update a service without recreating the whole stack?
Run docker compose up -d --no-deps <service_name> to recreate only that service. If you have changed the image or configuration, Compose will detect the change and recreate the container.
Why can’t my container connect to another service?
Ensure both services are on the same network. Check the network configuration in your YAML and verify that you are using the service name as the hostname (e.g., db instead of localhost). Also, confirm that the target service is listening on the expected port.
What is the difference between ports and expose?
ports publishes the container port to the host, making it accessible from outside the Docker network. expose only makes the port available to linked services on the same network, and it does not publish it to the host. Use expose for internal communication and ports for external access.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.
More cards in this drawer
- Mac Start Up Automatically After Power Failure?
- Expose Self-Hosted Service Securely Without Cloudflare Tunnel
- Nano Delivery for Functional Foods: What It Can and Can’t Do4.00
- Disable Apple Watch Turn-by-Turn Vibration While Driving
- Convert Play FLAC in iTunes: Full Guide
- Soda Foundation Charter: What It Means