# ═══════════════════════════════════════════════════════════════════════════════ # $APP_NAME — Docker Compose (self-hosted) # # Prerequisites: # 1. Backend → cp apps/api/.env-production apps/api/.env and fill in all CHANGE_ME values # 2. Frontend → cd apps/web && cp .env.example .env → fill in values # → pnpm --filter web build (must run BEFORE building the Docker image) # 3. Run: docker compose up -d # # ⚠ SSL NOTE — db.config.js and the session store both require SSL from # PostgreSQL. The bundled `postgres` service here ships without SSL by # default, so the backend will refuse to connect. Two options: # # Option A (recommended) — use a managed PostgreSQL with SSL: # Set DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD in .env to # your managed provider (CockroachDB, Supabase, Neon, etc.) and remove # the `postgres` service + its depends_on entry below. # # Option B — fix the code to make SSL optional: # Add DB_SSL=false to .env and update db.config.js + the session store # connection string in server.js to read that flag. Then the bundled # postgres service works without SSL. # # Redis / Valkey: # In Docker the service is reachable by its service name, not 127.0.0.1. # Set this in .env: REDIS_URL=redis://valkey:6379 # ═══════════════════════════════════════════════════════════════════════════════ services: # ── Backend (Express) ─────────────────────────────────────────────────────── backend: build: context: . dockerfile: apps/api/Dockerfile ports: - "3024:3024" env_file: apps/api/.env depends_on: postgres: condition: service_healthy valkey: condition: service_started garage: condition: service_healthy restart: unless-stopped # ── Frontend (React / Nginx) ───────────────────────────────────────────────── # Run `pnpm --filter web build` before starting this service. frontend: build: ./apps/web ports: - "80:80" restart: unless-stopped # ── PostgreSQL ─────────────────────────────────────────────────────────────── # See SSL NOTE above before using this service. postgres: image: postgres:16-alpine environment: POSTGRES_DB: ${DB_NAME} POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped # ── Valkey (Redis-compatible cache) ───────────────────────────────────────── valkey: image: valkey/valkey:8-alpine volumes: - valkey_data:/data restart: unless-stopped # ── Garage (S3-compatible self-hosted storage) ──────────────────────────────── # Exposes the S3 API on port 3900. Point your reverse proxy at this port # to serve S3_PUBLIC_URL (e.g. https://cdn.yourdomain.com → http://garage:3900). # GARAGE_RPC_SECRET must be set in .env (generate with: openssl rand -hex 32). # # The dxflrs/garage image ships only the static `garage` binary — no shell, # so it can't run garage-init.sh (and CMD-SHELL healthchecks can't run # either, same reason). Cluster init (layout/bucket/key) is a one-time # manual step after first `docker compose up` — see README/setup notes. garage: image: dxflrs/garage:v2.3.0 volumes: - ./apps/api/config/garage.toml:/etc/garage.toml:ro - garage_meta:/var/lib/garage/meta - garage_data:/var/lib/garage/data ports: - "3900:3900" env_file: apps/api/.env healthcheck: test: ["CMD", "/garage", "status"] interval: 10s timeout: 5s retries: 10 start_period: 30s restart: unless-stopped volumes: postgres_data: valkey_data: garage_meta: garage_data: