# Production stack for XIP — runs on the dedicated CT (xip-app, Echelon CT502). # Postgres + Redis + bun backend + nginx (serves SPA, proxies /api and /ws). # Secrets come from .env.prod (gitignored), loaded via `--env-file .env.prod`. services: postgres: image: postgres:16 restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB:-xip} POSTGRES_USER: ${POSTGRES_USER:-xip} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env.prod} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-xip}"] interval: 5s timeout: 5s retries: 20 redis: image: redis:7 restart: unless-stopped command: ["redis-server", "--appendonly", "yes"] volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 5s retries: 20 backend: build: context: . dockerfile: backend/Dockerfile restart: unless-stopped environment: DATABASE_URL: postgresql://${POSTGRES_USER:-xip}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-xip} REDIS_URL: redis://redis:6379 PORT: "3000" NODE_ENV: production # Prod "open bar": paywall disabled for everyone (see backend/src/lib/ip.ts). XIP_OPEN_BAR: ${XIP_OPEN_BAR:-true} volumes: - uploads_data:/app/uploads depends_on: postgres: condition: service_healthy redis: condition: service_healthy web: build: context: . dockerfile: frontend/Dockerfile args: VITE_API_URL: ${PUBLIC_URL:-https://xip.kerboul.me} restart: unless-stopped ports: - "80:80" depends_on: - backend volumes: postgres_data: redis_data: uploads_data: