# Public VPS (asgard) deployment — hardened, single-host, behind Caddy. # # Differs from docker-compose.server.yml (the home/LXC stack): the database is NEVER # published, the app binds to 127.0.0.1 only (Caddy terminates TLS and reverse-proxies it), # there is no apparmor:unconfined (this is a bare VPS, not Docker-in-LXC), and every service # has a hard memory/CPU cap so the stack can't exhaust the small VPS. # # Secrets live OUTSIDE the repo, in an env file on the host. Deploy with: # docker compose -f docker-compose.prod.yml --env-file /srv/siftlode/.env up -d --build # # Required keys in that env file: POSTGRES_PASSWORD, SECRET_KEY (>=32 chars), # TOKEN_ENCRYPTION_KEY (Fernet), GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, # OAUTH_REDIRECT_URL=https://siftlode.b1fr0st.eu/auth/callback, ALLOWED_EMAILS, ADMIN_EMAILS. services: db: image: postgres:16-alpine environment: POSTGRES_USER: ${POSTGRES_USER:-subfeed} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in the env file} POSTGRES_DB: ${POSTGRES_DB:-subfeed} volumes: - subfeed_pgdata:/var/lib/postgresql/data networks: [internal] mem_limit: 512m cpus: 0.75 security_opt: - no-new-privileges:true healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-subfeed} -d ${POSTGRES_DB:-subfeed}"] interval: 10s timeout: 5s retries: 12 restart: unless-stopped api: build: context: . dockerfile: Dockerfile image: siftlode-api:local env_file: - /srv/siftlode/.env environment: DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-subfeed}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-subfeed} # The public instance owns the background scheduler (single writer). SCHEDULER_ENABLED: "true" depends_on: db: condition: service_healthy networks: [internal] ports: # Localhost only — Caddy (host network) reverse-proxies 443 -> here. Never public. - "127.0.0.1:8080:8000" mem_limit: 768m cpus: 1.0 pids_limit: 256 security_opt: - no-new-privileges:true cap_drop: - ALL read_only: true tmpfs: - /tmp healthcheck: test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/healthz').status==200 else 1)"] interval: 15s timeout: 5s retries: 5 start_period: 30s restart: unless-stopped volumes: subfeed_pgdata: networks: internal: