Docker Compose: Production Patterns and Best Practices
Learn production-ready Docker Compose patterns including environment management, custom images, and multi-service architectures
On this page
You know how to run applications with Docker Compose. Now let’s learn patterns you’ll use in real projects.
Using .env Files for Configuration
Hardcoding passwords in docker-compose.yml is fine for learning. For real projects, use .env files.
Create .env in your project root:
# Database
POSTGRES_PASSWORD=mysecretpassword
POSTGRES_DB=myapp
POSTGRES_USER=postgres
# Application
NODE_ENV=development
APP_PORT=3000
# Redis
REDIS_PASSWORD=anothersecret
Update docker-compose.yml to use these variables:
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
volumes:
- postgres_data:/var/lib/postgresql/data
web:
image: node:18-alpine
working_dir: /app
volumes:
- ./app:/app
ports:
- "${APP_PORT}:3000"
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
NODE_ENV: ${NODE_ENV}
depends_on:
db:
condition: service_healthy
volumes:
postgres_data:
Important: Add .env to your .gitignore:
.env
.env.local
.env.production
Commit a .env.example with dummy values instead:
POSTGRES_PASSWORD=changeme
POSTGRES_DB=myapp
POSTGRES_USER=postgres
NODE_ENV=development
APP_PORT=3000
Adding More Services: Redis Cache
Real applications often need caching. Let’s add Redis:
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
web:
image: node:18-alpine
working_dir: /app
volumes:
- ./app:/app
ports:
- "${APP_PORT}:3000"
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
NODE_ENV: ${NODE_ENV}
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
command: sh -c "npm install && npm start"
volumes:
postgres_data:
redis_data:
Update app/package.json to include Redis:
{
"dependencies": {
"express": "^4.18.2",
"pg": "^8.11.0",
"redis": "^4.6.0"
}
}
Now your app can use both PostgreSQL and Redis, all starting with one command.
Building Custom Docker Images
Instead of using node:18-alpine and running npm install every time, create a custom image.
Create app/Dockerfile:
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY . .
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
# Start application
CMD ["node", "index.js"]
Update docker-compose.yml to build this image:
services:
web:
build:
context: ./app
dockerfile: Dockerfile
ports:
- "${APP_PORT}:3000"
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
NODE_ENV: ${NODE_ENV}
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
Build and run:
docker compose up --build
The --build flag rebuilds your custom image.
Separating Development and Production
Create two compose files:
docker-compose.yml (base configuration):
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
web:
build:
context: ./app
dockerfile: Dockerfile
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
NODE_ENV: ${NODE_ENV}
depends_on:
db:
condition: service_healthy
volumes:
postgres_data:
docker-compose.dev.yml (development overrides):
services:
web:
volumes:
- ./app:/app
- /app/node_modules
ports:
- "3000:3000"
command: sh -c "npm install && npm run dev"
environment:
NODE_ENV: development
docker-compose.prod.yml (production overrides):
services:
web:
restart: always
environment:
NODE_ENV: production
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
db:
restart: always
Run with:
# Development
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Or add scripts to package.json:
{
"scripts": {
"docker:dev": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up",
"docker:prod": "docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d"
}
}
Common Multi-Service Patterns
Adding Nginx as Reverse Proxy
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- web
web:
# Remove ports - only nginx needs to expose
expose:
- "3000"
Background Workers
services:
web:
build: ./app
command: npm start
ports:
- "3000:3000"
worker:
build: ./app
command: npm run worker
# No ports needed - background process
depends_on:
- db
- redis
Same code, different commands. Perfect for job queues or scheduled tasks.
Resource Limits
Prevent containers from using all system resources:
services:
web:
build: ./app
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
Logging Configuration
services:
web:
build: ./app
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
This prevents log files from filling your disk.
Essential Debugging Commands
# View logs for all services
docker compose logs
# View logs for specific service
docker compose logs web
# Follow logs in real-time
docker compose logs -f
# Execute commands in running containers
docker compose exec web sh
docker compose exec db psql -U postgres
# Rebuild everything from scratch
docker compose down -v
docker compose build --no-cache
docker compose up
# See what's using resources
docker compose ps
docker compose top
Security Best Practices
- Never commit secrets: Use
.envfiles and add them to.gitignore - Don’t run as root: Add
USER nodeto Dockerfiles - Keep images updated: Regularly rebuild with latest base images
- Scan for vulnerabilities: Use
docker scoutor similar tools - Limit exposed ports: Only expose what’s necessary
- Use specific image tags: Not
latest, usenode:18-alpineorpostgres:15
Performance Tips
Use .dockerignore
Create app/.dockerignore:
node_modules
npm-debug.log
.git
.env
.DS_Store
*.md
.vscode
This excludes unnecessary files from your Docker build context, making builds faster.
Multi-stage Builds
For smaller production images:
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/*.js ./
USER node
EXPOSE 3000
CMD ["node", "index.js"]
When Things Go Wrong
Container exits immediately:
docker compose logs web
Can’t connect to database:
# Check if database is healthy
docker compose ps
# Try connecting manually
docker compose exec db psql -U postgres
Changes not appearing:
# Rebuild the image
docker compose up --build
# Or force recreate
docker compose up --force-recreate
Port conflicts:
# See what's using the port
lsof -i :3000 # macOS/Linux
netstat -ano | findstr :3000 # Windows
What You’ve Accomplished
You started this series knowing nothing about Docker. Now you can:
- Run multi-service applications with Docker Compose
- Configure services with environment variables
- Build custom Docker images
- Separate development and production configurations
- Add caching, reverse proxies, and background workers
- Debug issues effectively
- Apply security and performance best practices
Real-World Usage
This isn’t just for learning. Docker Compose is used for:
- Local development: Run your entire stack locally
- Testing: Spin up test environments in CI/CD
- Staging environments: Deploy to staging servers
- Small production deployments: Single-server applications
For larger production deployments, you’d graduate to Kubernetes or similar orchestration platforms. But the concepts you’ve learned—services, volumes, networks, environment variables—all translate.
Continue Learning
- Docker Documentation: Official guides and references
- Docker Compose Specification: All YAML options explained
- Example Projects: Search GitHub for “docker-compose.yml” in your stack
- Kubernetes: When you’re ready for container orchestration at scale
Final Thoughts
Docker Compose changes how you think about applications. Instead of “install these dependencies,” you think “describe the services I need.” Instead of “configure this manually,” you write it once in a file.
This is infrastructure as code. Reproducible, shareable, version-controlled.
You started as a beginner. Now you have skills that professional developers use daily. Keep building, keep experimenting, and keep learning.
Welcome to the world of containerized applications.