mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
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.
This commit is contained in:
parent
1661579aed
commit
25d637e917
3 changed files with 70 additions and 5 deletions
|
|
@ -1,3 +1,8 @@
|
|||
# NOTE: docker-compose.yml healthcheck probes the Caddy admin API at
|
||||
# localhost:2019 (enabled by default in Caddy 2.x). Do NOT add a global
|
||||
# `{ admin off }` block or rebind admin without also updating the healthcheck,
|
||||
# or Caddy will restart-loop.
|
||||
|
||||
{$DOMAIN} {
|
||||
reverse_proxy supersync:1900 {
|
||||
# Allow up to 85 seconds for long-running operations
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
# Monitoring stack for SuperSync Server
|
||||
#
|
||||
# Attaches to the external `super-sync-server_internal` network created by
|
||||
# the base docker-compose.yml — the base stack must be brought up first
|
||||
# (or merged into the same `up` invocation as below). Running this file
|
||||
# standalone will fail with "network not found".
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.monitoring.yml up -d
|
||||
#
|
||||
|
|
@ -23,6 +28,9 @@ services:
|
|||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- DOZZLE_NO_ANALYTICS=true
|
||||
networks:
|
||||
- internal
|
||||
mem_limit: 64m
|
||||
|
||||
# Uptime monitoring + alerts (~120MB RAM)
|
||||
# Provides health check monitoring and notifications
|
||||
|
|
@ -34,6 +42,16 @@ services:
|
|||
- '127.0.0.1:3001:3001'
|
||||
volumes:
|
||||
- uptime-kuma-data:/app/data
|
||||
networks:
|
||||
- internal
|
||||
mem_limit: 192m
|
||||
|
||||
# Attach to the internal network defined in docker-compose.yml so Kuma can
|
||||
# probe http://supersync:1900/health by service name (not just public URL).
|
||||
networks:
|
||||
internal:
|
||||
external: true
|
||||
name: super-sync-server_internal
|
||||
|
||||
volumes:
|
||||
uptime-kuma-data:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ services:
|
|||
supersync:
|
||||
image: ${SUPERSYNC_IMAGE:-ghcr.io/super-productivity/supersync:latest}
|
||||
container_name: supersync-server
|
||||
restart: always
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV:-production}
|
||||
- PORT=1900
|
||||
|
|
@ -65,7 +65,13 @@ services:
|
|||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
mem_limit: 1g
|
||||
# 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
|
||||
|
|
@ -74,11 +80,38 @@ services:
|
|||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: supersync-postgres
|
||||
restart: always
|
||||
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:
|
||||
|
|
@ -93,7 +126,7 @@ services:
|
|||
start_period: 10s
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 1g
|
||||
mem_limit: 1536m
|
||||
networks:
|
||||
internal:
|
||||
aliases:
|
||||
|
|
@ -104,7 +137,7 @@ services:
|
|||
caddy:
|
||||
image: caddy:2.11-alpine
|
||||
container_name: supersync-caddy
|
||||
restart: always
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
|
|
@ -114,6 +147,12 @@ services:
|
|||
- 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:
|
||||
|
|
@ -133,4 +172,7 @@ volumes:
|
|||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue