Docker Compose Production Deployment on Linux VPS: Traefik Reverse Proxy, Let’s Encrypt & CI/CD

Docker Compose Production Deployment - Orchestrating Containers with Traefik and Auto-SSL on VPS

Quick Answer & Architecture Summary

How to Deploy Docker Compose with Traefik in Production

Deploying Docker Compose in production on a Linux VPS requires pairing Compose v2 with Traefik v3 as an edge reverse proxy connected over an isolated external bridge network (proxy-tier). Access to the Docker daemon socket is secured by mediating it through a read-only socket proxy (tecnativa/docker-socket-proxy) rather than mounting the raw socket directly into the public ingress. Automated Let’s Encrypt SSL certificates are managed dynamically through container labels, while zero-downtime rolling updates are achieved via Docker health checks and GitHub Actions CI/CD automation over SSH.

🔒 Zero Root Socket Exposure
📜 Automatic Let’s Encrypt SSL
⚡ Zero-Downtime Rolling Updates
🚀 Automated GitHub Actions CI/CD

Deploying containerized microservices in production does not always require the cognitive overhead, operational complexity, and infrastructure expense of a multi-node Kubernetes cluster. For thousands of fast-growing SaaS startups, digital agencies, and engineering teams, running Docker Compose on a dedicated, high-performance Linux VPS provides an exceptionally fast, deterministic, and cost-effective deployment target.

When paired with Traefik as a cloud-native dynamic reverse proxy and Let’s Encrypt for zero-touch SSL certificate automation, Docker Compose transforms a standalone virtual private server into a resilient, self-healing production platform. In this engineering runbook, we provide an end-to-end, battle-tested blueprint for orchestrating production Docker Compose workloads—spanning host hardening, secure Docker daemon socket proxying, automated SSL certificates, zero-downtime rolling updates, and continuous deployment via GitHub Actions.

1. Production Architecture: Edge Routing & Container Isolation Topology

A robust containerized production environment relies on strict separation between public ingress traffic and private internal communication channels. Rather than exposing container ports directly to the host network interface (which bypasses standard host firewall rules), our production topology implements a multi-tier network architecture:

  • Public Edge Ingress (Traefik v3): Binds exclusively to host ports 80 (HTTP) and 443 (HTTPS). It inspects incoming host headers, terminates TLS encryption, and routes traffic over an isolated Docker bridge network.
  • Dedicated Ingress Network (proxy-tier): An externally defined Docker bridge network that connects Traefik to public-facing web containers. Backend services (such as databases and caching layers) never join this network.
  • Isolated Application Networks (app-internal): Private internal networks dedicated to individual application stacks. Databases, Redis instances, and internal microservices communicate exclusively within their private bridge, completely hidden from Traefik and the public Internet.
  • Daemon Security Boundary: The Docker daemon socket (/var/run/docker.sock) is never mounted directly into the public-facing Traefik container. Instead, access is mediated through a restricted, read-only socket proxy.

For organizations managing multi-tier hosting architectures, combining container network boundaries with high-performance cheap VPS compute instances ensures enterprise-grade isolation without enterprise licensing costs.

2. Host Preparation: Linux VPS Hardening, Docker Engine 26+ & UFW Firewall

Before launching container stacks, the underlying Linux VPS host must be properly hardened. Start with a fresh installation of Ubuntu 24.04 LTS or Debian 12 running on high-speed NVMe storage.

Step 1: Deploy Official Docker Engine & Compose v2

Avoid outdated distribution packages. Install the official Docker Engine repository according to the official Docker Compose documentation:

Bash — Host Setup

# Update repositories and install dependencies
sudo apt update && sudo apt install -y ca-certificates curl gnupg ufw

# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Set up official Docker repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install Docker Engine and Compose plugin
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Step 2: Configure Production Daemon Defaults

Create /etc/docker/daemon.json to enforce automated log rotation and prevent disk exhaustion from unconstrained container stdout logs:

JSON — /etc/docker/daemon.json

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "live-restore": true,
  "userland-proxy": false
}

Restart the Docker service to apply the configuration:

Bash — Restart Service

sudo systemctl restart docker
sudo systemctl enable docker

Step 3: Configure UFW Firewall & Mitigate Docker Port Exposure

By default, Docker manipulates Linux iptables directly, which can accidentally expose published container ports. In our architecture, services never bind to host ports directly; only Traefik binds to ports 80 and 443. Configure UFW accordingly:

