DEPLOY, WITHOUT THE GUESSWORK

How to Deploy Docker Compose Apps to a VPS (Multi-Container Setup)

Deploy multi-container applications with Docker Compose: app, database, cache, and reverse proxy all wired together with persistent storage.

How to Deploy Docker Compose Apps to a VPS (Multi-Container Setup)

You are going to deploy a multi-container application to a VPS:

This is the pattern for any real application that needs more than one service.

What you will have at the end

Step 1: Set up the directory structure

mkdir -p ~/apps/mystack
cd ~/apps/mystack
mkdir -p data/postgres data/redis

Your structure will look like:

~/apps/mystack/
├── docker-compose.yml
├── .env
└── data/
    ├── postgres/
    └── redis/

Step 2: Create the environment file

Create .env with your secrets:

# Database
POSTGRES_USER=myapp
POSTGRES_PASSWORD=change-this-strong-password
POSTGRES_DB=myapp_production

# App
DATABASE_URL=postgres://myapp:change-this-strong-password@db:5432/myapp_production
REDIS_URL=redis://cache:6379
SECRET_KEY=generate-a-64-char-random-string
NODE_ENV=production

# Domain
DOMAIN=your-domain.com
[email protected]

Generate a strong secret:

openssl rand -hex 32

Step 3: Create the docker-compose.yml

services:
  traefik:
    image: traefik:v2.11
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --certificatesresolvers.letsencrypt.acme.tlschallenge=true
      - --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}
      - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - letsencrypt:/letsencrypt
    restart: unless-stopped
    networks:
      - web

  app:
    image: your-app-image:latest  # Or use build: .
    restart: unless-stopped
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    labels:
      - traefik.enable=true
      - traefik.http.routers.app.rule=Host(`${DOMAIN}`)
      - traefik.http.routers.app.entrypoints=websecure
      - traefik.http.routers.app.tls=true
      - traefik.http.routers.app.tls.certresolver=letsencrypt
      - traefik.http.services.app.loadbalancer.server.port=3000
    networks:
      - web
      - internal

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - internal

  cache:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes:
      - ./data/redis:/data
    networks:
      - internal

networks:
  web:
    external: false
  internal:
    external: false

volumes:
  letsencrypt:

Key points:

Step 4: Configure database migrations

Most apps need to run migrations before starting. Add an init service:

  migrate:
    image: your-app-image:latest
    command: npm run migrate  # Or your migration command
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    networks:
      - internal
    restart: "no"

Run migrations before starting the app:

docker compose run --rm migrate
docker compose up -d app

Step 5: Deploy the stack

# Pull all images
docker compose pull

# Start services in order
docker compose up -d db cache
docker compose up -d app

# Check everything is running
docker compose ps

Verify database connection:

docker compose exec db psql -U myapp -d myapp_production -c "SELECT 1;"

Step 6: Backup and maintenance workflow

Create a backup script at ~/apps/mystack/backup.sh:

#!/bin/bash
set -e

BACKUP_DIR=~/backups/mystack
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p $BACKUP_DIR

# Backup PostgreSQL
docker compose exec -T db pg_dump -U myapp myapp_production | gzip > $BACKUP_DIR/db_$DATE.sql.gz

# Backup Redis (optional, RDB snapshot)
docker compose exec -T cache redis-cli BGSAVE
sleep 2
cp ./data/redis/dump.rdb $BACKUP_DIR/redis_$DATE.rdb

# Keep last 7 days
find $BACKUP_DIR -mtime +7 -delete

echo "Backup completed: $DATE"
chmod +x backup.sh
./backup.sh

Add to cron for daily backups:

crontab -e
# Add: 0 3 * * * cd ~/apps/mystack && ./backup.sh >> ~/logs/backup.log 2>&1

Troubleshooting

Database connection refused

The app cannot reach the database:

# Check if db is running
docker compose ps db

# Check db logs
docker compose logs db --tail 20

# Test connection from app container
docker compose exec app sh -c "nc -zv db 5432"

Common causes:

Container runs out of memory

Check memory usage:

docker stats --no-stream

If PostgreSQL is eating too much RAM, limit it:

  db:
    image: postgres:16-alpine
    deploy:
      resources:
        limits:
          memory: 1G
    # ... rest of config

Data disappears after restart

Volume paths must be correct:

# Check volume contents
ls -la ./data/postgres

# Verify volume is mounted
docker compose exec db df -h /var/lib/postgresql/data

If empty, the volume path in docker-compose.yml does not match where the app writes data.

Services cannot communicate

Docker Compose creates a default network, but explicit networks are safer:

# List networks
docker network ls

# Inspect network
docker network inspect mystack_internal

# Check which containers are connected
docker network inspect mystack_internal --format '{{range .Containers}}{{.Name}} {{end}}'

Internal links (recommended next reads)


Related in the StoicSoft network

If you work in AI-assisted coding, shared terminal sessions, or agent-driven shell workflows like the ones above, 1devtool is the StoicSoft network's tool for safer AI-assisted terminal work — shared sessions with auditing, preflight policy, and tiered model routing built in.

From across the StoicSoft network

Hand-curated reads on the same topic from sister sites in the StoicSoft family.