Module 5: Docker Compose — Multi-Container Stacks
7. Essential Compose Commands
Description
Docker Compose has a set of commands that cover your stack's entire lifecycle: bringing it up, monitoring it, debugging it, rebuilding it, and shutting it down. Mastering these commands is the difference between "I know docker compose up exists" and "I can operate a multi-service stack with confidence." These are the same commands you'll use every day as an AI Engineer working with development stacks.
Why it matters: In your day-to-day, you don't just bring the stack up once — you bring it up, watch logs, run commands inside a container, rebuild after changing the Dockerfile, restart a service that failed, and you do all of that several times a day. Every minute you save with the right command adds up over weeks and months.
Connection with the module: Capsules 02-06 taught you what to put in the docker-compose.yml. This capsule teaches you how to operate that file — the commands you run in your terminal to interact with your stack.
docker compose up
The most important command
docker compose up
It does everything in one step:
- Creates the default network (if it doesn't exist)
- Creates the declared volumes (if they don't exist)
- Builds the images that have
build:(if they don't exist) - Creates and starts the containers
- Shows logs in the foreground (attached mode)
Essential flags
# Detached: runs in the background, frees up your terminal
docker compose up -d
[+] Running 3/3
✔ Container stack-redis-1 Started
✔ Container stack-chromadb-1 Started
✔ Container stack-api-1 Started
# Build: rebuilds the images before bringing them up
docker compose up --build
It rebuilds every image that has build:. Without this flag, Compose uses the cached image — if you changed your Dockerfile or your code, you won't see the changes.
# The most common combination in development
docker compose up -d --build
Rebuilds and brings it up in the background. The command you'll use the most.
# Bring up a single service (and its dependencies)
docker compose up -d redis
Brings up only Redis. If another service depends on Redis and isn't running, it does NOT bring it up — only what you asked for.
# Force recreate: recreates containers even if they haven't changed
docker compose up -d --force-recreate
Useful when you change environment variables or other configs that don't trigger an automatic recreate.
When to use each variant
| Situation | Command |
|---|---|
| First time | docker compose up -d --build |
| I changed Python code | docker compose up -d --build |
| I changed docker-compose.yml | docker compose up -d |
| I changed .env | docker compose up -d --force-recreate |
| I just want to see logs | docker compose up (without -d) |
| I only need Redis | docker compose up -d redis |
docker compose down
Shut down and clean up
docker compose down
[+] Running 4/4
✔ Container stack-api-1 Removed
✔ Container stack-chromadb-1 Removed
✔ Container stack-redis-1 Removed
✔ Network stack_default Removed
It works in reverse order:
- Stops the containers (reverse dependency order)
- Removes the containers
- Removes the default network
It does not remove: volumes or the images you built.
Important flags
# Remove volumes (Redis data, ChromaDB data, etc.)
docker compose down -v
⚠️ Careful: This wipes out ALL persisted data. Use it only for a complete reset.
# Remove locally built images
docker compose down --rmi local
Removes the images that were built with build:. The images pulled from Docker Hub (Redis, ChromaDB) stay.
# Remove ALL images (including the Docker Hub ones)
docker compose down --rmi all
# Nuclear: remove everything (containers, networks, volumes, images)
docker compose down -v --rmi all
When to use each variant
| Situation | Command |
|---|---|
| Wrapping up for the day | docker compose down |
| Corrupted data, I want a reset | docker compose down -v |
| I want a clean rebuild | docker compose down --rmi local |
| Switched projects, freeing space | docker compose down -v --rmi all |
docker compose logs
See the logs from every service
docker compose logs
api-1 | INFO: Uvicorn running on http://0.0.0.0:8000
api-1 | INFO: Application startup complete.
redis-1 | Ready to accept connections tcp
chromadb-1 | Running Chroma
Essential flags
# Follow: real-time logs (Ctrl+C to exit)
docker compose logs -f
# Logs from a specific service
docker compose logs api
# Follow a specific service
docker compose logs -f api
# The last N lines
docker compose logs --tail 50 api
# With timestamps
docker compose logs -t api
api-1 | 2026-03-08T15:30:01.234Z INFO: Application startup complete.
api-1 | 2026-03-08T15:30:05.678Z INFO: GET /health 200 OK
# Combine: the last 20 lines + follow + timestamps
docker compose logs -f -t --tail 20 api
Filtering logs from several services
# Only api and redis (no chromadb)
docker compose logs -f api redis
The debugging pattern
When something breaks, the typical sequence is:
# 1. Check the state of the services
docker compose ps
# 2. If a service failed, look at its logs
docker compose logs failing-service
# 3. Watch the logs in real time while you reproduce the bug
docker compose logs -f api
# In another terminal: curl http://localhost:8000/broken-endpoint
docker compose ps
Check the state of the services
docker compose ps
NAME IMAGE COMMAND SERVICE STATUS PORTS
stack-api-1 stack-api "uvicorn main:app ..." api Up 5 minutes 0.0.0.0:8000->8000/tcp
stack-chromadb-1 chromadb/chroma:latest "/docker_entrypoin..." chromadb Up 5 minutes 8000/tcp
stack-redis-1 redis:7-alpine "docker-entrypoint..." redis Up 5 minutes 6379/tcp
With health status
docker compose ps
NAME STATUS PORTS
stack-api-1 Up 5 minutes (healthy) 0.0.0.0:8000->8000/tcp
stack-chromadb-1 Up 5 minutes (healthy) 8000/tcp
stack-redis-1 Up 5 minutes (healthy) 6379/tcp
Filtering by state
# Only running services
docker compose ps --status running
# Only stopped services
docker compose ps --status exited
Compact format
docker compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"
NAME STATUS PORTS
stack-api-1 Up 5 minutes (healthy) 0.0.0.0:8000->8000/tcp
stack-chromadb-1 Up 5 minutes (healthy) 8000/tcp
stack-redis-1 Up 5 minutes (healthy) 6379/tcp
docker compose exec
Run commands inside a running container
# An interactive shell in the API
docker compose exec api bash
# Run a specific command
docker compose exec api python3 -c "print('hello from container')"
# The Redis CLI
docker compose exec redis redis-cli
127.0.0.1:6379> PING
PONG
127.0.0.1:6379> KEYS *
1) "cache:sentiment:hello"
2) "model:gpt-4o-mini"
127.0.0.1:6379> exit
# Verify the connection between services
docker compose exec api python3 -c "
import redis
r = redis.Redis(host='redis', port=6379, decode_responses=True)
print(f'PING: {r.ping()}')
print(f'Keys: {r.dbsize()}')
"
Useful flags
# Without a TTY (for scripts)
docker compose exec -T api python3 script.py
# As a specific user
docker compose exec --user root api bash
# With extra environment variables
docker compose exec -e DEBUG=true api python3 app.py
exec vs run
| Command | Purpose |
|---|---|
docker compose exec | Runs in a container that is already running |
docker compose run | Creates a new container to run the command |
# exec: step into the api container that's already running
docker compose exec api bash
# run: create a new container based on the api service
docker compose run --rm api bash
Use exec for debugging running containers. Use run for one-off commands (migrations, tests) in a clean container.
docker compose build
Build/rebuild images
# Build every image that has build:
docker compose build
# Build a single service
docker compose build api
# Without cache (a full rebuild)
docker compose build --no-cache
# With build args
docker compose build --build-arg ENV=production api
When to use build vs up --build
# Build only (doesn't bring anything up)
docker compose build
# Build AND bring up
docker compose up --build -d
docker compose build is useful when you want to verify that the build works without bringing up the whole stack. docker compose up --build is the integrated command for daily development.
docker compose restart
Restart services
# Restart every service
docker compose restart
# Restart a specific service
docker compose restart api
restart vs up --build
| Command | What it does |
|---|---|
docker compose restart api | Stops and restarts the existing container (same image) |
docker compose up -d --build api | Rebuilds the image and recreates the container |
restart is faster but doesn't apply changes in code or the Dockerfile. If you changed something, you need up --build.
docker compose stop / start
Stop without removing
# Stop the services (the containers stay, they can be restarted)
docker compose stop
# Restart the stopped services
docker compose start
stop vs down
| Command | Containers | Network | Volumes |
|---|---|---|---|
docker compose stop | Stopped (they exist) | Exists | They exist |
docker compose down | Removed | Removed | They exist (without -v) |
stop is like hitting pause. down is closing everything.
docker compose config
Validate and see the effective configuration
docker compose config
It shows the "resolved" docker-compose.yml — with every environment variable interpolated, defaults applied, and paths resolved.
name: ai-stack
services:
api:
build:
context: /Users/dev/ai-stack
dockerfile: Dockerfile
depends_on:
redis:
condition: service_healthy
environment:
OPENAI_API_KEY: sk-proj-abc123
REDIS_HOST: redis
ports:
- mode: ingress
target: 8000
published: "8000"
protocol: tcp
redis:
healthcheck:
test:
- CMD
- redis-cli
- ping
interval: 5s
timeout: 3s
retries: 5
image: redis:7-alpine
Validate before bringing it up
docker compose config --quiet
# No output = valid YAML
# Error output = there are problems
# Just see the defined services
docker compose config --services
api
redis
chromadb
# Just see the volumes
docker compose config --volumes
redis-data
chromadb-data
docker compose pull
Download/update images
# Pull the latest images for services with image:
docker compose pull
# Just one service
docker compose pull redis
Useful when you want to make sure you have the latest version of redis:7-alpine or chromadb/chroma:latest.
The Development Workflow
The daily cycle
┌──────────────────┐
│ Edit code │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ up -d --build │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ logs -f api │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Test (curl/ │
│ browser) │
└────────┬─────────┘
│
┌─────────┴────────┐
│ │
Works? Doesn't work?
│ │
▼ ▼
┌─────────┐ ┌──────────┐
│ Commit │ │ exec / │
│ & push │ │ logs / │
└─────────┘ │ debug │
└────┬─────┘
│
└──► Back to editing
The typical sequence: a morning of work
# 1. Bring the stack up
docker compose up -d --build
# 2. Check that everything is running
docker compose ps
# 3. Watch the logs in a separate terminal
docker compose logs -f api
# 4. Make changes in the code...
# (you edit Python files)
# 5. Rebuild and relaunch
docker compose up -d --build
# 6. Test
curl http://localhost:8000/health
# 7. If something fails, debug
docker compose logs api
docker compose exec api bash
# 8. At the end of the day
docker compose down
The debugging sequence
# Something broke. Which services are running?
docker compose ps
# Look at the logs from the service that failed
docker compose logs chromadb
# Is it a configuration error? Validate the YAML
docker compose config --quiet
# Do I need to step into the container?
docker compose exec api bash
# Do I need to restart just one service?
docker compose restart api
# Do I need a clean rebuild?
docker compose down
docker compose up -d --build
Quick Reference
Every command in one table
| Command | Purpose | Common flags |
|---|---|---|
docker compose up | Bring the stack up | -d, --build, --force-recreate |
docker compose down | Shut down and clean up | -v, --rmi local/all |
docker compose logs | See the logs | -f, --tail N, -t, service |
docker compose ps | State of the services | --status running/exited |
docker compose exec | Run in a running container | -T, --user, -e |
docker compose run | Run in a new container | --rm, -e |
docker compose build | Build images | --no-cache, service |
docker compose restart | Restart services | service |
docker compose stop | Stop without removing | service |
docker compose start | Start the stopped ones | service |
docker compose config | Validate the YAML | --quiet, --services, --volumes |
docker compose pull | Update images | service |
Cheatsheet: the 5 commands you'll use the most
docker compose up -d --build # Bring up/rebuild
docker compose logs -f api # Watch logs in real time
docker compose ps # State of the services
docker compose exec api bash # Step into a container
docker compose down # Shut everything down
Troubleshooting
"no configuration file provided: not found"
There's no docker-compose.yml in the current directory:
ls docker-compose.yml compose.yml 2>/dev/null
# If neither exists, you're in the wrong directory
pwd
"service 'api' failed to build"
An error in your Dockerfile. Try the manual build to see the complete error:
docker build -t test-build .
"Bind for 0.0.0.0:8000 failed: port is already allocated"
Another process is using port 8000:
lsof -i :8000
# Identify the process and kill it, or change the port in docker-compose.yml
docker compose up hangs
Possible causes:
- A service has a health check that never passes
- A service crashes and Compose keeps waiting
# Ctrl+C to cancel, then check the state
docker compose ps
docker compose logs problem-service
Code changes aren't showing up
You forgot --build:
# ❌ Doesn't rebuild
docker compose up -d
# ✅ Rebuilds the images
docker compose up -d --build
Exercises
Exercise 1: The complete cycle
Create a stack with Redis, bring it up, check it with ps, write data with exec, look at the logs, and shut it down with down.
See solution
# docker-compose.yml
services:
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
# Bring it up
docker compose up -d
# Check it
docker compose ps
# Write data
docker compose exec redis redis-cli SET greeting "Hello Docker Compose"
# Read data
docker compose exec redis redis-cli GET greeting
# Look at the logs
docker compose logs redis
# Shut it down
docker compose down
Exercise 2: Rebuild after a change
Create a stack with a build: service. Change the code, and show the difference between up -d (no rebuild) and up -d --build (with rebuild).
See solution
# app.py
print("VERSION 1")
FROM python:3.11-slim
COPY app.py .
CMD ["python", "app.py"]
services:
app:
build: .
docker compose up --build
# Output: VERSION 1
# Change app.py to "VERSION 2"
echo 'print("VERSION 2")' > app.py
docker compose up
# Output: VERSION 1 (it didn't rebuild!)
docker compose up --build
# Output: VERSION 2 (it rebuilt!)
docker compose down
Exercise 3: Debugging with exec and logs
Bring up a stack with Redis. Use exec to create data, logs to see the activity, and inspect to check health.
See solution
services:
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
docker compose up -d
sleep 10
# Exec: create data
docker compose exec redis redis-cli SET key1 "value1"
docker compose exec redis redis-cli SET key2 "value2"
docker compose exec redis redis-cli DBSIZE
# (integer) 2
# Logs: see the activity
docker compose logs redis
# PS: see the status
docker compose ps
# redis-1 Up X seconds (healthy)
docker compose down
Exercise 4: Bring up a single service
Create a stack with 3 services. Bring up only one and verify that the others aren't running.
See solution
services:
app:
image: python:3.11-slim
command: sleep 3600
redis:
image: redis:7-alpine
worker:
image: python:3.11-slim
command: sleep 3600
docker compose up -d redis
docker compose ps
# Only redis is Up
# app and worker don't show up (they were never created)
docker compose up -d
# Now all three are Up
docker compose ps
# app-1, redis-1, worker-1 all Up
docker compose down
Exercise 5: down with different flags
Bring up a stack with volumes. Show the difference between down, down -v, and down --rmi local.
See solution
services:
app:
build: .
volumes:
- app-data:/data
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
volumes:
app-data:
redis-data:
# Bring it up
docker compose up -d --build
# Basic down: containers and network removed, volumes and images stay
docker compose down
docker volume ls | grep data # ✅ The volumes exist
docker images | grep app # ✅ The image exists
# Bring it up again
docker compose up -d --build
# down -v: it also removes the volumes
docker compose down -v
docker volume ls | grep data # ❌ Volumes removed
# Bring it up again
docker compose up -d --build
# down --rmi local: it also removes the built images
docker compose down -v --rmi local
docker images | grep app # ❌ Image removed
Summary
docker compose up -d --buildis the most common command — it rebuilds and brings the stack up in the background.docker compose downshuts everything down cleanly. Add-vto remove volumes (data).docker compose logs -f serviceshows logs in real time — essential for debugging.docker compose psshows the state and health of every service.docker compose exec service commandruns commands inside running containers.docker compose buildrebuilds images without bringing the stack up.docker compose restartrestarts without rebuilding (it doesn't apply code changes).docker compose configvalidates your YAML before you bring the stack up — it prevents syntax errors.- The development flow is: edit → up --build → logs → test → repeat.
- You only need to memorize 5 commands:
up,down,logs,ps,exec.
Additional Resources
- Docker Compose CLI Reference — Complete reference for every command
- docker compose up — Detailed documentation for up
- docker compose down — Detailed documentation for down
- docker compose logs — Logging options
- docker compose exec — Running commands in containers
- docker compose build — Build options
- Docker Compose Cheat Sheet (Docker Blog) — Quick-reference PDF
- Docker Compose Watch (File Sync) — Auto-rebuild with file watching