Bash — UFW Rules

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'SSH Management'
sudo ufw allow 80/tcp comment 'Traefik HTTP'
sudo ufw allow 443/tcp comment 'Traefik HTTPS'
sudo ufw enable

3. Securing the Docker Socket: Deploying the Read-Only Socket Proxy

Traefik relies on the Docker API to detect container launch events and read routing labels dynamically. However, mounting /var/run/docker.sock directly into Traefik grants the container root-equivalent privileges over the host VPS. If an attacker exploits an edge reverse proxy vulnerability, they can compromise the entire host.

To eliminate this vulnerability, we deploy tecnativa/docker-socket-proxy. This lightweight security service sits between Traefik and the Docker socket, granting read-only access strictly to container and network metadata while blocking all write, execution, and volume manipulation API calls.

Security Best Practice: By setting POST=0, BUILD=0, and EXEC=0 in the socket proxy, no container connected to the proxy can execute commands or spawn new containers on the host, adhering strictly to the principle of least privilege.

Directory Structure & Network Initialization

Create the core production infrastructure directory and the external Docker bridge networks:

Bash — Directory & Network Prep

sudo mkdir -p /srv/traefik/acme
sudo touch /srv/traefik/acme/acme.json
sudo chmod 600 /srv/traefik/acme/acme.json
sudo docker network create proxy-tier
sudo docker network create socket-tier --internal

4. Core Ingress Stack: Traefik v3 Docker Compose with Let’s Encrypt ACME

Now deploy the central reverse proxy stack. In accordance with the official Traefik v3 documentation and Let’s Encrypt ACME standards, create /srv/traefik/docker-compose.yml:

YAML — /srv/traefik/docker-compose.yml

services:
  # Security Barrier: Read-Only Docker Socket Proxy
  docker-socket-proxy:
    image: tecnativa/docker-socket-proxy:latest
    container_name: docker-socket-proxy
    restart: unless-stopped
    environment:
      - CONTAINERS=1
      - NETWORKS=1
      - SERVICES=1
      - POST=0
      - BUILD=0
      - COMMIT=0
      - CONFIGS=0
      - DISTRIBUTION=0
      - EXEC=0
      - IMAGES=0
      - INFO=0
      - NODES=0
      - PLUGINS=0
      - SECRETS=0
      - SESSION=0
      - SWARM=0
      - SYSTEM=0
      - TASKS=0
      - VOLUMES=0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - socket-tier

  # Edge Reverse Proxy & Automatic SSL Termination
  traefik:
    image: traefik:v3.1
    container_name: traefik
    restart: unless-stopped
    depends_on:
      - docker-socket-proxy
    security_opt:
      - no-new-privileges:true
    command:
      # API & Dashboard
      - "--api.dashboard=true"
      # Logging
      - "--log.level=INFO"
      - "--accesslog=true"
      # Providers: Connect to Socket Proxy instead of raw docker.sock
      - "--providers.docker=true"
      - "--providers.docker.endpoint=tcp://docker-socket-proxy:2375"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=proxy-tier"
      # EntryPoints
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--entrypoints.websecure.address=:443"
      # SSL / Let's Encrypt ACME Configuration
      - "--certificatesresolvers.letsencrypt.acme.email=admin@yourdomain.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /srv/traefik/acme/acme.json:/etc/traefik/acme/acme.json
    networks:
      - proxy-tier
      - socket-tier
    labels:
      - "traefik.enable=true"
      # Secure Traefik Dashboard access
      - "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.yourdomain.com`)"
      - "traefik.http.routers.traefik-dashboard.service=api@internal"
      - "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
      - "traefik.http.routers.traefik-dashboard.tls=true"
      - "traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt"
      - "traefik.http.routers.traefik-dashboard.middlewares=auth"
      # Basic Auth: generate with: htpasswd -nb admin yourpassword
      - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$xyz$$yourhashedpassword"

networks:
  proxy-tier:
    external: true
  socket-tier:
    external: true

Start the core ingress stack:

Bash — Launch Ingress

cd /srv/traefik
docker compose up -d
docker compose ps

5. Application Workload Deployment: Multi-Tier Microservice with Dynamic Traefik Labels

With the Traefik ingress stack running, any application deployed on the VPS can be automatically exposed to the web with an SSL certificate simply by declaring Docker labels in its local docker-compose.yml.

Here is a complete production microservice stack (Web API + PostgreSQL) located in /srv/apps/web-app/docker-compose.yml:

