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

Set Up Backups

Configure automated database backups

Add Monitoring

Install Prometheus + Grafana

Configure Alerts

Set up email/Slack notifications

Explore Alternatives

Compare other deployment methods
Docker Compose provides the perfect balance of control and simplicity for experienced Docker users.