What Docker Compose Solves
Let me paint a picture you might recognize.
You've got a media server running in one Docker container. You decide to add a database for it — that's a second container. Then you want a download manager to feed content into the media server — third container. Each one needs its own docker run command with port mappings, volume mounts, and environment variables.
Pretty soon, you're pasting five long commands into your terminal every time you restart your server. And heaven forbid you need to change a port — now you're updating commands across multiple containers in the right order.
That's exactly what Docker Compose fixes.
Docker Compose lets you describe every container, its settings, and how they connect to each other inside a single YAML file called docker-compose.yml. Then you start everything with one command: docker compose up. Stop everything: docker compose down. No more command-line memory games.
Compose is a tool for people who want to run multi-app setups without losing their minds. If you've ever thought "there has to be an easier way," this is it.
Docker vs Docker Compose: What's the Difference
The difference is straightforward:
- Docker runs one container at a time. You use docker run to pull an image and start a container from the command line.
- Docker Compose orchestrates multiple containers that work together. You define all of them in a file and run them as a group.
Here's a quick comparison:
| Feature | Docker Alone | Docker Compose |
|---|---|---|
| Number of containers | One per command | Many from one file |
| Configuration | Command-line flags | YAML file |
| Starting order | Manual | Handles dependencies |
| Restarting after reboot | Each container separately | One command starts everything |
| Commands to learn | docker run, docker start, etc. | docker compose up, down, logs |
If you're running a single container, Docker alone is fine. But once you have two or three containers that depend on each other — a database plus a web app, for instance — Compose is the way to go.
Installing Docker Compose
Before you install Compose, you need Docker itself. My Docker beginner guide walks through the installation from scratch.
On Ubuntu/Debian Linux, install the Compose plugin with apt:
# Update your package list
sudo apt update
# Install Docker Compose v2
sudo apt install docker-compose-v2
# Verify the installation
docker compose version
On Ubuntu 24.04, this installs version 2.40.3 or newer. The key thing to notice is the command is docker compose — with a space. The old hyphenated version docker-compose is outdated.
On Windows or macOS, Docker Compose comes bundled when you install Docker Desktop. Once Docker Desktop is running, open your terminal and test it:
docker compose version
If you see a version number, you're good to go.
Your First docker-compose.yml
A Compose file is plain text formatted as YAML. It describes the services (containers) you want to run, their images, ports, and settings.
Let's build a real example: a simple web server (Nginx) that shows a page, connected to a tiny database (Redis) for counting visits.
Create a new directory and open a file called docker-compose.yml:
# docker-compose.yml
# Services section defines each container
services:
# First service: a web server
web:
# Which image to pull from Docker Hub
image: nginx:alpine
# Map host port 8080 to container port 80
ports:
- "8080:80"
# Mount a local folder into the container
volumes:
- ./html:/usr/share/nginx/html
# This service can be reached by other containers
# using the service name "web" as the hostname
# Second service: a Redis database
redis:
image: redis:alpine
# Expose Redis's default port to the host
ports:
- "6379:6379"
Let me break down each part:
- services: The section that lists all your containers. Each service gets a name (like web or redis).
- image: The Docker image to use. nginx:alpine is the lightweight version of the Nginx web server. redis:alpine is a small in-memory database.
- ports: Maps a port on your host machine (left side) to a port inside the container (right side). "8080:80" means your host's port 8080 routes to the container's port 80. Keep the quotes — without them YAML can misread "8080:80" as a key-value pair instead of a string.
- volumes: Keeps files or data outside the container. This line mounts a local html folder into the container's web root.
This Compose file describes two services that work together. The web server can talk to Redis using the hostname redis, because Compose sets up a network where services find each other by their names.
Starting, Stopping, and Checking Your Apps
Now let's run this thing. Navigate to the directory where your docker-compose.yml lives and run:
# Start all services in detached mode (background)
docker compose up -d
The -d flag runs everything in the background so your terminal stays free.
# Check the status of all services
docker compose ps
This shows which containers are running, which ports they're using, and their status.
# View logs from all containers
docker compose logs
# View logs from just the web service
docker compose logs web
# Follow logs in real time (like tail -f)
docker compose logs -f
When you want to stop everything:
# Stop and remove containers, but keep data volumes
docker compose down
docker compose down stops the containers and removes them, but it leaves your volumes untouched. That means any data your apps generated stays safe.
If you want to remove everything including volumes:
# Stop and remove containers AND delete volumes (caution!)
docker compose down -v
The -v flag deletes volumes, so only use it when you're sure you want to wipe all your app data.
Keeping Your Data: Volumes Explained
Containers are temporary. When you delete a container, any files written inside it are gone. That's a big problem for databases, user uploads, or any data you want to keep.
Volumes solve this. They store data outside the container, on your host machine, so it survives container deletion.
In the example above, we used a bind mount — a direct path on your host machine mapped into the container:
volumes:
- ./html:/usr/share/nginx/html
This maps a folder called html in your current directory to the container's web root. Any files you put in ./html show up in the container.
For data that you don't need to access directly, named volumes are cleaner:
services:
db:
image: postgres:16
volumes:
- postgres_data:/var/lib/postgresql/data
# Top-level volumes declaration
volumes:
postgres_data:
Here's what's happening:
- postgres_data is a named volume.
- Docker manages the actual storage location on your host.
- docker compose down doesn't delete named volumes.
- Only docker compose down -v removes them.
Named volumes are the safer option for app data because Docker handles the paths for you.
Managing Settings with .env Files
Hard-coding database passwords or API keys in your docker-compose.yml is a bad idea — especially if you share that file. Instead, use a .env file to inject environment variables.
Create a file called .env in the same directory as your compose file:
# .env file
DB_USER=myappuser
DB_PASSWORD=S3cr3tP@ssw0rd
DB_NAME=myapp
Now reference these variables in your docker-compose.yml:
services:
app:
image: your-app:latest
environment:
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=${DB_NAME}
# Provide a default in case the .env file is missing
- APP_PORT=${APP_PORT:-3000}
Docker Compose loads variables from .env automatically. The ${VAR:-default} syntax gives you a fallback value if the variable isn't set.
This keeps secrets out of your compose file and makes it easy to run the same setup with different credentials.
Common Problems (And How to Fix Them)
Port already in use. You see an error like port is already allocated. Another process on your host is using the port you mapped. Change the host-side port in your compose file. Instead of "8080:80", use "8081:80". Or find and stop the process using the port.
Permission errors with volumes. When you mount a local folder, the container runs with a specific user ID. If the container can't write to your folder, you'll get permission errors. The fix depends on your setup, but a common approach is to create the folder first and set permissions:
mkdir -p html
chmod 755 html
Configuration changes don't take effect. You updated docker-compose.yml but nothing changed. Compose doesn't automatically restart containers when you edit the file. After making changes, run:
docker compose down
docker compose up -d
This recreates the containers with the new settings.
Orphaned containers still running. You used docker compose up in one terminal, closed it, and now the containers are lingering. Run docker compose down from the same directory to stop them. If they're orphaned, you can see them with docker ps -a and remove them with docker rm.
FAQ
Is Docker Compose free?
Yes, Docker Compose is free and open source. It's included with Docker Desktop and available as a plugin for Docker Engine on Linux. There are no licensing fees or subscriptions needed to use it personally or even in most commercial environments.
What is the difference between Docker and Docker Compose?
Docker runs a single container from a command. Docker Compose runs multiple containers described in a YAML file. Think of Docker as the engine and Compose as the control panel that tells many engines what to do and how to work together.
Where is my app's data stored?
With bind mounts, data lives wherever you mounted it — often a folder like ./data in your project directory. With named volumes, Docker stores the data in its managed storage area, typically /var/lib/docker/volumes/ on Linux. You don't normally need to access named volumes directly — just reference them by name in your compose file.
Can I use Docker Compose on a Raspberry Pi?
Yes, as long as the images you use support ARM architecture. Many official images offer ARM-compatible tags, often with -arm64 or -alpine variants. Install Docker and the Compose plugin on your Raspberry Pi using the apt method described earlier. Then use the same compose files you'd use on any other Linux system.
How do I update my services?
To pull newer images and restart your containers, run:
docker compose pull
docker compose up -d
The pull command fetches the latest version of each image. Then up -d recreates the containers with those fresh images. For major version updates, check the app's release notes — configuration changes might be required.
Next Steps
Now that you've got Docker Compose working, here are a few guides that put it to practical use:
- Docker beginner guide — if this article felt fast-paced, start here for the absolute basics of Docker itself.
- Jellyfin home media server — a complete walkthrough that uses Docker Compose to deploy a media server.
- Old PC home server guide — turn your old computer into a permanent home server, and use Compose to manage all your services.
- CasaOS beginners guide — see what happens when someone builds a graphical interface on top of Docker Compose; every app you click in CasaOS generates a compose config behind the scenes.
All code in this article was tested and runs successfully on Docker 29.1.3 + Docker Compose 2.40.3 (Ubuntu 24.04) — nginx:alpine and redis:alpine started together with docker compose up -d, inter-service DNS by service name, named-volume data persistence across down/up, and .env variable injection all verified on a live server — verified August 2026.