# Pad — local production setup with PostgreSQL + Redis # Usage: # 1. cp .env.example .env # generates a POSTGRES_PASSWORD if missing # 2. docker compose up -d # # Starts Pad with PostgreSQL for storage and Redis for real-time events. # The web UI binds to 127.0.0.1:7777 on the host by default — LAN/internet # access requires explicit opt-in (see PAD_BIND_ADDR below or the prod override). # First-time setup: visit the UI or run `pad auth setup` from a local CLI. services: pad: build: context: . dockerfile: Dockerfile ports: # Bind to loopback only by default. A fresh install exposes the bootstrap # endpoint until the first admin is created, so defaulting to 0.0.0.0 # would hand control of the instance to anyone who can route to the host. # Override by setting PAD_BIND_ADDR=0.0.0.0 (or a specific LAN IP) in .env. - "${PAD_BIND_ADDR:-127.0.0.1}:7777:7777" environment: PAD_HOST: "0.0.0.0" PAD_PORT: "7777" PAD_DB_DRIVER: "postgres" # POSTGRES_PASSWORD is REQUIRED — docker compose refuses to start when unset. # Use libpq's keyword=value DSN (not the URI form) so passwords with # reserved URI characters like '/', '+', ':', '@' — common in # `openssl rand -base64` output — don't need percent-encoding and # can't break the connection string by accident. PAD_DATABASE_URL: "host=postgres port=5432 user=pad password=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required — see .env.example} dbname=pad sslmode=disable" # PAD_ENCRYPTION_KEY is REQUIRED on Postgres — Pad won't auto-generate # one because a multi-replica deploy would generate a different key # per replica and fail cross-instance decryption. Generate with # `openssl rand -hex 32` and keep it stable across restarts. PAD_ENCRYPTION_KEY: "${PAD_ENCRYPTION_KEY:?PAD_ENCRYPTION_KEY is required — see .env.example}" PAD_REDIS_URL: "redis://redis:6379" PAD_DATA_DIR: "/data" PAD_LOG_LEVEL: "info" # Cloud-mode wiring — safe to keep enabled on self-host deploys # because every var defaults to empty and pad treats empty as # "not in cloud mode". Populate from .env when co-deploying with # pad-cloud so the reverse sidecar can cascade Stripe cancels on # account delete (TASK-690). PAD_CLOUD: "${PAD_CLOUD:-}" PAD_CLOUD_SECRET: "${PAD_CLOUD_SECRET:-}" PAD_CLOUD_SIDECAR_URL: "${PAD_CLOUD_SIDECAR_URL:-}" PAD_CLOUD_OUTBOUND_SECRET: "${PAD_CLOUD_OUTBOUND_SECRET:-}" volumes: - pad-data:/data depends_on: postgres: condition: service_healthy redis: condition: service_healthy restart: unless-stopped healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:7777/api/v1/health"] interval: 10s timeout: 5s retries: 3 start_period: 10s postgres: image: postgres:17-alpine environment: POSTGRES_USER: pad # Required. Fail-fast with the helpful error below when unset. POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required — see .env.example}" POSTGRES_DB: pad volumes: - pg-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U pad"] interval: 5s timeout: 3s retries: 5 restart: unless-stopped redis: image: redis:7-alpine command: redis-server --maxmemory 64mb --maxmemory-policy allkeys-lru volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 restart: unless-stopped volumes: pad-data: pg-data: redis-data: