# PicPeak Environment Configuration # Copy this file to .env and update with your values # Environment NODE_ENV=production # JWT Secret — OPTIONAL. Leave unset and it is auto-generated on first run # (Docker: the secrets-init service writes it to a private volume and reuses it # across restarts). Set it explicitly only to pin your own value. # Generate one with: openssl rand -base64 64 #JWT_SECRET=your_very_long_random_jwt_secret_here # OIDC SSO for admins (#798) — configured in the admin UI; only these two # values live in the environment: # Key encrypting the OIDC client secret at rest (defaults to JWT_SECRET). #OIDC_ENCRYPTION_KEY= # Break-glass: 'true' re-enables local password login even while the SSO # settings disable it (recovery when the IdP is down or misconfigured). #OIDC_BREAK_GLASS=false # Auth cookie Secure flag # unset - default: follows NODE_ENV (production=true, dev=false) # true - always set Secure (HTTPS-only cookies; breaks plain-HTTP access) # false - never set Secure (allows HTTP; cookies not protected on HTTPS) # auto - decide per request: Secure on HTTPS, not on HTTP # # Use COOKIE_SECURE=auto if your deployment is reachable over both HTTPS # (via reverse proxy like Nginx Proxy Manager, Traefik, Caddy) AND plain # HTTP (e.g. LAN access at http://192.168.x.x:3010). The backend reads # req.secure from Express, which respects the X-Forwarded-Proto header # when the proxy is in the trust list. # # Requirements for auto mode: # 1. Your reverse proxy MUST send X-Forwarded-Proto: https on HTTPS # requests. Standard configs for NPM/Traefik/Caddy do this by default. # 2. The proxy must be on a trusted IP range. By default PicPeak trusts # loopback and private networks (127.0.0.1, 10.x, 172.16-31.x, # 192.168.x, link-local). Proxies outside those ranges need custom # trust proxy configuration. # COOKIE_SECURE=auto # Cookie SameSite attribute (Lax | Strict | None). Default: Lax # COOKIE_SAMESITE=Lax # Cookie Domain — set this if serving auth cookies across subdomains. # Leave unset for same-origin setups. # COOKIE_DOMAIN=.example.com # Database Configuration (PostgreSQL) DATABASE_CLIENT=pg DB_USER=picpeak # DB_PASSWORD — OPTIONAL. Leave unset and it is auto-generated on first run # (Docker). Set it explicitly to pin your own, e.g. for an external database. # IMPORTANT: Avoid $ character in passwords - Docker Compose interprets it as variable substitution # If you must use $, escape it as $$ (e.g., Pass$$word instead of Pass$word) #DB_PASSWORD=your_secure_postgres_password_here DB_NAME=picpeak_prod # Redis Configuration # REDIS_PASSWORD — OPTIONAL. Leave unset and it is auto-generated on first run (Docker). # IMPORTANT: Same warning applies - avoid $ or escape as $$ #REDIS_PASSWORD=your_secure_redis_password_here # Admin Account (initial setup) — OPTIONAL # Leave these unset (default) to create your admin IN THE BROWSER on first run: # open /admin and PicPeak shows a setup screen. The one-time setup token is # written to data/SETUP_TOKEN with mode 0600 — read it with # `docker compose exec backend cat /app/data/SETUP_TOKEN`. It is NOT logged # unless that write fails, so it never sits in `docker logs`. # Set ADMIN_PASSWORD to auto-create the admin on first boot instead (legacy; # credentials written to data/ADMIN_CREDENTIALS.txt). #ADMIN_USERNAME=admin #ADMIN_EMAIL=admin@yourdomain.com #ADMIN_PASSWORD=your_secure_admin_password_here # Email Configuration # For Gmail: use app-specific password # For SendGrid: SMTP_USER=apikey, SMTP_PASS=your-api-key SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_SECURE=false SMTP_USER=your-email@gmail.com SMTP_PASS=your-app-specific-password EMAIL_FROM=noreply@yourdomain.com # Application URLs # Use full origin with scheme, no trailing slash. # Admin UI is served by the frontend at /admin. FRONTEND_URL=https://yourdomain.com ADMIN_URL=https://yourdomain.com # Static HTML title + description used for social link previews when the # fetcher doesn't trigger the per-event OG endpoint — most notably the # WhatsApp Business API and various 3rd-party preview-service caches # (#521). Set these to your brand so link previews aren't generic. # Substituted into index.html at frontend-container start, so changes # take effect on the next `docker compose up -d frontend` — no rebuild # required. BRAND_TITLE=PicPeak BRAND_DESCRIPTION=Photo gallery shared with PicPeak. # API URL for email assets (logos, images in notification emails) # This must be the publicly accessible URL where email recipients can load images. # If not set, defaults to http://localhost:3001 which will show broken images in emails. API_URL=https://yourdomain.com/api # Frontend API base # For pre-built images and production behind a reverse proxy, keep '/api'. # If you rebuild the frontend yourself, you may set a full URL at build time. VITE_API_URL=/api # Port Configuration (optional) # BACKEND_PORT=3001 # FRONTEND_PORT=3000 # DB_PORT=5432 # REDIS_PORT=6379 # File watcher (watch-folder auto-import, local storage only) # Max photos processed in parallel — raise on hosts with memory headroom, # lower to 1 on very small hosts. Default: 2 # FILE_WATCHER_CONCURRENCY=2 # Release Channel # Options: 'stable' (default), 'beta', or specific version like 'v2.3.0' # 'stable' uses the :stable tag (same as :latest on main) # 'beta' uses the :beta tag for pre-release versions PICPEAK_CHANNEL=stable # Update Check Configuration # Set to 'false' to disable update notifications in admin UI UPDATE_CHECK_ENABLED=true # Timezone TZ=UTC # Analytics (Optional - Umami) VITE_UMAMI_URL= VITE_UMAMI_WEBSITE_ID= VITE_UMAMI_SHARE_URL= # Storage variables (host paths) # These control where data is stored on the host. Defaults are local folders. APP_STORAGE=./storage APP_DATA=./data LOGS=./logs # ─── Storage Backend ──────────────────────────────────────────────────────── # PicPeak can store photos, thumbnails and archive zips on the local filesystem # (default) or on any S3-compatible object store (AWS S3, MinIO, Cloudflare R2, # Backblaze B2, Wasabi, DigitalOcean Spaces, …). # # STORAGE_BACKEND=local (default) # Uses STORAGE_PATH on the local filesystem. Backwards compatible — every # existing deployment keeps working unchanged. # # STORAGE_BACKEND=s3 # Reads STORAGE_S3_* below. Auto-import via the filesystem watcher is # disabled in this mode (S3 has no inotify) — every photo must enter via the # admin upload UI/API. Run `node backend/scripts/migrate-storage.js` to copy # existing local content to S3 before flipping the env. # # STORAGE_BACKEND=local # # STORAGE_S3_BUCKET=picpeak # STORAGE_S3_REGION=us-east-1 # STORAGE_S3_ACCESS_KEY=AKIAxxxxxxxxxxxxxxxx # STORAGE_S3_SECRET_KEY=xxxxxxxxxxxxxxxxxxxxxxxx # Custom endpoint — set this for MinIO / R2 / B2 / Spaces. Leave unset for AWS. # STORAGE_S3_ENDPOINT=https://s3.us-west-002.backblazeb2.com # Optional namespace prefix inside the bucket — useful for multi-deployment buckets. # STORAGE_S3_PREFIX=picpeak # STORAGE_S3_FORCE_PATH_STYLE=false # MinIO needs true; auto-on when endpoint is set # STORAGE_S3_SSL=true # # Minimum IAM policy (AWS S3) for the bucket above: # { # "Version": "2012-10-17", # "Statement": [{ # "Effect": "Allow", # "Action": [ # "s3:GetObject", "s3:PutObject", "s3:DeleteObject", # "s3:ListBucket", "s3:GetBucketLocation" # ], # "Resource": [ # "arn:aws:s3:::picpeak", # "arn:aws:s3:::picpeak/*" # ] # }] # } # # EXTERNAL_MEDIA_ROOT (above) always lives on the local filesystem regardless # of STORAGE_BACKEND — reference-mode galleries are not migrated to S3 in v1. # ─── Outbound Webhooks (#327) ──────────────────────────────────────────────── # PicPeak POSTs event/photo lifecycle notifications to URLs you configure # under Settings → Webhooks. Each delivery is signed HMAC-SHA256 with a # per-webhook secret in the X-PicPeak-Signature header. # # WEBHOOK_ALLOW_PRIVATE_URLS (default: false) # Block URLs resolving to private IPs / loopback / .local etc. as an # SSRF mitigation. Set to "true" ONLY in dev when your receiver is on # the same docker network or localhost. Production deployments must # leave this OFF. # WEBHOOK_ALLOW_PRIVATE_URLS=false # # WEBHOOK_DELIVERY_INTERVAL_MS (default: 5000) # How often the worker polls webhook_deliveries for pending rows. # WEBHOOK_DELIVERY_INTERVAL_MS=5000 # # WEBHOOK_DELIVERY_CONCURRENCY (default: 5) # Maximum in-flight deliveries per worker tick. One slow consumer can # monopolize all 5 slots — bump this if your receivers are slow OR ship # a separate webhook-only deployment. # WEBHOOK_DELIVERY_CONCURRENCY=5 # # WEBHOOK_HTTP_TIMEOUT_MS (default: 10000) # Per-request timeout. Beyond this, the delivery is recorded as a # network error and retried. # WEBHOOK_HTTP_TIMEOUT_MS=10000 # # WEBHOOK_MAX_ATTEMPTS (default: 5) # Total attempts before a delivery is marked failed. Backoff between # attempts is exponential: 1m, 5m, 30m, 2h, 12h. # WEBHOOK_MAX_ATTEMPTS=5 # ----------------------------------------------------------------------------- # Face recognition — "People in this gallery" (#1074, optional) # ----------------------------------------------------------------------------- # Requires the optional picpeak-ml sidecar container: # docker compose --profile faces up -d # # NONE of these variables do anything until the `faces` feature flag is # enabled in Admin → Settings, AND the per-event "Detect people in this # gallery" toggle is switched on. Both default to OFF. With the flag off the # backend never contacts the sidecar, so leaving these at their defaults on an # install without the container is completely inert. # # Face embeddings are biometric data (GDPR Art. 9 special category in the EU). # The photographer is the controller and needs a lawful basis for the people # in their photos — see docs/feature-face-recognition.md before enabling. # # NOT AVAILABLE ON THE ALL-IN-ONE IMAGE. The single-container build sets # PICPEAK_SINGLE_CONTAINER=true and the backend refuses to enable face # recognition there regardless of these variables or the feature flag: that # image runs the backend, frontend, database and every worker in one # container, with no ML sidecar to talk to, and face detection would compete # with image processing for the same CPU and memory. Use the standard # multi-container deployment if you want this feature. # # FACE_ML_TOKEN (no default — REQUIRED to run the sidecar) # Shared secret between the backend and the sidecar. The sidecar refuses to # start without it rather than serving anonymously, so an accidentally # published port is never a free face-detection API. Generate with: # openssl rand -hex 32 # FACE_ML_TOKEN= # # FACE_ML_URL (default: http://picpeak-ml:8000) # Defaults to the sidecar's compose service name, so the standard # deployment needs no configuration here. Only change it if you run the # sidecar outside the default compose network. # FACE_ML_URL=http://picpeak-ml:8000 # # FACE_PROCESSOR_CONCURRENCY (default: 1) # Face-detection workers in the backend. Defaults to 1 deliberately: face # scanning shares a host with Sharp image processing, which is the real # memory pressure (see UPLOAD_PROCESSOR_CONCURRENCY). Raise only on hosts # with headroom to spare. # FACE_PROCESSOR_CONCURRENCY=1 # # FACE_ORT_THREADS (default: 1) # ONNX Runtime threads inside the sidecar. More threads mean faster # per-photo inference and higher RSS. # FACE_ORT_THREADS=1 # Note on FRONTEND_API_URL (documentation only): # When using pre-built frontend images, runtime env vars cannot override the built JS. # Do NOT rely on FRONTEND_API_URL in Compose. Instead, keep VITE_API_URL=/api and # let the frontend Nginx proxy /api to the backend. Only if you rebuild the frontend # should you change VITE_API_URL at build time.