Multi-Container Apps: Building Complete Applications
Real applications have multiple services. Database. Cache. Application. Web server. They need to work together. Docker Compose makes that easy.
🎯 The Big Picture​
Think of multi-container apps like a restaurant. You have kitchen (database). You have storage (cache). You have chefs (application). You have servers (web server). They all work together. One can't work without others.
Docker Compose orchestrates all these services. Makes them work together. As one complete application.
What is a Multi-Container App?​
A multi-container app has multiple services working together:
Typical stack:
- Database - Stores data
- Cache - Speeds up access
- Application - Business logic
- Web Server - Serves requests
Each is a container. All work together. That's a multi-container app.
Real-World Example: Web Application Stack​
Let me show you a complete stack:
services:
# PostgreSQL Database
postgres:
image: postgres:14-alpine
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: myapp
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
# Redis Cache
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
command: redis-server --appendonly yes
networks:
- backend
# Node.js Application
app:
build: .
environment:
NODE_ENV: production
DB_HOST: postgres
DB_PORT: 5432
DB_USER: appuser
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: myapp
REDIS_HOST: redis
REDIS_PORT: 6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
networks:
- backend
- frontend
restart: unless-stopped
# Nginx Web Server
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- app
networks:
- frontend
restart: unless-stopped
volumes:
postgres-data:
redis-data:
networks:
frontend:
backend:
What this does:
- Complete web application
- Database with persistence
- Cache for performance
- Application with dependencies
- Web server for serving
- All connected. All working together.
Service Communication​
Services communicate using service names:
In the application code:
// Connect to database
const db = connect('postgres', 5432);
// 'postgres' is the service name!
// Connect to Redis
const redis = connect('redis', 6379);
// 'redis' is the service name!
How it works:
- Docker Compose creates a network
- Services can reach each other by name
- Automatic DNS resolution
- No IP addresses needed
Think of it as: All services in same building. They know each other's room numbers (names). Can visit each other easily.
The Restaurant Analogy​
Think of multi-container apps like a restaurant:
Database (postgres): Kitchen storage
- Stores ingredients (data)
- Persistent (refrigerator)
- Always available
Cache (redis): Quick access storage
- Fast access (counter)
- Temporary (prepared items)
- Speeds up service
Application (app): Chefs
- Prepare dishes (process requests)
- Use ingredients (database)
- Use prepared items (cache)
- Create final product
Web Server (nginx): Waiters
- Serve customers (handle requests)
- Get orders from chefs (app)
- Present final dish (serve response)
All work together. One can't work without others. That's a multi-container app.
Dependency Management​
Services depend on each other:
services:
db:
image: postgres:14
app:
depends_on:
- db
What depends_on does:
- Starts
dbbeforeapp - Ensures order
- But doesn't wait for readiness
With health checks:
services:
db:
image: postgres:14
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 10s
app:
depends_on:
db:
condition: service_healthy
Now waits for db to be healthy, not just started.
Network Isolation​
Separate networks for different layers:
networks:
frontend: # Public-facing
backend: # Internal services
Why:
- Security (isolate public from private)
- Organization (clear separation)
- Control (who can access what)
Frontend network:
- Web server (nginx)
- Public access
Backend network:
- Database (postgres)
- Cache (redis)
- Application (app)
- Internal only
Think of it as: Restaurant has dining area (frontend) and kitchen (backend). Customers in dining area. Kitchen is private.
Volume Management​
Persistent data for each service:
volumes:
postgres-data: # Database data
redis-data: # Cache data
Why separate volumes:
- Isolation
- Easy backup
- Easy restore
- Clear ownership
Each service has its own data. Organized. Manageable.
Scaling Services​
Scale specific services:
# Scale app to 3 instances
docker compose up -d --scale app=3
What happens:
- Creates 3 app containers
- All share same network
- Load balanced (if configured)
- Database and cache shared
Useful for:
- High traffic
- Load distribution
- Availability
Note: Not all services can scale. Database usually can't (use replication instead).
My Take: Multi-Container Apps​
I build multi-container apps daily. Here's what I learned:
Start simple:
- Get one service working
- Add another
- Test connections
- Iterate
Use proper structure:
- Separate networks
- Named volumes
- Health checks
- Dependencies
Test everything:
- Each service individually
- Service communication
- Complete stack
- Failure scenarios
The key: Build incrementally. Test constantly. Structure properly.
Memory Tip: The Restaurant Analogy​
Multi-container apps = Restaurant
Database: Kitchen storage Cache: Quick access Application: Chefs Web Server: Waiters Networks: Dining area vs kitchen Volumes: Storage areas
Once you see it this way, multi-container apps make perfect sense.
Common Mistakes​
- Not using health checks: Dependencies don't work properly
- Wrong network setup: Services can't communicate
- Not isolating networks: Security issues
- Shared volumes incorrectly: Data conflicts
- Not testing dependencies: Services start in wrong order
Hands-On Exercise: Build a Multi-Container App​
1. Create docker-compose.yml:
services:
db:
image: postgres:14-alpine
environment:
POSTGRES_PASSWORD: secret
volumes:
- db-data:/var/lib/postgresql/data
app:
image: node:18-alpine
command: node -e "console.log('App running, DB:', process.env.DB_HOST)"
environment:
DB_HOST: db
depends_on:
- db
volumes:
db-data:
2. Run it:
docker compose up
3. Check logs:
docker compose logs app
# Should show: App running, DB: db
4. Verify connection:
docker compose exec app ping db
# Should work! Service discovery!
5. Clean up:
docker compose down -v
Key Takeaways​
- Multi-container apps have multiple services - Database, cache, app, web server
- Services communicate by name - Automatic DNS resolution
- Use dependencies - Ensure proper startup order
- Isolate networks - Security and organization
- Use separate volumes - Data isolation and management
What's Next?​
Now that you understand multi-container apps, let's learn about environment variables in Compose. Next: Environment Variables.
Remember: Multi-container apps are like restaurants. Multiple services work together. Each has a role. All connected. All orchestrated by Compose.