services: db: image: postgres:17 container_name: initiative-db restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER:-initiative} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-initiative} POSTGRES_DB: ${POSTGRES_DB:-initiative} volumes: - postgres_data:/var/lib/postgresql/data configs: # Runs ONCE when the database volume is first initialized: creates the # least-privilege role the app uses for migrations + guild provisioning, # so the app never holds Postgres superuser credentials. Existing # deployments (volume already initialized) instead run # backend/scripts/create-provisioner.sql once — see the deployment docs. - source: initdb-provisioner target: /docker-entrypoint-initdb.d/01-app-provisioner.sql healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"] interval: 5s timeout: 5s retries: 5 initiative: # Use the published Docker image from Docker Hub # Available tags: latest, 0.1, 0.1.1, etc. # See: https://hub.docker.com/r/morelitea/initiative image: morelitea/initiative:latest container_name: initiative restart: unless-stopped volumes: - ./uploads:/app/uploads environment: # Optional: Run as a specific UID/GID (default: 1000/1000). # Set PUID/PGID here — do NOT add a compose `user:` override. The image # must start as root to create the runtime user and drop privileges; a # `user:` override makes the entrypoint fail with # "fatal: Only root may add a user or group to the system". # Unraid users: set PUID=99 PGID=100 for nobody:users # PUID: 1000 # PGID: 1000 # RLS-enforced connection (app_user role, the request path) DATABASE_URL_APP: postgresql+asyncpg://app_user:${APP_USER_PASSWORD:-app_user_password}@db:5432/${POSTGRES_DB:-initiative} # System engine for background jobs and seeding (app_admin role — policy-bound, no RLS bypass) DATABASE_URL_ADMIN: postgresql+asyncpg://app_admin:${APP_ADMIN_PASSWORD:-app_admin_password}@db:5432/${POSTGRES_DB:-initiative} # Provisioning connection for migrations + guild schema/role creation. # app_provisioner is least-privilege (no superuser) — created by the db # init script above on fresh installs. DATABASE_URL: postgresql+asyncpg://app_provisioner:${PROVISIONER_PASSWORD:-provisioner_password}@db:5432/${POSTGRES_DB:-initiative} # REQUIRED: generate with `openssl rand -hex 32`. The stack refuses to start without it. SECRET_KEY: ${SECRET_KEY:?SECRET_KEY must be set - generate with openssl rand -hex 32} ACCESS_TOKEN_EXPIRE_MINUTES: 60 APP_URL: ${APP_URL:-http://localhost:8173} # Optional: Enable OIDC authentication # OIDC_ENABLED: true # OIDC_ISSUER: https://your-oidc-provider # OIDC_CLIENT_ID: your-client-id # OIDC_CLIENT_SECRET: your-client-secret # OIDC_PROVIDER_NAME: Your Provider Name # OIDC_SCOPES: openid,profile,email,offline_access # Optional: Enable push notifications (requires Firebase project) # See docs/en/admin/push-notifications.md for setup instructions # FCM_ENABLED: false # FCM_PROJECT_ID: your-firebase-project-id # FCM_APPLICATION_ID: 1:123456789:android:abcdef # FCM_API_KEY: AIzaSy... # FCM_SENDER_ID: 123456789 # FCM_SERVICE_ACCOUNT_JSON: '{"type":"service_account","project_id":"..."}' depends_on: db: condition: service_healthy ports: - "8173:8173" volumes: postgres_data: driver: local configs: initdb-provisioner: content: | CREATE ROLE app_provisioner WITH LOGIN CREATEROLE NOSUPERUSER NOBYPASSRLS PASSWORD '${PROVISIONER_PASSWORD:-provisioner_password}'; GRANT CREATE, CONNECT ON DATABASE "${POSTGRES_DB:-initiative}" TO app_provisioner; -- Postgres 15+ made CREATE on the public schema owner-only, and the -- baseline migration (running as app_provisioner) builds the shared -- schema there — including COMMENT ON SCHEMA, which needs ownership. ALTER SCHEMA public OWNER TO app_provisioner; -- Postgres 16+ lets only BYPASSRLS holders create/alter BYPASSRLS -- roles, so the login roles are created here (superuser context), -- never by the provisioner-run migration — it only syncs their -- passwords from the APP/ADMIN URLs. ADMIN OPTION lets it do that. CREATE ROLE app_user WITH LOGIN NOINHERIT PASSWORD '${APP_USER_PASSWORD:-app_user_password}'; CREATE ROLE app_admin WITH LOGIN BYPASSRLS PASSWORD '${APP_ADMIN_PASSWORD:-app_admin_password}'; GRANT app_user TO app_provisioner WITH ADMIN OPTION; GRANT app_admin TO app_provisioner WITH ADMIN OPTION;