mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-24 00:17:23 +00:00
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.
178 lines
5.1 KiB
YAML
178 lines
5.1 KiB
YAML
# 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
|