# Production docker-compose for super-sync-server # # Usage: # 1. Copy env.example to .env and configure your settings # 2. Run: ./scripts/deploy.sh # 3. View logs: docker-compose logs -f # # For local builds (development): # ./scripts/deploy.sh --build x-logging: &default-logging driver: 'json-file' options: max-size: '10m' max-file: '3' services: supersync: image: ${SUPERSYNC_IMAGE:-ghcr.io/super-productivity/supersync:latest} container_name: supersync-server restart: unless-stopped environment: - NODE_ENV=${NODE_ENV:-production} - PORT=1900 - HOST=${HOST:-0.0.0.0} - DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER:-supersync}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-supersync}} - RUN_MIGRATIONS_ON_STARTUP=${RUN_MIGRATIONS_ON_STARTUP:-false} - JWT_SECRET=${JWT_SECRET} - PUBLIC_URL=${PUBLIC_URL:-http://localhost:1900} - CORS_ORIGINS=${CORS_ORIGINS:-https://app.super-productivity.com} - SMTP_HOST=${SMTP_HOST:-} - SMTP_PORT=${SMTP_PORT:-587} - SMTP_SECURE=${SMTP_SECURE:-false} - SMTP_USER=${SMTP_USER:-} - SMTP_PASS=${SMTP_PASS:-} - SMTP_FROM=${SMTP_FROM:-} - WEBAUTHN_RP_ID=${WEBAUTHN_RP_ID:-localhost} - WEBAUTHN_RP_NAME=${WEBAUTHN_RP_NAME:-Super Productivity Sync} - WEBAUTHN_ORIGIN=${WEBAUTHN_ORIGIN:-http://localhost:1900} - OLD_OPS_CLEANUP_DELETE_BATCH_SIZE=${OLD_OPS_CLEANUP_DELETE_BATCH_SIZE:-5000} - OLD_OPS_CLEANUP_MAX_DELETED_PER_RUN=${OLD_OPS_CLEANUP_MAX_DELETED_PER_RUN:-25000} - SUPERSYNC_BATCH_UPLOAD=${SUPERSYNC_BATCH_UPLOAD:-false} - SUPERSYNC_PAYLOAD_BYTES_BACKFILL_COMPLETE=${SUPERSYNC_PAYLOAD_BYTES_BACKFILL_COMPLETE:-false} ports: - '127.0.0.1:1900:1900' volumes: - supersync-data:/app/data depends_on: postgres: condition: service_healthy healthcheck: test: [ 'CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:1900/health', ] interval: 30s timeout: 5s retries: 3 start_period: 30s security_opt: - no-new-privileges:true cap_drop: - ALL # 200+ WSS clients each consume an fd; Alpine default soft limit (1024) # would be tight under reconnect bursts. ulimits: nofile: soft: 65535 hard: 65535 mem_limit: 768m cpus: '1.0' networks: - internal logging: *default-logging postgres: image: postgres:16-alpine container_name: supersync-postgres restart: unless-stopped environment: - POSTGRES_USER=${POSTGRES_USER:-supersync} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=${POSTGRES_DB:-supersync} # Tuned for the 1.5g mem_limit. shared_buffers needs shm_size > default 64m. # 120 × 4MB work_mem = 480MB worst-case sort budget + 384MB shared_buffers, # well inside the 1.5g cap. Prisma's DEFAULT pool is ~(host_cpu_count × 2), # read from host cores NOT the container cpu limit — it is NOT single-digit # on this VPS and will silently consume the entire cap. The app MUST bound # it via DATABASE_URL ?connection_limit=N (currently 60). max_connections # stays well above N to reserve slots for migrations, psql, the old-ops # cleanup job, and the reconnect stampede after an in-place crash recovery. command: - postgres - -c - shared_buffers=384MB - -c - effective_cache_size=1GB - -c - work_mem=4MB - -c - maintenance_work_mem=128MB - -c - max_connections=120 - -c - random_page_cost=1.1 - -c # 5 min — kill leaked Prisma idle-in-tx, but well above the # migrate-deploy.sh 30-min STEP_TIMEOUT for CONCURRENTLY index builds. - idle_in_transaction_session_timeout=300000 - -c - wal_compression=on - -c - huge_pages=off shm_size: 256m volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: - CMD-SHELL - > pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" && psql -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" -c "SELECT 1" > /dev/null 2>&1 interval: 10s timeout: 5s retries: 5 start_period: 10s security_opt: - no-new-privileges:true mem_limit: 1536m networks: internal: aliases: # Keep existing .env files with DATABASE_URL=...@db:5432/... working. - db logging: *default-logging caddy: image: caddy:2.11-alpine container_name: supersync-caddy restart: unless-stopped ports: - '80:80' - '443:443' volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy-data:/data - caddy-config:/config environment: - DOMAIN=${DOMAIN} healthcheck: # Use 127.0.0.1, not localhost: Caddy binds the admin API to IPv4 # 127.0.0.1:2019, but busybox wget resolves `localhost` to ::1 first # and gets "connection refused", marking a healthy Caddy unhealthy. test: ['CMD', 'wget', '-q', '--spider', 'http://127.0.0.1:2019/config/'] interval: 30s timeout: 5s retries: 3 start_period: 10s networks: - internal cap_drop: - ALL cap_add: - NET_BIND_SERVICE mem_limit: 256m depends_on: - supersync logging: *default-logging volumes: supersync-data: postgres-data: caddy-data: caddy-config: networks: internal: # Pinned name so docker-compose.monitoring.yml can attach via # `external: true` regardless of the COMPOSE_PROJECT_NAME used. name: super-sync-server_internal driver: bridge