Docker Compose enables declarative orchestration of multi-container applications with isolated networks, persistent volumes, and health-dependent startup sequences on a single VPS. This guide covers production-ready architectures linking web services, background workers, and databases.
Complex cloud orchestration tools like Kubernetes introduce unnecessary operational overhead for small to medium workloads. A well-structured Docker Compose setup delivers 90% of the reliability benefits on a simple $10-$20 monthly VPS without the management complexity.
Core Components of Multi-Container Systems
A typical fullstack application consists of multiple interacting services.
**Reverse Proxy (Nginx / Caddy)**: Handles SSL termination, routes traffic to internal services, and serves static files.
**Web Application API**: Serves dynamic HTTP/WebSocket endpoints (Node.js, Python, or Go).
**Database (PostgreSQL / MySQL)**: Manages persistent relational state with isolated volume mounts.
**Cache / Message Queue (Redis)**: Accelerates session lookups and brokers background job queues.
**Background Worker**: Processes asynchronous tasks (emails, report generation, video transcoding) independently of web request lifecycles.
Production Docker Compose Architecture
Below is a complete, production-hardened `docker-compose.yml` linking a web API, PostgreSQL database, Redis cache, and background worker.
yaml
version: "3.8"
networks:
frontend:
driver: bridge
backend:
driver: bridge
volumes:
postgres_data:
driver: local
redis_data:
driver: local
app_uploads:
driver: local
services:
proxy:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
- app_uploads:/var/www/uploads:ro
networks:
- frontend
depends_on:
api:
condition: service_healthy
api:
build:
context: ./api
dockerfile: Dockerfile
restart: unless-stopped
environment:
- NODE_ENV=production
- PORT=3000
- DATABASE_URL=postgres://appuser:${DB_PASSWORD}@postgres:5432/appdb
- REDIS_URL=redis://redis:6379
volumes:
- app_uploads:/app/uploads
networks:
- frontend
- backend
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s
worker:
build:
context: ./api
dockerfile: Dockerfile
command: ["node", "dist/worker.js"]
restart: unless-stopped
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://appuser:${DB_PASSWORD}@postgres:5432/appdb
- REDIS_URL=redis://redis:6379
networks:
- backend
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: appdb
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"]
volumes:
- redis_data:/data
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
```Network Isolation Principles
The compose file establishes two distinct network layers for security.
**frontend network**: Contains the reverse proxy and web API. The proxy communicates with the API, but has zero network access to the database or Redis.
**backend network**: Contains the web API, background worker, PostgreSQL, and Redis. The database is never exposed to the public internet or the frontend reverse proxy.
This prevents direct database compromise even if a vulnerability exists in the reverse proxy layer.
Service Dependency and Health Checks
A frequent bug in multi-container setups is starting the web API before the database is ready to accept connections.
The naive `depends_on: ["postgres"]` only waits until the container starts, not until the database process is actually initialized. Using `condition: service_healthy` solves this race condition:
yaml
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
```The API container will pause startup until `pg_isready` succeeds and Redis returns `PONG`, eliminating startup crash loops.
Persistent Storage Management
Containers are ephemeral; their internal state disappears upon restart. Named volumes preserve critical data across deployments.
**postgres_data**: Stores database tables and indexes on the host disk.
**redis_data**: Stores append-only persistence logs so cache warm state survives reboots.
**app_uploads**: Shared volume allowing the API container to write uploaded files and the Nginx proxy to serve them directly without application overhead.
Back up named volumes regularly using automated snapshot tools or host-level file copies.
Environment Variable Security
Store secrets in a `.env` file that is excluded from version control via `.gitignore`:
# .env (add to .gitignore)
DB_PASSWORD=a_very_strong_random_password_here_12345
REDIS_PASSWORD=another_strong_random_token_here_67890
Docker Compose automatically interpolates variables from `.env` into your YAML configuration using `${VARIABLE_NAME}` syntax.
Deploying and Managing via M.A.F Cloud
Managing multiple containers across different VPS instances becomes tedious with raw terminal commands. M.A.F Cloud simplifies multi-container operations.
Connect your VPS using the one-line installer:
curl -fsSL https://cexi.my.id/agent-install.sh | sudo MAFCLOUD_TOKEN=<token> MAFCLOUD_API=https://cexi.my.id bash
With M.A.F Cloud, you can:
Run individual application containers alongside Minecraft servers on the same VPS
Allocate custom TCP and UDP ports with protocol verification (TCP/UDP/BOTH)
Monitor container CPU and memory metrics in real time via live WebSocket dashboards
Enable automated AI self-healing and circuit-breaker crash recovery to keep services online 24/7
Create scheduled snapshot backups before deploying risky schema migrations
Operational Best Practices
Maintain high availability with these operational routines.
### Graceful Rolling Deployments
Rebuild and update containers with zero downtime:
docker compose pull
docker compose up -d --no-deps --build api
The `--no-deps` flag ensures that dependent services like PostgreSQL and Redis are not unnecessarily restarted during an API code update.
### Log Rotation Configuration
Unbounded Docker logs will eventually consume all VPS disk space. Configure global log limits in `/etc/docker/daemon.json`:
json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
```### Monitoring Resource Usage
Inspect real-time container resource consumption with standard tooling:
docker stats --no-stream
Ensure total container memory limits do not exceed 80% of available VPS physical RAM to leave headroom for OS buffers and caching.
Summary Checklist
Multi-container setups give you production-grade separation of concerns without cloud vendor lock-in.
Verify network isolation between frontend and database tiers
Ensure all services define valid health checks and depends_on conditions
Test automated container restart on VPS reboots
Store database backups in offsite storage locations regularly