YAML — /srv/apps/web-app/docker-compose.yml

services:
  # Public Web Application Service
  web:
    image: yourregistry/production-web-app:latest
    container_name: web-app-production
    restart: unless-stopped
    env_file:
      - .env.production
    depends_on:
      db:
        condition: service_healthy
    networks:
      - proxy-tier
      - app-internal
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 20s
    labels:
      - "traefik.enable=true"
      # Routing Rule
      - "traefik.http.routers.webapp.rule=Host(`app.yourdomain.com`)"
      - "traefik.http.routers.webapp.entrypoints=websecure"
      - "traefik.http.routers.webapp.tls=true"
      - "traefik.http.routers.webapp.tls.certresolver=letsencrypt"
      # Internal Port Discovery
      - "traefik.http.services.webapp.loadbalancer.server.port=3000"
      # Security Middlewares
      - "traefik.http.routers.webapp.middlewares=security-headers"
      - "traefik.http.middlewares.security-headers.headers.sslredirect=true"
      - "traefik.http.middlewares.security-headers.headers.stsSeconds=315360000"
      - "traefik.http.middlewares.security-headers.headers.browserXssFilter=true"
      - "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"

  # Private Database Service (Never Exposed to Traefik or Public Internet)
  db:
    image: postgres:16-alpine
    container_name: web-app-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: app_production
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - app-internal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app_user -d app_production"]
      interval: 10s
      timeout: 5s
      retries: 5

networks:
  proxy-tier:
    external: true
  app-internal:
    driver: bridge

volumes:
  postgres_data:
    driver: local

secrets:
  db_password:
    file: ./secrets/db_password.txt

Deploy the application stack:

Bash — Launch Application

cd /srv/apps/web-app
docker compose up -d

Traefik instantly discovers the new container via the socket proxy, negotiates an SSL certificate with Let’s Encrypt within seconds, and begins routing traffic to internal port 3000.

6. Zero-Downtime Rolling Deployments & Healthcheck Automation

In production environments, deploying application updates must never drop HTTP connections or interrupt active user sessions. Docker Compose supports atomic rolling updates when combined with Traefik health checks.

How Traefik Handles Zero-Downtime Swaps:

  1. When Docker Compose begins updating a service, Traefik continues routing traffic to the currently active, healthy container.
  2. Compose starts the new container image in parallel and initiates the declared healthcheck.
  3. Traefik monitors the health status. Once the new container passes its health checks, Traefik seamlessly shifts incoming traffic to the new container.
  4. Compose gracefully stops and removes the obsolete container (SIGTERM followed by SIGKILL after 10 seconds).

The Production Deployment Command Runbook:

Bash — deploy.sh

#!/usr/bin/env bash
set -e

APP_DIR="/srv/apps/web-app"
cd "$APP_DIR"

echo "Pulling latest production image..."
docker compose pull web

echo "Executing zero-downtime rolling update..."
docker compose up -d --no-deps --build web

echo "Pruning dangling images..."
docker image prune -f

echo "Deployment completed successfully!"

7. Continuous Delivery: Automated GitHub Actions CI/CD over SSH

Automating production deployments eliminates manual error and ensures consistent version control. In our dedicated guide to CI/CD deployment on VPS with GitHub Actions, we explore advanced runners. Below is the complete workflow (.github/workflows/deploy.yml) utilizing the official GitHub Actions documentation standards:

YAML — .github/workflows/deploy.yml

name: Production CI/CD Deployment

on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Source Code
        uses: actions/checkout@v4

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and Push Docker Image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}/web-app:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Deploy to Linux VPS via SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USERNAME }}
          key: ${{ secrets.VPS_SSH_KEY }}
          port: ${{ secrets.VPS_SSH_PORT }}
          script: |
            cd /srv/apps/web-app
            docker compose pull web
            docker compose up -d --remove-orphans web
            docker image prune -f
            docker compose ps

8. Production Verification, Monitoring & VPS Sizing

Once deployed, maintain operational resilience with standard sysadmin verification and backup runbooks. For server administrators who prefer visual monitoring alongside SSH, deploying Cockpit Linux Web Console on VPS offers real-time CPU, RAM, and storage analytics.

