super-productivity/packages/super-sync-server/docker-compose.yml
johannesjo 25d637e917 chore(supersync): harden compose for 4GB VPS production
Right-size container limits to fit a 4GB / 2-vCore VPS host without
OOM risk, and apply minimal best-practice hardening informed by
multi-agent review.

Container memory budget (sum 2816 MB, leaves ~1.2 GB host headroom):
- postgres: 1g → 1.5g (was at 74% under 199 WSS connections)
- supersync: 1g → 768m (uses 143 MiB steady state)
- dozzle: add 64m cap (was unbounded)
- uptime-kuma: add 192m cap (was unbounded)
- caddy: unchanged at 256m

Postgres tuning (1.5g cap, shm_size 256m to allow shared_buffers > 64m):
- shared_buffers=384MB, effective_cache_size=1GB
- work_mem=4MB, maintenance_work_mem=128MB
- max_connections=40 (Prisma pool is single-digit; 40 leaves headroom
  for migrations + psql; 100 was an OOM landmine vs 1.5g cap)
- random_page_cost=1.1 (SSD)
- idle_in_transaction_session_timeout=300000 (5 min — catches leaked
  Prisma idle-in-tx but stays well above migrate-deploy.sh
  STEP_TIMEOUT=1800 for CONCURRENTLY index recovery)
- wal_compression=on, huge_pages=off

Other hardening:
- Standardize restart policy to unless-stopped across all services
- Add caddy healthcheck via admin API at localhost:2019; Caddyfile
  comment warns future edits not to disable admin
- Add ulimits.nofile=65535 to supersync (Alpine soft default 1024 is
  tight for 200+ WSS + reconnect bursts)
- Pin networks.internal.name=super-sync-server_internal so the
  monitoring overlay can attach via external: true regardless of
  COMPOSE_PROJECT_NAME
- Attach dozzle + uptime-kuma to internal network so Kuma can probe
  http://supersync:1900/health by service name

Notes for future maintenance:
- oom_score_adj was tried and removed: Compose v2 silently ignores it
  (verified on host via /proc/<pid>/oom_score_adj returning 0).
  Would need a systemd post-up hack to apply — not worth it.
- The reconnect storm that motivated this work is fixed separately
  on master in b404bf8a3.
2026-05-16 12:34:13 +02:00

178 lines
5.1 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
- 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.
# max_connections=40 keeps the worst-case sort budget (max_conn × work_mem)
# well inside the 1.5g cap; Prisma's per-instance pool is single-digit so
# 40 is generous headroom for migrations + psql + a future second replica.
command:
- postgres
- -c
- shared_buffers=384MB
- -c
- effective_cache_size=1GB
- -c
- work_mem=4MB
- -c
- maintenance_work_mem=128MB
- -c
- max_connections=40
- -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:
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost: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