You are going to deploy a multi-container application to a VPS:
- app server, database, and cache in one stack
- internal Docker networking (services talk to each other by name)
- persistent volumes that survive container restarts
- HTTPS and reverse proxy included
This is the pattern for any real application that needs more than one service.
What you will have at the end
https://your-domain.comserving your app- PostgreSQL database with persistent storage
- Redis cache for sessions or queues
- All services managed with
docker compose up -d
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:
dbandcacheare only on theinternalnetwork (not exposed to internet)appis on both networks (can reach database AND be reached by Traefik)- Health checks ensure the app waits for the database
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:
- Database not healthy yet (check health status)
- Wrong credentials in
.env - Network misconfiguration (both services must share a network)
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)
- Tutorial: Deploy Docker App - single container basics
- Tutorial: Self-Host Supabase - complex Compose example
- Tutorial: Deploy Laravel - PHP + database stack
- Comparison: Railway Alternatives
- ServerCompass: Manage Compose stacks visually
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.

