Docker Compose Demo - Multi-Container Application¶
Overview¶
- This lab demonstrates a production-ready multi-container application using Docker Compose.
- You’ll learn how to orchestrate three interconnected services (web server, application, and database) with proper networking, volume management, and service dependencies.
What You’ll Build¶
-
A complete 3-tier application architecture:
Layer Technology Description Frontend Layer Nginx High-performance web server and reverse proxy Application Layer Node.js JavaScript runtime for scalable network applications Database Layer PostgreSQL Advanced relational database with ACID compliance
Key Features¶
- Multi-container orchestration with Docker Compose
- Isolated networks for frontend and backend communication
- Persistent data storage using Docker volumes
- Service dependencies to ensure proper startup order
- Environment variable configuration for flexibility
- Restart policies for high availability
- Detailed explanations of each section of the Docker Compose file
- Hands-on exercises to deploy and test the application
- Best practices for production-ready Docker Compose setups
Learning Objectives¶
By the end of this lab, you will:
- Understand the complete structure of a Docker Compose file
- Master service definitions and configuration
- Implement multi-network architecture for security
- Configure persistent data storage with volumes
- Manage service dependencies and startup order
- Apply best practices for environment variables
- Understand restart policies for high availability
Prerequisites¶
Before starting this lab, ensure you have:
- Docker Engine 19.03.0+ installed
- Docker Compose v1.27+ or Docker Compose V2
- Completed Lab 001-intro
- Basic understanding of web applications
- Text editor for viewing YAML files
Architecture Overview¶
- Our sample application uses a 3-tier architecture with network isolation:
graph LR
Client[👤 Client]
subgraph Frontend["Frontend Layer<br/>webnet Network"]
web[**Nginx**<br/>Web Server<br/>Port 8080]
app1[**Node.js**<br/>Application<br/>Port 3000]
end
subgraph Backend["Backend Layer<br/>dbnet Network"]
app2[**Node.js**<br/>Application<br/>Port 3000]
db[**PostgreSQL**<br/>Database<br/>Port 5432]
end
subgraph Storage["Data Layer"]
volume[**db-data**<br/><br/>Volume<br/>Persistent Storage]
end
Client --> web
Client --> app1
web --> app1
app1 -.Same Container.-> app2
app2 --> db
db --> volume
style Frontend fill:none,stroke:#2196f3,stroke-width:3px,padding:10px
style Backend fill:none,stroke:#00c853,stroke-width:3px,padding:10px
style Storage fill:none,stroke:#7c4dff,stroke-width:3px,padding:10px Key Design Principles:¶
Network Segmentation
Isolation Strategy: Our architecture implements separate networks for different application tiers to enhance security and performance.
Benefits¶
- Security Boundaries: Web and database tiers are isolated, preventing direct access
- Traffic Control: Network policies can be applied at each layer
- Fault Isolation: Issues in one network don’t affect others
- Scalability: Each network can be scaled independently
Implementation¶
- The
webnetnetwork handles frontend communication, whiledbnetsecures backend database access. - This creates a defense-in-depth strategy where the database is shielded from direct external access.
Service Isolation
Security First: The database is not directly accessible from the web service, enforcing the principle of least privilege.
Architecture Benefits¶
- Attack Surface Reduction: Database cannot be reached directly from the internet
- Access Control: Only the application layer can communicate with the database
- Audit Trail: All database access flows through the application, enabling logging
- Compliance: Meets security standards for multi-tier applications
Real-world Impact:¶
- If the web service is compromised, attackers cannot directly access the database.
- They must first breach the application layer, providing an additional security boundary.
Data Persistence
Reliability: Named volumes ensure your data survives container restarts, updates, and even host migrations.
Persistence Features:¶
- Container Independence: Data persists even when containers are deleted
- Easy Backups: Docker provides built-in tools for volume backup and restore
- Performance: Optimized for database workloads with efficient I/O
- Portability: Volumes can be moved between hosts
Use Cases:¶
- Perfect for databases, user uploads, application state, logs, and any data that must survive container lifecycle events.
- Our PostgreSQL data is stored in the
db-datavolume, ensuring no data loss during updates or restarts.
Scalability
Growth Ready: Each service can be independently scaled based on demand, allowing efficient resource utilization.
Scaling Strategies:¶
- Horizontal Scaling: Add more web or app instances behind a load balancer
- Vertical Scaling: Increase resources (CPU/RAM) for specific services
- Independent Scaling: Scale services without affecting others
- Resource Optimization: Allocate resources where they’re needed most
Example Scenario:¶
- During high traffic, scale up web and app services while keeping a single database instance.
- Use
docker-compose up --scale app=3to run three application instances, all connecting to the same database through the shared network.
Docker Compose File Structure¶
A Docker Compose file is structured into several key sections, each serving a specific purpose in defining your multi-container application:
| Section | Purpose | Required | Key Properties |
|---|---|---|---|
version | Declares Compose file format version | ✅ Yes | Version number (3.9, 3.8, etc.) |
services | Defines all containers in your application | ✅ Yes | image, build, ports, environment, volumes, networks, depends_on, restart |
networks | Creates custom networks for service communication | ❌ Optional | Network name, driver, ipam, internal |
volumes | Defines named volumes for data persistence | ❌ Optional | Volume name, driver, driver_opts, external |
configs | Manages configuration files as first-class resources | ❌ Optional | Config name, file, external |
secrets | Stores sensitive data (passwords, tokens, keys) | ❌ Optional | Secret name, file, external |
Section Details¶
services - The core of your Compose file where you define each container:
- Each service can use an
imagefrom a registry or bebuildfrom a Dockerfile - Configure container behavior with environment variables, volumes, networks, and restart policies
- Define dependencies between services using
depends_on
networks - Create isolated network segments:
- Services on the same network can communicate using service names
- Implements network segmentation for security
- Default bridge driver for single-host deployments
volumes - Persistent data storage:
- Data survives container restarts and removals
- Managed by Docker with automatic cleanup capabilities
- Better performance than bind mounts on Mac/Windows
configs & secrets - Secure configuration management:
- Configs for non-sensitive configuration files
- Secrets for sensitive data with encryption at rest
- Both are mounted into containers as files
Let’s break down the docker-compose-sample.yml file section by section:
File Version Declaration¶
-
Purpose: Declares the Compose file format version
-
Details:
- Version
3.9supports all modern Docker Compose features - Compatible with Docker Engine 19.03.0+
- Enables features like
depends_on, custom networks, and named volumes - Higher versions support additional features (GPU access, device mapping, etc.)
- Version
-
Version Compatibility:
Version Docker Engine Key Features 3.9 19.03.0+ Full feature set, build secrets 3.8 19.03.0+ Max memory, CPU limits 3.7 18.06.0+ Init process, isolation modes 3.0-3.6 1.13.0+ Basic orchestration
Services Section¶
-
Purpose: The
servicessection defines all containers in your application
Web Service (Nginx)¶
-
Purpose: Serves as a reverse proxy and static content server
-
Configuration Breakdown:
Property Value Description imagenginx:latestOfficial Nginx image from Docker Hub ports8080:80Map host port 8080 → container port 80 environmentNGINX_HOST,NGINX_PORTRuntime configuration variables volumes./web-data:/usr/share/nginx/htmlMount local files for serving networkswebnetConnect to frontend network restartalwaysAlways restart if stopped -
Environment Variables:
NGINX_HOST=localhost: Sets the server name in Nginx configurationNGINX_PORT=80: Internal port Nginx listens on
-
Volume Mount Explained:
-
Port Mapping:
-
Access URL:
http://localhost:8080
App Service (Node.js Application)¶
-
Purpose: The application layer that handles business logic
-
Configuration Breakdown:
Property Value Description build.context./appDirectory containing Dockerfile build.dockerfileDockerfileDockerfile name (optional if default) container_nameapp-containerCustom name for easy identification ports3000:3000Map host port 3000 → container port 3000 environmentNODE_ENV=productionSet Node.js to production mode depends_ondbWait for database container to start networkswebnet,dbnetConnect to both networks restarton-failureOnly restart on error -
Build Context Explained:
-
Multi-Network Configuration:
webnet: Communicates with Nginx (receives HTTP requests)dbnet: Communicates with PostgreSQL (database queries)
-
Depends On Behavior:
-
Environment Variable Usage:
-
Access URL:
http://localhost:3000
Database Service (PostgreSQL)¶
-
Purpose: Persistent data storage layer
-
Configuration Breakdown:
Property Value Description imagepostgres:latestOfficial PostgreSQL image container_namedb-containerCustom container name environmentPostgreSQL credentials Initial database setup volumesdb-datavolumePersistent data storage networksdbnetBackend network only restartunless-stoppedRestart automatically unless manually stopped -
PostgreSQL Environment Variables:
-
Security Best Practice:
-
Volume Persistence:
graph TB Create[Create Container] Stop[Stop Container] Remove[Remove Container] Recreate[Recreate Container] Write[Data written to<br/>/var/lib/postgresql/data] Volume[(db-data<br/>Volume)] Persists[Data persists!] Restore[Data restored<br/>from volume] Create --> Write Create --> Stop Stop --> Remove Remove --> Recreate Write --> Volume Volume --> Persists Persists --> Restore Restore --> Recreate style Create stroke:#ff6f00,stroke-width:2px style Stop stroke:#c2185b,stroke-width:2px style Remove stroke:#7b1fa2,stroke-width:2px style Recreate stroke:#2e7d32,stroke-width:2px style Write stroke:#1565c0,stroke-width:2px style Volume stroke:#0288d1,stroke-width:3px style Persists stroke:#388e3c,stroke-width:2px style Restore stroke:#f57f17,stroke-width:2px -
Connection String (from app service):
Volumes Section¶
-
Purpose: Named volumes for data persistence
-
Volume Details:
Property Description db-dataNamed volume managed by Docker Storage Location /var/lib/docker/volumes/db-data/_data(Linux)Lifecycle Survives container deletion Backup Can be backed up using docker volumecommands -
Volume Operations:
# List volumes docker volume ls # Inspect volume docker volume inspect 002-compose-demo_db-data # Backup volume docker run --rm -v 002-compose-demo_db-data:/source -v $(pwd):/backup \ alpine tar czf /backup/db-backup.tar.gz -C /source . # Restore volume docker run --rm -v 002-compose-demo_db-data:/target -v $(pwd):/backup \ alpine tar xzf /backup/db-backup.tar.gz -C /target -
Volume vs Bind Mount:
Feature Named Volume Bind Mount Managed by Docker ✅ Yes ❌ No Location Docker-managed User-specified path Backup tools Built-in Manual Performance Better on Mac/Windows Better on Linux Use case Databases, persistent data Development, code sharing
Networks Section¶
-
Purpose: Custom networks for service communication and isolation
-
Network Architecture:
graph TB subgraph DockerHost["🐳 Docker Host"] subgraph Webnet["webnet (bridge network)<br/>Subnet: 172.18.0.0/16"] web1["**web:8080**<br/>172.18.0.2"] app1["**app:3000**<br/>172.18.0.3"] web1 <--> app1 end subgraph Dbnet["dbnet (bridge network)<br/>Subnet: 172.19.0.0/16"] app2["**app:3000**<br/>172.19.0.2"] db["**db:5432**<br/>172.19.0.3"] app2 <--> db end app1 -.Same Container.-> app2 end style DockerHost fill:none,stroke:#424242,stroke-width:3px,stroke-dasharray: 5 5 style Webnet fill:#e3f2fd,stroke:#2196f3,stroke-width:2px style Dbnet fill:#e8f5e9,stroke:#4caf50,stroke-width:2px style web1 fill:#bbdefb,stroke:#1976d2,stroke-width:2px style app1 fill:#c8e6c9,stroke:#388e3c,stroke-width:2px style app2 fill:#c8e6c9,stroke:#388e3c,stroke-width:2px style db fill:#fff9c4,stroke:#f57f17,stroke-width:2px -
Network Benefits:
-
DNS Resolution: Services can communicate using service names
```bash # From app container curl http://db:5432 # Resolves to db container IP ``` -
Isolation:
webanddbservices cannot directly communicate```bash # From web container curl http://db:5432 # ❌ Cannot resolve - not on same network ``` -
Security: Database is only accessible through the application layer
-
-
Network Communication Table:
From Service To Service Network Can Communicate? web app webnet ✅ Yes app web webnet ✅ Yes app db dbnet ✅ Yes db app dbnet ✅ Yes web db N/A ❌ No db web N/A ❌ No -
Custom Network Configuration (Advanced):
Hands-On Exercises¶
Exercise 1: Deploy the Application¶
Step 1: Navigate to the lab directory
Step 2: Start all services
Expected Output:
Creating network "002-compose-demo_webnet" ... done
Creating network "002-compose-demo_dbnet" ... done
Creating volume "002-compose-demo_db-data" ... done
Creating db-container ... done
Creating app-container ... done
Creating 002-compose-demo_web_1 ... done
Step 3: Verify all services are running
Expected Output:
Name Command State Ports
-------------------------------------------------------------------------
app-container node server.js Up 0.0.0.0:3000->3000/tcp
db-container postgres Up 5432/tcp
002-compose-demo_web_1 nginx -g daemon off; Up 0.0.0.0:8080->80/tcp
Exercise 2: Test Service Communication¶
Test Web Service:
Test App Service:
Check Logs:
# All services
docker-compose -f docker-compose-sample.yml logs
# Specific service
docker-compose -f docker-compose-sample.yml logs app
# Follow logs in real-time
docker-compose -f docker-compose-sample.yml logs -f app
Exercise 3: Inspect Networks¶
List networks:
Inspect webnet:
Verify network connectivity:
# From app container, ping web service
docker exec app-container ping -c 3 web
# From app container, check database connectivity
docker exec app-container pg_isready -h db -U user
Exercise 4: Data Persistence Test¶
Step 1: Create sample data in database
-- Create a test table
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
-- Insert sample data
INSERT INTO users (name, email) VALUES
('John Doe', 'john@example.com'),
('Jane Smith', 'jane@example.com');
-- Verify data
SELECT * FROM users;
-- Exit psql
\q
Step 2: Stop and remove containers
Step 3: Restart services
Step 4: Verify data persists
Expected: Your data should still be there! ✅
Exercise 5: Environment Variable Management¶
Create .env file:
cat > .env << 'EOF'
# Database Configuration
DB_USER=myuser
DB_PASSWORD=securepassword123
DB_NAME=production_db
# Application Configuration
NODE_ENV=production
APP_PORT=3000
# Nginx Configuration
NGINX_HOST=myapp.local
NGINX_PORT=80
EOF
Update docker-compose.yml to use variables:
db:
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
Deploy with environment variables:
🔧 Troubleshooting Guide¶
Issue 1: Port Already in Use¶
Error:
Solution:
# Find process using port 8080
lsof -i :8080
# Kill the process
kill -9 <PID>
# Or change port in docker-compose.yml
ports:
- "8081:80" # Use different host port
Issue 2: Database Connection Failed¶
Error:
Solutions:
- Check if database is ready:
- Add healthcheck to docker-compose.yml:
- Update depends_on with condition:
Issue 3: Volume Permission Issues¶
Error:
Solution:
📊 Service Management Commands¶
Start/Stop Services¶
# Start all services
docker-compose -f docker-compose-sample.yml up -d
# Start specific service
docker-compose -f docker-compose-sample.yml up -d web
# Stop all services
docker-compose -f docker-compose-sample.yml stop
# Stop specific service
docker-compose -f docker-compose-sample.yml stop app
# Restart service
docker-compose -f docker-compose-sample.yml restart app
View Status and Logs¶
# View running services
docker-compose -f docker-compose-sample.yml ps
# View logs
docker-compose -f docker-compose-sample.yml logs
# Follow logs
docker-compose -f docker-compose-sample.yml logs -f app
# View resource usage
docker stats app-container db-container
Clean Up¶
# Stop and remove containers
docker-compose -f docker-compose-sample.yml down
# Remove containers and volumes
docker-compose -f docker-compose-sample.yml down -v
# Remove everything including images
docker-compose -f docker-compose-sample.yml down -v --rmi all
🎓 Key Takeaways¶
After completing this lab, you should understand:
✅ Complete Compose File Structure
- Version declaration and compatibility
- Service definitions and configuration
- Volume and network management
✅ Service Configuration
- Image-based vs build-based services
- Port mapping and networking
- Environment variable management
- Container naming and identification
✅ Networking Architecture
- Multi-network design for security
- Service discovery and DNS resolution
- Network isolation patterns
✅ Data Persistence
- Named volumes vs bind mounts
- Volume lifecycle management
- Backup and restore strategies
✅ Dependency Management
- Service startup order with
depends_on - Health checks for service readiness
- Graceful shutdown handling
✅ Production Best Practices
- Environment-based configuration
- Restart policies for reliability
- Security considerations
🔗 Additional Resources¶
- Docker Compose Documentation
- Compose File Reference
- Docker Networking Guide
- Volume Management
- Best Practices for Compose
➡️ Next Steps¶
Now that you understand multi-container applications, proceed to:
- Lab 003: Deep dive into Docker Compose file structure
- Lab 004: Advanced networking configurations
- Lab 005: Production deployment strategies