21.10. Docker Compose
Table of Contents
Why Docker Compose Exists
Running a single container with docker run is simple. Running a real backend system is not.
A typical backend needs:
- An application container, for example FastAPI
- A database, for example PostgreSQL
- A cache, for example Redis
- Maybe a message broker, for example Redis or RabbitMQ
Starting all these with plain docker run commands quickly becomes:
- Hard to remember
- Hard to reproduce on another machine
- Hard to change consistently
Docker Compose solves this by letting you define all services in one file and start them with a single command.
Key idea: Docker Compose is a tool to define and run multi-container Docker applications using a single configuration file, usually docker-compose.yml.
You still use Docker images and containers. Compose simply orchestrates them for you.
The `docker-compose.yml` File
Docker Compose uses a YAML file, usually named docker-compose.yml, to describe:
- What services (containers) your app needs
- What images they use
- What ports they expose
- What environment variables they need
- What volumes they mount
- How they depend on each other
Minimal `docker-compose.yml` Example
Here is a very simple example with a FastAPI app and PostgreSQL:
version: "3.9"
services:
app:
image: my-fastapi-app:latest
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/app_db
depends_on:
- db
db:
image: postgres:16
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=app_db
ports:
- "5432:5432"You can start everything with:
docker compose up
(Typically the newer command is docker compose, older installs use docker-compose.)
Understanding Services
In Compose, each service represents one type of container.
In the example above, we have 2 services:
app: Your backend applicationdb: Your PostgreSQL database
Docker Compose will create containers with names based on:
- The directory name
- The service name
- An index
For example, in a project folder named myproject, Compose might create:
myproject_app_1myproject_db_1
You can see running containers with:
docker psBuilding Images with Compose
Instead of using an existing image with image:, you can ask Compose to build an image using a Dockerfile.
Example directory:
backend/
Dockerfile
app/
main.py
docker-compose.yml
docker-compose.yml:
version: "3.9"
services:
app:
build: ./backend
ports:
- "8000:8000"This tells Compose:
- Use the
Dockerfilein./backendto build an image - Then run a container from that image as the
appservice
You can also combine build with image to give the built image a name:
services:
app:
build: ./backend
image: my-fastapi-app:devThis is useful if you want to push the image to a registry later.
Defining Service Configuration
Each service has its own configuration. Here are common options you will use for backend development.
Ports
ports maps a host port to a container port:
services:
app:
ports:
- "8000:8000"This means:
- Host port 8000
- Forwards to container port 8000
You can also map different ports:
services:
app:
ports:
- "8080:8000"
Now you access the app at http://localhost:8080, but the app inside the container still listens on port 8000.
Environment Variables
Compose supports two formats for environment variables.
List format:
services:
app:
environment:
- ENV=development
- DATABASE_URL=postgresql://postgres:postgres@db:5432/app_dbMap format (often easier to read):
services:
app:
environment:
ENV: development
DATABASE_URL: postgresql://postgres:postgres@db:5432/app_db
You can also load variables from an .env file:
.env:
POSTGRES_PASSWORD=supersecret
POSTGRES_DB=app_db
docker-compose.yml:
services:
db:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
Compose will substitute ${POSTGRES_PASSWORD} and ${POSTGRES_DB} from .env.
Volumes
Volumes allow you to:
- Persist data, for example database data
- Mount code from your machine into the container for live development
Example for PostgreSQL data persistence:
services:
db:
image: postgres:16
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:Here:
db_datais a named volume defined at the bottom- It is mounted inside the container at
/var/lib/postgresql/data
Example for mounting source code into a FastAPI container:
services:
app:
build: ./backend
volumes:
- ./backend:/app
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
ports:
- "8000:8000"This is very useful in development:
- You edit files on your host in
./backend - The changes appear inside the container at
/app - Uvicorn with
--reloadrestarts on code changes
Command and Entrypoint
You can override the default command defined in the Docker image.
Example:
services:
app:
build: ./backend
command: uvicorn app.main:app --host 0.0.0.0 --port 8000This is common if you want different commands for development and production, for example:
- Development:
uvicorn ... --reload - Production:
gunicorn -k uvicorn.workers.UvicornWorker ...
Service Dependencies and Networks
depends_on
depends_on is used to declare that one service depends on another.
Example:
services:
app:
build: ./backend
depends_on:
- db
db:
image: postgres:16This means:
- Compose will start
dbbeforeapp - It does not wait until
dbis fully ready, only until the container is started
For real production readiness checks, you usually add your own health checks or retry logic in the app.
Service-to-Service Networking
Compose automatically creates a network for your project. Each service can reach other services by their service name.
In this configuration:
services:
app:
build: ./backend
environment:
DATABASE_URL: postgresql://postgres:postgres@db:5432/app_db
db:
image: postgres:16
The connection string uses db as the host, not localhost:
postgresql://postgres:postgres@db:5432/app_db
This works because inside the app container:
- The hostname
dbresolves to thedbcontainer
Common Docker Compose Commands
Here are the commands you will use most often in development.
| Command | Description |
|---|---|
docker compose up | Start all services, attach logs |
docker compose up -d | Start in detached mode (run in background) |
docker compose down | Stop and remove containers, default network |
docker compose build | Build or rebuild service images |
docker compose ps | List containers in the current project |
docker compose logs | Show logs from all services |
docker compose logs app | Show logs only from the app service |
docker compose restart app | Restart only the app service |
docker compose exec app bash | Open a shell inside the app container |
Example workflow for local development:
# Start services
docker compose up -d
# View logs
docker compose logs -f app
# Execute a command inside app container
docker compose exec app python -m pytest
# Stop and clean up
docker compose downExample: FastAPI + PostgreSQL + Redis with Compose
Here is a more realistic example for a backend application that uses:
- FastAPI app
- PostgreSQL for data
- Redis for caching or sessions
Directory structure:
project/
backend/
Dockerfile
app/
main.py
docker-compose.yml
backend/Dockerfile (simplified):
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
docker-compose.yml:
version: "3.9"
services:
app:
build: ./backend
image: fastapi-backend:dev
ports:
- "8000:8000"
environment:
ENV: development
DATABASE_URL: postgresql://postgres:postgres@db:5432/app_db
REDIS_URL: redis://redis:6379/0
depends_on:
- db
- redis
db:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_db
ports:
- "5432:5432"
volumes:
- db_data:/var/lib/postgresql/data
redis:
image: redis:7
ports:
- "6379:6379"
volumes:
db_data:How this works:
appcan connect to PostgreSQL using hostdbappcan connect to Redis using hostredis- PostgreSQL data is persisted in the
db_datavolume - You can run the whole stack with one command:
docker compose upDevelopment vs Production Use
You can use Docker Compose both for:
- Local development
- Simple deployments
Typical patterns:
- Development:
- Mount local source code as a volume for fast edits
- Use
--reloadfor automatic app reloads - Expose database ports to your host for tools like pgAdmin or a SQL client
- Production (small setups, or as part of a larger deployment):
- Build images with production settings
- Use environment files or secret management for credentials
- Avoid exposing database ports publicly
Example development-specific settings:
services:
app:
build: ./backend
volumes:
- ./backend:/app
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
ports:
- "8000:8000"In production you might:
- Remove the volume mount
- Use Gunicorn instead of
uvicorn --reload - Adjust environment variables for production settings
Summary
- Docker Compose lets you describe multi-container applications in a single
docker-compose.ymlfile. - Each service represents a type of container, for example
app,db,redis. - Compose handles:
- Building images from
Dockerfiles - Port mappings
- Environment variables
- Volumes
- Basic service dependencies
- Networking between services by name
- You control the whole stack with a few commands like
docker compose upanddocker compose down.
As you build more complex backends, Compose will be one of your primary tools for local development and small deployments.
Views: 9
KAHIBARO