Back to blog
DockerDevOpsContainersArchitecture

Docker Compose Multi-Container Setup — Production Architecture

· 9 min · By M.A.F Cloud Team

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

Ready to host your own server?

Deploy your first Minecraft or App server in about 30 seconds.