Docker Compose
Running multiple containers
A real app is not just one container. You have the app, a database, maybe a Redis cache, a reverse proxy. Each runs in its own container. Docker Compose defines and runs multi-container applications with a single YAML file.
docker-compose.yml
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=/data/app.db
volumes:
- app-data:/data
depends_on:
- redis
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
restart: unless-stopped
volumes:
app-data:
redis-data: What each section does
services — Each service is a container. app is your Node.js app (built from the local Dockerfile). redis uses a pre-built image from Docker Hub.
build: . — Build the image from the Dockerfile in the current directory. Alternatively, use image: myapp:latest to pull a pre-built image.
ports — Same as docker run -p. Maps host port to container port.
environment — Same as docker run -e. Sets environment variables. For secrets, use env_file: instead.
volumes — Named volumes persist data across container restarts. app-data:/data mounts the app-data volume at /data inside the container. Your SQLite database lives here.
depends_on — Start Redis before the app. This controls startup order but does not wait for Redis to be ready (use condition: service_healthy for that).
restart — Same as docker run --restart.
healthcheck — Same as the HEALTHCHECK Dockerfile instruction, but defined in Compose.
Running with Compose
# Start all services
docker compose up -d
# View logs
docker compose logs
docker compose logs app # Just the app
docker compose logs -f # Follow
# Stop all services
docker compose down
# Stop and remove volumes (deletes data!)
docker compose down -v
# Rebuild after code changes
docker compose up -d --build Networks
Docker Compose creates a network for your services automatically. Containers can reach each other by service name:
// Inside the app container, connect to Redis by service name
const redis = createClient({ url: "redis://redis:6379" }); The hostname redis resolves to the Redis container’s IP. No need to know the IP address. Docker’s DNS handles it.
Using env_file for secrets
services:
app:
build: .
env_file:
- .env.production # .env.production
NODE_ENV=production
DATABASE_URL=/data/app.db
JWT_SECRET=your-production-secret [!WARNING] Do not commit
.env.productionto git. Add it to.gitignore. On the server, create it manually or use a secrets manager.
Development vs production compose files
Use separate compose files for development and production:
# docker-compose.yml (development)
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- .:/app # Mount source code for live reload
- /app/node_modules # Exclude node_modules
command: npx tsx watch src/server.ts
environment:
- NODE_ENV=development The development version mounts your source code into the container (volumes: - .:/app) so changes reload without rebuilding. The production version copies the code into the image at build time.
Exercises
Exercise 1: Create docker-compose.yml with your app. Run docker compose up -d. Verify the app works.
Exercise 2: Add Redis as a service. Verify the app can connect to redis://redis:6379 by service name.
Exercise 3: Stop and restart with docker compose down and docker compose up -d. Verify your data persists (it is in the named volume).
How do containers in the same Docker Compose file communicate?