Verification Checklist:

  • SSL Validation: Verify certificate status and TLS 1.3 negotiation:
    curl -Iv https://app.yourdomain.com
  • Real-Time Log Aggregation: Stream application and ingress logs cleanly:
    docker compose -f /srv/traefik/docker-compose.yml logs -f --tail=100 traefik
  • Persistent Volume Backups: Store offsite backups using S3-compatible storage such as self-hosted MinIO object storage on VPS:
    docker exec -t web-app-db pg_dumpall -c -U app_user | gzip > /srv/backups/db_$(date +%F).sql.gz
  • Snapshot Disaster Recovery: Implement structured disaster recovery as detailed in our guide on server snapshots vs backups for disaster recovery.

VPS Hardware Sizing Matrix for Container Fleets:

When provisioning infrastructure for production containers, match your workload scale to the appropriate compute tier:

Workload Scale Active Containers Recommended Hosting Tier RAM Allocation Storage Tier
Entry / Starter 1–4 Microservices Linux VPS (2 vCPU / 4 GB) 1 GB Traefik, 2 GB App 50 GB NVMe
Growth / Mid-Tier 5–15 Microservices High-RAM VPS (4 vCPU / 8–16 GB) 2 GB Traefik, 10 GB Apps + DB 100–200 GB NVMe
Enterprise Fleet 15–30+ Microservices Dedicated Server Hosting 4 GB Traefik, 24 GB Multi-Apps 400 GB+ Enterprise NVMe

9. Frequently Asked Questions: Production Docker Compose & Traefik

Why should I use a Docker Socket Proxy instead of mounting /var/run/docker.sock directly into Traefik?
Mounting the Docker socket directly gives Traefik root-level control over the host Docker daemon. If Traefik is compromised, attackers can spawn privileged containers and seize the host. Tecnativa Docker Socket Proxy intercepts API calls and allows only read-only metadata (CONTAINERS=1), blocking container creation and execution.
How does Traefik handle Let’s Encrypt rate limits during staging and testing?
Let’s Encrypt enforces strict limits (e.g., 50 certificates per registered domain per week). In production, Traefik uses acme.caserver=https://acme-v02.api.letsencrypt.org/directory. During initial testing and debugging, you should switch to Let’s Encrypt Staging (https://acme-staging-v02.api.letsencrypt.org/directory) to avoid exhausting rate limits while testing DNS and ACME challenges.
How do I achieve zero-downtime rolling updates with Docker Compose on a single VPS?
Deploy multiple container replicas using docker compose up -d --no-deps --scale web=2 --no-recreate web or healthcheck-driven staggered recreation. Traefik’s dynamic Docker provider continuously monitors container health; as soon as the new container passes its HTTP health check, Traefik routes traffic to it before the old container is stopped.
What is the purpose of the traefik-public bridge network?
The traefik-public network is an external Docker bridge network dedicated to ingress traffic. Only the Traefik proxy, the socket proxy, and public-facing web applications attach to it. Backend microservices, databases, and caches reside in isolated internal networks, preventing direct access from the public router.
Where are SSL certificates stored and how are their file permissions secured?
Traefik stores Let’s Encrypt certificates, private keys, and renewal tokens in /letsencrypt/acme.json. Linux requires this file to have strict POSIX permissions of chmod 600 (readable and writable only by the root owner). Traefik will refuse to start or generate certificates if acme.json has loose permissions.
Can I run this Docker Compose + Traefik architecture on a budget Linux VPS?
Yes. The base stack (Docker Engine, Traefik v3, and Docker Socket Proxy) consumes under 150 MB of RAM and minimal idle CPU. A basic Linux VPS with 2 vCPUs and 2 GB to 4 GB of RAM is more than adequate to run the reverse proxy alongside multiple production web applications, provided memory and CPU limits are defined in Compose files.

10. Architectural Summary & Production Deployment Runbook

Orchestrating production microservices with Docker Compose, Traefik v3, and Let’s Encrypt delivers an exceptionally resilient, automated, and cost-effective hosting environment. By enforcing strict network boundaries, isolating the Docker daemon with a read-only socket proxy, and automating deployments through GitHub Actions, engineering teams achieve enterprise-level reliability without Kubernetes overhead.

1. Hardened Ingress

Traefik terminates TLS at the edge with automated Let’s Encrypt HTTP-01 challenges, isolating public web traffic from private database bridges.

2. Zero-Trust Socket

The read-only Tecnativa Socket Proxy eliminates root container escape vulnerabilities, granting Traefik only necessary container metadata.

3. Continuous Delivery

GitHub Actions CI/CD automates image building, registry pushes, and zero-downtime rolling updates over secure SSH connections.