Overview
Manual Docker Compose deployment gives you full control over your infrastructure without abstraction layers like Dokploy or Coolify.
Best for: Teams comfortable with Docker who want maximum control and minimal abstractions.
Why Docker Compose?
Full Control
Complete control over every aspect of deployment
No Overhead
No platform overhead - just Docker
Version Control
Entire infrastructure in Git
Standard Tools
Uses standard Docker tooling
Prerequisites
- Hetzner CPX32 (4 vCPU, 8GB RAM)
- Ubuntu 24.04 LTS
- Docker & Docker Compose installed
- Domain with DNS access
- Comfortable with command line
Quick Start
1. Install Dependencies
# Install Docker
curl -fsSL https://get.docker.com | sh
# Install Docker Compose
apt install docker-compose-plugin
# Verify
docker compose version
2. Create Project Structure
mkdir -p /opt/ripplecore
cd /opt/ripplecore
# Clone repository
git clone https://github.com/your-username/ripplecore-forge.git
cd ripplecore-forge
3. Create docker-compose.yml
Create /opt/ripplecore/docker-compose.yml:
version: '3.8'
services:
# Traefik Reverse Proxy
traefik:
image: traefik:v3.0
command:
- --api.dashboard=true
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.letsencrypt.acme.email=your@email.com
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
restart: unless-stopped
# PostgreSQL 18
postgres:
image: postgres:18-alpine
environment:
POSTGRES_DB: ripplecore
POSTGRES_USER: ripplecore
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ripplecore"]
interval: 10s
timeout: 5s
retries: 5
# Redis 7
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
# Main App
app:
build:
context: .
dockerfile: apps/app/Dockerfile
args:
DATABASE_URL: postgresql://ripplecore:${POSTGRES_PASSWORD}@postgres:5432/ripplecore
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
NEXT_PUBLIC_APP_URL: https://app.${DOMAIN}
NEXT_PUBLIC_WEB_URL: https://${DOMAIN}
environment:
DATABASE_URL: postgresql://ripplecore:${POSTGRES_PASSWORD}@postgres:5432/ripplecore
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
BETTER_AUTH_URL: https://app.${DOMAIN}
BETTER_AUTH_TRUST_HOST: "true"
NODE_ENV: production
PORT: 3000
labels:
- traefik.enable=true
- traefik.http.routers.app.rule=Host(`app.${DOMAIN}`)
- traefik.http.routers.app.entrypoints=websecure
- traefik.http.routers.app.tls.certresolver=letsencrypt
- traefik.http.services.app.loadbalancer.server.port=3000
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
# API Service
api:
build:
context: .
dockerfile: apps/api/Dockerfile
args:
DATABASE_URL: postgresql://ripplecore:${POSTGRES_PASSWORD}@postgres:5432/ripplecore
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
environment:
DATABASE_URL: postgresql://ripplecore:${POSTGRES_PASSWORD}@postgres:5432/ripplecore
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
NODE_ENV: production
PORT: 3002
labels:
- traefik.enable=true
- traefik.http.routers.api.rule=Host(`api.${DOMAIN}`)
- traefik.http.routers.api.entrypoints=websecure
- traefik.http.routers.api.tls.certresolver=letsencrypt
- traefik.http.services.api.loadbalancer.server.port=3002
depends_on:
- postgres
- redis
restart: unless-stopped
# Marketing Website
web:
build:
context: .
dockerfile: apps/web/Dockerfile
args:
NEXT_PUBLIC_APP_URL: https://app.${DOMAIN}
NEXT_PUBLIC_WEB_URL: https://${DOMAIN}
environment:
NODE_ENV: production
PORT: 3001
labels:
- traefik.enable=true
- traefik.http.routers.web.rule=Host(`${DOMAIN}`) || Host(`www.${DOMAIN}`)
- traefik.http.routers.web.entrypoints=websecure
- traefik.http.routers.web.tls.certresolver=letsencrypt
- traefik.http.services.web.loadbalancer.server.port=3001
restart: unless-stopped
volumes:
postgres_data:
redis_data:
4. Create .env File
Create .env in same directory:
# Domain
DOMAIN=yourdomain.com
# Database
POSTGRES_PASSWORD=your_secure_password_here
# Redis
REDIS_PASSWORD=your_redis_password_here
# Better Auth (generate with: npx @better-auth/cli secret)
BETTER_AUTH_SECRET=your_32_char_secret_here
5. Deploy
# Build and start services
docker compose up -d --build
# View logs
docker compose logs -f
# Check status
docker compose ps
6. Run Migrations
# Run migration job
docker compose run --rm -e DATABASE_URL=postgresql://ripplecore:${POSTGRES_PASSWORD}@postgres:5432/ripplecore \
app sh -c "cd packages/database && pnpm db:push"
Management Commands
# View logs
docker compose logs -f [service-name]
# Restart service
docker compose restart app
# Stop all services
docker compose down
# Stop and remove volumes (DANGER)
docker compose down -v
# Update to latest code
git pull
docker compose up -d --build
# View resource usage
docker stats
SSL Certificate Management
Traefik handles SSL automatically via Let’s Encrypt. Certificates stored in ./letsencrypt/acme.json.
Manual renewal (if needed):
# Restart Traefik to force renewal
docker compose restart traefik
Backup Strategy
Database Backup
# Create backup
docker compose exec postgres pg_dump -U ripplecore ripplecore > backup-$(date +%Y%m%d).sql
# Restore from backup
cat backup-20250116.sql | docker compose exec -T postgres psql -U ripplecore ripplecore
Automated Backups
Create /opt/ripplecore/backup.sh:
#!/bin/bash
BACKUP_DIR="/opt/ripplecore/backups"
mkdir -p $BACKUP_DIR
# Backup PostgreSQL
docker compose exec -T postgres pg_dump -U ripplecore ripplecore | gzip > \
$BACKUP_DIR/postgres-$(date +%Y%m%d-%H%M).sql.gz
# Keep last 7 days
find $BACKUP_DIR -type f -mtime +7 -delete
# Optional: Upload to S3
# aws s3 cp $BACKUP_DIR/postgres-$(date +%Y%m%d-%H%M).sql.gz s3://your-bucket/
Add to crontab:
0 2 * * * /opt/ripplecore/backup.sh
Monitoring
Health Checks
# Check all services
curl https://app.yourdomain.com/api/health
curl https://api.yourdomain.com/api/health
# Check Traefik dashboard
curl http://localhost:8080/api/rawdata
Resource Monitoring
# Real-time stats
docker stats
# Disk usage
docker system df
# View container logs
docker compose logs --tail=100 app
Migration from Dokploy
1. Export Data
# From Dokploy VPS
docker exec ripplecore-postgres pg_dump -U ripplecore ripplecore > backup.sql
# Copy to new VPS
scp backup.sql root@new-vps:/opt/ripplecore/
2. Deploy Docker Compose
Follow Quick Start above on new VPS.
3. Restore Data
cat backup.sql | docker compose exec -T postgres psql -U ripplecore ripplecore
4. Update DNS
Point A records to new VPS IP.
Migration time: 3-4 hours
Pros & Cons
Advantages
- Maximum control
- No platform overhead
- Version control everything
- Standard Docker tools
- Easy to customize
Limitations
- No web UI
- Manual SSL management
- No built-in monitoring
- More DevOps required
- Manual rollbacks
Troubleshooting
Build failures:
# Clear build cache
docker compose build --no-cache app
# Check build logs
docker compose logs app
SSL issues:
# Check Traefik logs
docker compose logs traefik
# Verify DNS
dig app.yourdomain.com
Database connection:
# Test connection
docker compose exec postgres psql -U ripplecore
# Check logs
docker compose logs postgres
Next Steps
Docker Compose provides the perfect balance of control and simplicity for experienced Docker users.