Skip to Content
DeploymentEnvironment Variables

Environment Variables

Complete reference for all PicPeak environment variables. Set these in the .env file at the project root.

Avoid $ characters in passwords. Docker Compose interprets $ as variable substitution. Either omit $ entirely or escape it as $$.

Unless a section says otherwise, a variable works the same on the stable and the beta channel. Sections marked beta channel only describe features that are not in a stable release yet.

Core

VariableRequiredDefaultDescription
NODE_ENVNoproductionEnvironment mode
JWT_SECRETYes (generated in Docker)---Authentication secret. The Compose stack and the all-in-one image generate one on first start when it is unset and reuse it from their secrets volume. If you set it yourself it must be at least 32 characters, and the example values from older .env files are refused: the backend stops at startup with a message saying so. Generate one with openssl rand -hex 32
TZNoUTCTimezone
SHUTDOWN_TIMEOUT_MSNo8000Backend shutdown deadline in milliseconds after SIGTERM or SIGINT; forces an exit when exceeded. Set in the backend container’s environment and keep below Docker Compose’s stop_grace_period. See Stopping and restarting.
TRUST_PROXYNoCompose: 1; all-in-one image: 0; unset: legacy private-range trustHow many reverse-proxy hops in front of the backend PicPeak trusts for X-Forwarded-For / X-Forwarded-Proto. See TRUST_PROXY below.
EMAIL_QUEUE_ENCRYPTION_KEYNoJWT_SECRETEncrypts invitation, password-reset and temporary-password variables, and gallery passwords and client PINs, while their emails wait in the queue. At least 32 characters (openssl rand -hex 32). Keep it unchanged while such mail is pending: a changed or lost key makes those queued messages fail instead of sending. If you rotate JWT_SECRET, set this to the old value first or let the queue drain

TRUST_PROXY

PicPeak keys rate limits, login lockouts and audit addresses on the client IP, and decides from X-Forwarded-Proto whether a request arrived over HTTPS. Both headers are only believed from proxies inside the TRUST_PROXY boundary. The value is anything Express’ trust proxy setting accepts: a number of hops, loopback, linklocal, uniquelocal, a CIDR, a comma list, true, or false/0.

DeploymentDefaultSet it to
Compose stack, clients reach the frontend port directly1 (the bundled frontend nginx)Leave it
Compose stack behind another reverse proxy (Traefik, Nginx Proxy Manager, Caddy, Cloudflare Tunnel) that forwards to the frontend port12
All-in-one image, clients reach the container directly0 (no proxy inside the image)Leave it
All-in-one image behind a reverse proxy01, plus one for each further proxy in front
Native install, or an older compose file that does not set itLegacy: trust loopback and every private-range address, with a warning at startupThe exact number of proxies in front of the backend, or false when there is none

With too low a value every visitor appears under the proxy’s address: one shared rate-limit budget, and five failed logins from anyone lock out logins for everyone for 15 minutes. With too high a value a client can choose its own address through X-Forwarded-For. A hop count is only safe when clients cannot take a shorter path to the app; that is why the Compose stack publishes the backend port on host loopback only. Never use true unless every path to the app crosses a proxy you control.

When TRUST_PROXY is 1 and a request shows another private-range proxy in front of the bundled nginx, the backend logs one warning per process suggesting 2. See Reverse Proxy → Client IP.

Network and transport

VariableRequiredDefaultDescription
PICPEAK_BIND_ADDRESSNo0.0.0.0Compose: host address the frontend port is published on. Set 127.0.0.1 when a TLS reverse proxy on the same host fronts PicPeak, so the plain-HTTP port is not reachable around the proxy. The installer writes 127.0.0.1 in reverse-proxy mode
PICPEAK_BACKEND_BIND_ADDRESSNo127.0.0.1Compose: host address the raw backend port (BACKEND_PORT) is published on. The frontend reaches the backend over the Compose network, so nothing else needs it. Set 0.0.0.0 only when something on another host must reach the backend port directly, and set TRUST_PROXY to match
LISTEN_HOSTNoevery interfaceAddress the Node process listens on. For a native install whose reverse proxy runs on the same host, set 127.0.0.1; the installer does this in its TLS modes
ENABLE_HSTSNofalsetrue sends Strict-Transport-Security (one year, subdomains, preload) and upgrade-insecure-requests. Only for installs that are reached over HTTPS exclusively. The installer sets it in its TLS modes
INTEGRATION_PRIVATE_ORIGINSNo---Comma-separated exact origins (scheme, host, port) of analytics servers and OIDC identity providers on a private network, for example http://umami:3000,https://idp.internal. Without an entry PicPeak refuses private, loopback and similar addresses for these integrations. Cloud metadata addresses stay blocked even when listed
MAIL_PRIVATE_ENDPOINTSNo---Comma-separated smtp://host:port and imap://host:port entries for mail servers on a private, loopback or carrier-grade NAT (Tailscale) address. The SMTP_HOST/SMTP_PORT pair from the environment is trusted on its own (port 1025 when SMTP_PORT is unset); any other private mail server, including one changed in the admin UI, is refused until it is listed. Metadata, link-local and reserved addresses stay forbidden. An invalid entry is ignored with one error in the log

Authentication cookies

Admin, gallery, and guest sessions are carried in HTTP cookies. These variables control how those cookies are set.

VariableRequiredDefaultDescription
COOKIE_SECURENoauto in production, false in developmenttrue, false, or auto — see below
COOKIE_SAMESITENoLaxLax, Strict, or None
COOKIE_DOMAINNo---Set to .example.com only if serving cookies across subdomains
ValueBehaviorWhen to use
(unset)auto when NODE_ENV=production, false otherwiseDefault. Works for HTTPS and plain-HTTP installs
autoDecide per request: Secure on HTTPS, not on HTTPMixed HTTPS + LAN HTTP access (see below)
trueAlways set the Secure flag (HTTPS-only cookies)Recommended once the site is only ever reached over HTTPS. Login over plain HTTP no longer works
falseNever set the Secure flagLocal/LAN-only deployments with no HTTPS

The value is trimmed and case-insensitive; 1/yes/on mean true and 0/no/off mean false. Any other value is treated as auto, with one warning in the log at startup. The Compose files pass COOKIE_SECURE=${COOKIE_SECURE:-auto}. The installer writes true for installs behind a TLS proxy and false for plain-HTTP installs.

A common self-hosted setup:

  • Public: HTTPS via a reverse proxy like Nginx Proxy Manager, Traefik, or Caddy (e.g. https://photos.example.com)
  • LAN: Plain HTTP on a private address (e.g. http://192.168.1.50:3000, the frontend port) for admin access without going through the proxy

With COOKIE_SECURE=true, LAN login breaks because browsers refuse to send Secure cookies over HTTP. With COOKIE_SECURE=false, HTTPS login loses the security of the Secure flag. auto, the production default, decides the Secure flag per request — the backend reads req.secure from Express, which reflects the X-Forwarded-Proto header forwarded by your reverse proxy.

Requirements for auto mode:

  1. Your reverse proxy must forward X-Forwarded-Proto: https on HTTPS requests. Nginx Proxy Manager, Traefik, and Caddy do this by default. Custom Nginx configs need proxy_set_header X-Forwarded-Proto $scheme;.
  2. Your proxy must be inside the TRUST_PROXY boundary. In the Compose stack that means TRUST_PROXY=2 when a proxy forwards to the frontend port; on the all-in-one image TRUST_PROXY=1. A proxy outside the boundary is not believed, every request counts as plain HTTP, and cookies lose their Secure flag.

The backend port (3001) is published on host loopback only, so LAN access goes through the frontend port. To reach the backend port from another machine, set PICPEAK_BACKEND_BIND_ADDRESS.

Database

VariableRequiredDefaultDescription
DATABASE_CLIENTNopgDatabase client (pg or sqlite3)
DB_HOSTNopostgresPostgreSQL host (defaults to bundled container)
DB_PORTNo5432PostgreSQL port
DB_USERNopicpeakDatabase username
DB_PASSWORDYes (generated in Docker)---Database password. The Compose stack generates one on first start when it is unset
DB_NAMENopicpeak_prodDatabase name
DB_SSLNofalsetrue connects to PostgreSQL over TLS and verifies the certificate and the host name — in the application, at startup, and for backup dumps and restores. The bundled database has no TLS, so leave it off for the default Compose stack. 1/yes/on and 0/no/off are accepted; any other value stops the backend at startup
DB_SSL_CANo---CA for a private or self-signed server certificate: PEM text, or the path of a read-only file mounted into the container
DB_SSL_REJECT_UNAUTHORIZEDNotruefalse accepts any certificate. Insecure; prefer DB_SSL_CA
DB_SSL_SERVERNAMENo---Name on the certificate when it is not DB_HOST (for example when DB_HOST is an IP address or a Compose alias). The application verifies this name. psql/pg_dump (startup, backups, restores) verify it too when DB_HOST is an IP address; for a DNS DB_HOST they verify the CA chain only, so pair it with DB_SSL_CA

DB_SSL=true used to encrypt without checking the certificate. It now verifies certificate and host name (beta from v3.167.2-beta.0, stable from v3.134.2). An external database with a self-signed or private-CA certificate needs DB_SSL_CA (or, if you accept the risk, DB_SSL_REJECT_UNAUTHORIZED=false) before you upgrade, or the backend cannot connect.

Redis

VariableRequiredDefaultDescription
REDIS_PASSWORDYes (generated in Docker)---Redis password. The Compose stack generates one on first start when it is unset

Admin account

VariableRequiredDefaultDescription
ADMIN_USERNAMENoadminInitial admin username
ADMIN_EMAILNo[email protected]Initial admin email (used for login)
ADMIN_PASSWORDNoauto-generatedInitial admin password

Email (SMTP)

VariableRequiredDefaultDescription
SMTP_HOSTNo---SMTP server hostname
SMTP_PORTNo587SMTP server port
SMTP_SECURENofalseUse TLS (true for port 465)
SMTP_USERNo---SMTP username
SMTP_PASSNo---SMTP password or API key
EMAIL_FROMNo[email protected]Sender email address

A mail server on a private address needs an entry in MAIL_PRIVATE_ENDPOINTS unless it is the SMTP_HOST/SMTP_PORT pair set here.

Application URLs

VariableRequiredDefaultDescription
FRONTEND_URLNohttp://localhost:3000Frontend origin (scheme + host + port, no trailing slash)
ADMIN_URLNohttp://localhost:3000Admin origin (usually same as FRONTEND_URL)
API_URLNohttp://localhost:3001Public API URL for email assets
VITE_API_URLNo/apiFrontend API base path (keep /api for Docker)

Ports

VariableRequiredDefaultDescription
BACKEND_PORTNo3001Host port for the backend, published on PICPEAK_BACKEND_BIND_ADDRESS (host loopback by default)
FRONTEND_PORTNo3000Host port for the frontend, published on PICPEAK_BIND_ADDRESS (all interfaces by default)
DB_PORTNo5432Host port for PostgreSQL. Only the source-build docker-compose.yml publishes it, on 127.0.0.1; the production compose file does not publish PostgreSQL
REDIS_PORTNo6379Host port for Redis. Only the source-build docker-compose.yml publishes it, on 127.0.0.1

Storage

VariableRequiredDefaultDescription
APP_STORAGENo./storageHost path for storage
APP_DATANo./dataHost path for application data
LOGSNo./logsHost path for logs
APP_BACKUPNo./backupHost path mounted at /backup for the built-in local backups. Keep it outside APP_STORAGE, or the backup would include itself. In the production compose file from v3.142.1-beta.0 (the compose file on the stable branch does not mount /backup yet)
APP_UPDATENo./updateHost path for the request and status files of in-app updates. Its request/ and status/ folders are created on the next docker compose up -d even when the feature stays off. Beta compose file only
EXTERNAL_MEDIA_ROOTNoAuto-detected or set by ComposeLibrary path visible to the backend process. In Docker, set this to the container-side path of the read-only mount; see External Media configuration.

External-folder watching

Available in builds containing PicPeak PR #1345 . These settings control automatic imports for reference-mode events. Each event must also opt in using Watch folder for new files; the global default does not enable watching for every event. See External Media for setup, permissions and deletion behaviour.

VariableRequiredDefaultDescription
EXTERNAL_MEDIA_WATCHNotrueGlobal service switch. Set false to stop automatic external imports for all events; saved per-event watch settings remain unchanged.
EXTERNAL_MEDIA_WATCH_POLLINGNofalseUse repeated filesystem metadata checks instead of notifications. Useful for NFS/SMB shares that do not report writes from other machines.
EXTERNAL_MEDIA_WATCH_POLL_INTERVAL_MSNo5000File polling interval when polling is enabled (5 seconds).
EXTERNAL_MEDIA_WATCH_SWEEP_INTERVAL_MSNo900000Full scan of watched folders every 15 minutes as a fallback. 0 disables this scan.
EXTERNAL_MEDIA_WATCH_DEBOUNCE_MSNo10000Quiet period after the last detected new file before starting an import (10 seconds).
EXTERNAL_MEDIA_WATCH_STABILITY_MSNo5000Settling interval for new files (5 seconds). Automatic passes defer recently modified or changing files and retry later.
EXTERNAL_MEDIA_WATCH_RECONCILE_INTERVAL_MSNo60000Advanced: how often the backend re-reads event watch settings (1 minute). Add this explicitly to Compose’s backend environment block if overriding it.

All interval values are in milliseconds. The updated bundled Compose files forward the first six options from .env; they do not forward EXTERNAL_MEDIA_WATCH_RECONCILE_INTERVAL_MS. Custom Compose definitions must pass the desired variables through environment or env_file. On AIO, set them on the PicPeak container. Recreate the container after changes; for a native process, restart the backend with the new environment.

The watcher uses the local EXTERNAL_MEDIA_ROOT with both local and S3 storage backends. These settings do not configure the managed-storage file watcher or the S3 prefix walker.

Watch-folder import

The managed-storage file watcher (local storage only).

VariableRequiredDefaultDescription
FILE_WATCHER_CONCURRENCYNo2Photos processed in parallel. Raise it on hosts with memory to spare, lower it to 1 on very small hosts
FILE_WATCHER_STABILITY_MSNo2000How long a new file must stop growing before it is imported. Raise it on a slow NAS or a camera tether that writes in bursts. Beta channel only
FILE_WATCHER_POLL_INTERVAL_MSNo100How often that is checked. Beta channel only

Docker user mapping

PUID and PGID are no longer read. The backend container starts as root, hands its bind mounts to UID 1001 and drops privileges; to run as another UID, pin user: in a compose override. See Docker → Permissions.

Used for the static HTML title and meta description on the SPA shell — the fallback that catches link-preview fetchers (WhatsApp Business API, Twilio, LinkPreview, etc.) that don’t trigger the per-event Open Graph endpoint. Substituted into index.html at frontend-container start, so changes take effect on the next docker compose up -d frontend — no frontend rebuild needed.

VariableRequiredDefaultDescription
BRAND_TITLENoPicPeakTitle shown when a non-crawler fetcher reads index.html
BRAND_DESCRIPTIONNoPhoto gallery shared with PicPeak.Meta description / og:description for the same fallback

Release channel

VariableRequiredDefaultDescription
PICPEAK_CHANNELNostableImage tag: stable, beta, or a specific version
UPDATE_CHECK_ENABLEDNotrueShow update notifications in admin dashboard

In-app updates

Beta channel only (from v3.165.0-beta.0). Off by default. See In-App Updates.

VariableRequiredDefaultDescription
PICPEAK_SELF_UPDATENoofftrue turns the feature on. Any other value, including unset, keeps it off and the update dialogs unchanged. picpeak-setup.sh --enable-self-update sets it
PICPEAK_SELF_UPDATE_REQUEST_TTL_MINUTESNo10How long a filed update request waits for the updater before the backend withdraws it
COMPOSE_PROFILESContainer updater---updater starts the updater container. Only for the container packaging
PICPEAK_PROJECT_DIRContainer updater---Absolute host path of the directory with docker-compose.production.yml. Must be the real host path; the updater refuses to run on a mismatch
PICPEAK_UPDATER_TAGNo1Tag of ghcr.io/picpeak/picpeak/updater. 1 follows the latest 1.x; set an exact version such as 1.0.0 to pin it
PICPEAK_UPDATER_REQUEST_MAX_AGENo15Read by the updater, not the backend: minutes a request may be old before the updater discards it instead of updating

Analytics

Umami and Rybbit are configured under Settings → Analytics, not in the environment. The build-time VITE_UMAMI_URL, VITE_UMAMI_WEBSITE_ID and VITE_UMAMI_SHARE_URL are no longer read. See Analytics.

VariableRequiredDefaultDescription
ANALYTICS_ALLOW_INSECURE_HTTPNofalsetrue allows an http:// analytics collector. Ignored when NODE_ENV=production: production collectors must use HTTPS

Public landing page

VariableRequiredDefaultDescription
PUBLIC_SITE_CACHE_TTL_MSNo60000Cache TTL for the landing page in milliseconds

Upload limits

Limits on what the server admits, set as JSON in the backend environment and applied after a restart. Empty means the defaults; omitted keys keep their default. Sizes are integer bytes, counts positive integers. Unknown keys and zero, negative or fractional values are refused. The defaults are the same on the stable and the beta channel. The full reference is docs/PUBLIC_UPLOAD_LIMITS.md in the code repository.

VariableRequiredDefaultDescription
PUBLIC_UPLOAD_LIMITS_JSONNosee belowLimits on public uploads only: gallery guests and PicTransfer file requests. Photographer, admin, API and import uploads never count
ADMIN_UPLOAD_LIMITS_JSONNosee belowAuthenticated uploads (admin UI, API token, resumable uploads, PicTransfer files). These are never rate-limited or charged; only what is in flight at once is bounded

Public defaults: one request carries at most 95 MiB (never less than one file of the configured maximum size). Lifetime allowances, which deleting a file does not give back:

ScopeLifetimeConcurrent requestsPer hour
Gallery50 GiB / 20,000 files1620 GiB
Guest50 GiB / 20,000 files1620 GiB
File request50 GiB / 20,000 files420 GiB
Account500 GiB / 200,000 files32100 GiB
Deploymentunlimited (free-disk headroom)64200 GiB

Admin defaults: at most the smaller of 50 GiB and 25% of free space staged at once, 50,000 staged files, 16 concurrent requests per admin account, 64 per deployment, 10 minutes per request or chunk. Both profiles keep 512 MiB and 1,024 inodes free (inodes only where the filesystem counts them); headroomPercent additionally keeps a share of the volume free and is off by default.

PUBLIC_UPLOAD_LIMITS_JSON={"requestBytes":209715200,"gallery":{"bytes":107374182400}} ADMIN_UPLOAD_LIMITS_JSON={"stagedBytes":214748364800,"accountRequests":32}

Larger request bodies also need matching limits in your reverse proxy.

Image and media processing

Image parsing and transforms run in a small pool of worker processes outside the backend; ffmpeg, ffprobe and exiftool run under a process guard where the host supports it (Linux with the native build, which the Docker images include). The backend logs one line at startup saying which protections are active. All overrides must be positive integers. Defaults accept what earlier versions accepted. The full reference is docs/MEDIA_PROCESSING.md.

VariableRequiredDefaultDescription
IMAGE_WORKER_MEMORY_MIBNoa quarter of host/container memory, 768 MiB to 4 GiBMemory limit per image worker. At most two workers run, within half the memory
IMAGE_MAX_PIXELSNo268402689Largest image in pixels. An image over a limit is refused at upload with a message naming it
IMAGE_MAX_DIMENSIONNo65535Largest width or height in pixels
IMAGE_MAX_FRAMESNono limitMost frames (animated images)
IMAGE_MAX_DECODED_MIBNono limitLargest decoded size
IMAGE_MAX_INPUT_MIBNono limitLargest input file
IMAGE_WORKER_QUEUE_LENGTHNo256Waiting jobs before on-demand thumbnail/preview requests are answered 503 with Retry-After. Queued uploads wait instead of being refused
IMAGE_WORKER_TIMEOUT_MSNo120000Time limit per job, counted from when it starts
MEDIA_PROCESS_CONCURRENCYNo2ffprobe/ffmpeg/exiftool jobs at once
MEDIA_PROCESS_QUEUE_LENGTHNo256Waiting media jobs
MEDIA_WORKER_MEMORY_MIBNo2048Memory limit under the guard for ffprobe, exiftool and poster frames
MEDIA_FFMPEG_MEMORY_MIBNono limitMemory limit under the guard for a transcode
MEDIA_FFMPEG_THREADSNoCPU count, at most 4ffmpeg threads
MEDIA_PROBE_TIMEOUT_MS / MEDIA_RAW_TIMEOUT_MSNo30000Time limits for a probe and a RAW preview
MEDIA_THUMBNAIL_TIMEOUT_MSNo60000Time limit for a video poster frame
MEDIA_MAX_SNAPSHOT_MIBNo1024Private input copies staged at once; a larger file is read where it is
MEDIA_MAX_VIDEO_PIXELS / MEDIA_MAX_VIDEO_DIMENSION / MEDIA_MAX_VIDEO_STREAMS / MEDIA_MAX_VIDEO_FPSNo40000000 / 16384 / 16 / 240Video limits. A video over one keeps its original and gets the placeholder tile with a note
MEDIA_MAX_INPUT_MIB / MEDIA_MAX_VIDEO_OUTPUT_MIB / MEDIA_MAX_VIDEO_DURATION_SECONDS / MEDIA_MAX_VIDEO_PIXEL_FRAMESNono limitOptional further video limits
MEDIA_PROCESS_HOST_IDNo/etc/machine-id, else a generated idHost identity for crash recovery. Set it only when several backend processes on one host do not share the data directory
MEDIA_PROCESS_LEASE_PATHNo<temp>/picpeak-media-leasesLease directory for crash recovery; must be on a local filesystem

Refreshing old renditions

Beta channel only (from v3.174.2-beta.0). Thumbnails, previews and heroes are written without the photo’s EXIF (GPS position, camera, copyright). After the upgrade a background pass rebuilds the renditions written before that, one photo at a time, starting with link-preview covers.

VariableRequiredDefaultDescription
RENDITION_REFRESH_DISABLEDNofalsetrue skips the pass
RENDITION_REFRESH_DELAY_MSNo30000Wait after startup before the pass begins
RENDITION_REFRESH_PAUSE_MSNo200Pause between photos

Storage backend

See Storage Backends for the migration playbook and provider-specific examples.

VariableRequiredDefaultDescription
STORAGE_BACKENDNolocallocal or s3
STORAGE_S3_BUCKETWhen s3---Bucket name
STORAGE_S3_REGIONNous-east-1AWS region; use auto for R2
STORAGE_S3_ACCESS_KEYWhen s3---Access key ID
STORAGE_S3_SECRET_KEYWhen s3---Secret access key
STORAGE_S3_ENDPOINTNo---Custom endpoint (MinIO, R2, B2, Spaces…)
STORAGE_S3_PREFIXNo---Namespace inside the bucket
STORAGE_S3_FORCE_PATH_STYLENoauto when endpoint settrue for MinIO and IP-based endpoints
STORAGE_S3_SSLNotruefalse for plain-HTTP endpoints (dev MinIO)
STORAGE_AUTO_IMPORTNofalseEnable the S3 prefix walker (replaces chokidar in S3 mode)
STORAGE_AUTO_IMPORT_INTERVAL_MSNo300000Walker poll interval (5 minutes)
VIDEO_THUMBNAIL_MAX_SOURCE_BYTESNo536870912 (512 MB)S3 only. Largest video PicPeak downloads from the bucket to rebuild a missing video thumbnail when a guest opens a gallery; a bigger video gets the placeholder tile instead. Does not affect uploads or local storage. See Video Support → Thumbnail generation. Builds containing PicPeak PR #1417 .

Video copies

The worker behind Browser-playable copies of videos (Video Support → Browser playback). All optional; the setting itself is switched on in the admin, not here. Builds containing PicPeak PR #1761 ; beta channel only.

VariableRequiredDefaultDescription
VIDEO_RENDITION_CONCURRENCYNo1Transcodes running at once. One is right for a small server that also serves galleries; each extra worker keeps another core busy.
VIDEO_RENDITION_POLL_MSNo5000How often an idle worker looks for queued videos.
VIDEO_RENDITION_STUCK_TIMEOUT_MSNo7200000 (2 hours)A video claimed by a worker that died is queued again after this long. Raise it if a single transcode on your hardware can take longer.
VIDEO_RENDITION_TIMEOUT_MSNo3600000 (1 hour)The least time a transcode gets before it is killed and the video marked failed. A longer video gets 20 seconds per second of video, up to VIDEO_RENDITION_MAX_TIMEOUT_MS.
VIDEO_RENDITION_MAX_TIMEOUT_MSNo86400000 (24 hours)Upper bound for that per-video time limit.
VIDEO_RENDITION_MAX_EDGENo1920Longest edge of the copy in pixels. Smaller sources are not upscaled.
VIDEO_RENDITION_CRFNo23x264 quality (lower is better and larger).
VIDEO_RENDITION_DISABLEDNofalsetrue keeps the worker from starting on this instance, for example on a replica that should only serve requests.
VIDEO_HLS_CONCURRENCYNo1Adaptive-streaming transcodes at once. Adaptive streaming is off until switched on under Settings → General
VIDEO_HLS_MAX_DURATION_SECONDSNo1800Longest video that gets adaptive streaming; a longer one keeps playing from the original
VIDEO_HLS_MAX_SOURCE_MIBNo4096Largest source file that gets adaptive streaming

Webhooks

See Webhooks for event catalog, payload shapes, and verification examples.

VariableRequiredDefaultDescription
WEBHOOK_ALLOW_PRIVATE_URLSNofalseWhen true, allows webhooks to POST to private/loopback addresses. Production must leave this off. Dev only.
WEBHOOK_DELIVERY_INTERVAL_MSNo5000How often the worker polls for pending deliveries
WEBHOOK_DELIVERY_CONCURRENCYNo5Max in-flight deliveries per tick. Bump if your receivers are slow.
WEBHOOK_HTTP_TIMEOUT_MSNo10000Per-request timeout
WEBHOOK_MAX_ATTEMPTSNo5Total attempts before a delivery is marked failed

Incoming mail

Capacity and retention for mail captured from the accounting mailbox and additional mailboxes. The limits apply before a new message is downloaded and never delete mail on the IMAP server. A message refused for capacity or rate stays unread on the server and is fetched by a later poll; the backend logs one warning per mailbox, limit and hour naming the variable. Values must be positive integers; invalid values fall back to the defaults. The full reference is docs/incoming-mail-retention.md.

VariableRequiredDefaultDescription
EMAIL_INTAKE_INSTALLATION_BYTESNo2147483648 (2 GiB)Shared byte budget for the unhandled intake backlog
EMAIL_INTAKE_MAILBOX_BYTESNo536870912 (512 MiB)Byte budget per mailbox
EMAIL_INTAKE_INSTALLATION_ROWSNo50000Shared ceiling on unhandled items
EMAIL_INTAKE_MAILBOX_ROWSNo10000Ceiling per mailbox
EMAIL_INTAKE_INSTALLATION_PER_HOURNo1000Shared admissions per hour
EMAIL_INTAKE_MAILBOX_PER_HOURNo500Admissions per hour per mailbox
EMAIL_INTAKE_SENDER_PER_HOURNo50Admissions per hour for one sender in one mailbox
EMAIL_INTAKE_RETENTION_DAYSNounset (keep forever)Opt-in age limit for captured messages and untouched intake documents. A value below 91 is raised to 91 with a warning. Booked, edited or otherwise curated documents are never removed
EMAIL_INTAKE_METADATA_RETENTION_DAYSNo7How long bare refusal/failure records are kept
EMAIL_INTAKE_MAX_MESSAGE_BYTESNo26214400 (25 MiB)Largest message; a bigger one is recorded once as an error and marked seen
EMAIL_INTAKE_MAX_ATTACHMENTSNo25Most attachments per message
EMAIL_INTAKE_MAX_ATTACHMENT_BYTESNo26214400 (25 MiB)Largest attachment

Automatic scene tags

Beta channel only. Uses the same picpeak-ml sidecar, FACE_ML_URL and FACE_ML_TOKEN as face recognition, but has its own feature flag (autoTagging) and per-gallery switch, both off by default.

VariableRequiredDefaultDescription
TAG_PROCESSOR_CONCURRENCYNo1Tagging workers in the backend
TAG_ENCODER_ONNX_URL / TAG_ENCODER_ONNX_SHA256No---Build arguments for the sidecar image. The published sidecar does not include the tagging model yet; build it with these (see ml/README.md in the code repository), or tagged galleries stay queued

Contract signing evidence

Beta channel only. See Contracts.

VariableRequiredDefaultDescription
PICPEAK_EVIDENCE_KEYNokey file storage/business-docs/keys/evidence.key, created with the first signatureKey that encrypts each signer’s name, email address, IP address, user agent and decline reason. 64 hex digits or 43 characters of base64 are used as the key itself; any other value is treated as a passphrase. Use a raw 32-byte key in production (openssl rand -hex 32): a passphrase is derived with a fixed salt, so the evidence is only as strong as the passphrase, and every start logs a warning while one is configured. A value that is one or two characters off a key is refused at startup. When unset, PicPeak generates the key file; once it exists, every start logs a warning asking you to back it up
PICPEAK_EVIDENCE_KEYS_OLDNo(unset)Earlier evidence keys, comma-separated, in the same forms as PICPEAK_EVIDENCE_KEY. Evidence written under one of them stays readable while scripts/rotate-evidence-key.js moves it to the current key; new evidence is always written under the current key. A malformed entry is refused at startup. Needed only while rotating

Losing or changing the evidence key

Each stored value names the key it was written under. PicPeak reads it with the current key, any key in PICPEAK_EVIDENCE_KEYS_OLD, and any file named evidence.key.<keyId> next to the key file in storage/business-docs/keys/. New evidence is always written under the current key.

  • Losing the key that evidence was written under, with no copy anywhere, makes those signer names, email addresses and IP addresses unreadable. The signatures, the signed PDFs and the signing event log stay valid.
  • Recovery means making the lost key readable again: put its value in PICPEAK_EVIDENCE_KEYS_OLD, or restore its file as evidence.key (if it is the current key) or evidence.key.<keyId> (if it is an old one).
  • System Health shows the key in use, how many stored values are still under an older key that can be read, and how many are under a key PicPeak no longer has.

Rotating the key

  1. Set PICPEAK_EVIDENCE_KEY to the new key. Keep the old one readable: put it in PICPEAK_EVIDENCE_KEYS_OLD, or — if the old key was the generated key file — leave the file where it is.

  2. Restart the backend.

  3. Run the rotation script:

    docker compose exec backend node scripts/rotate-evidence-key.js --dry-run # counts per key, changes nothing docker compose exec backend node scripts/rotate-evidence-key.js

    On its first run it keeps a replaced generated key file as evidence.key.<keyId>, then re-encrypts every value that is under another key, in batches. It is safe to stop and run again at any time: a run re-encrypts only values still under another key, so a second run after a complete one changes nothing. A signature recorded while it runs is never overwritten; it is skipped and picked up by the next run. It prints how many values it moved and how many are still under another key.

  4. Keep the old key until the script reports 0 values still under another key, then remove it from PICPEAK_EVIDENCE_KEYS_OLD (or delete the evidence.key.<keyId> file).

Rotation changes only the encrypted values. The content hash, the PDFs, the signing certificate and the signing log’s hashes stay exactly as they were.

Back up PICPEAK_EVIDENCE_KEY (and any old keys still needed) with your other secrets, or the key files with the database. They sit under business-docs, which is part of the backup export.

Malware scanner for customer documents

Beta channel only. See Customer Documents → Malware scanning. Unset, no scanner is used and customer uploads wait for an admin’s review. The bundled compose files have an opt-in ClamAV service: docker compose --profile clamav up -d, then set CLAMAV_HOST=clamav.

VariableRequiredDefaultDescription
CLAMAV_HOSTNo—Host of a clamd listening on TCP. Unset turns scanning off
CLAMAV_PORTNo3310clamd TCP port
CLAMAV_TIMEOUT_MSNo30000Connect plus the whole scan; a scan that takes longer leaves the file awaiting review
CLAMAV_MAX_BYTESNo26214400 (25 MiB)Largest file sent to clamd; a larger one stays awaiting review. Keep it at least as large as the upload limit, and raise clamd’s StreamMaxLength to match if you raise it

Backup and restore

See Backup and Restore.

VariableRequiredDefaultDescription
BACKUP_DIRNo/backup in the production compose file (from v3.142.1-beta.0); /data/backup in the all-in-one image; otherwise unsetA backup root the entrypoint creates and hands to UID 1001 on start. Set it together with a /backup mount to keep database dumps at /backup/database. See Where the database dump is written
BACKUP_MANIFEST_KEYNogenerated (see below)Key that signs and verifies backup manifests: 64 hex digits (openssl rand -hex 32). A backup whose manifest does not verify is not restored. Keep a copy off the host: a lost key never verifies a backup again. A value that is not 64 hex digits disables signing (with a warning at startup) but still verifies the backups it signed before
BACKUP_MANIFEST_KEY_FILENoCompose: /run/secrets/backup_manifest_key; otherwise DATA_DIR/backup-manifest.key (/data/db/backup-manifest.key in the all-in-one image)File holding the key. It must be a private regular file outside the backed-up storage. Compose generates it in the backend’s secrets volume; native and all-in-one installs create it with the first backup
BACKUP_MANIFEST_KEYS_OLDNo---Earlier 64-hex signing keys, comma-separated, while rotating
BACKUP_MANIFEST_LEGACY_KEYNo---The original key string of an older install, for manifests signed before the current format
BACKUP_MANIFEST_RECOVERY_SHA256 / BACKUP_MANIFEST_RECOVERY_REASONNo---One-time approval to restore one unauthenticated backup (an unsigned one, or one whose key is lost) after you inspected it offline: the full digest from backend/scripts/backup-manifest-recovery-digest.js plus a reason. Restore on an isolated host, then remove both variables
RESTORE_ALLOWED_ROOTSNo---Extra directories a restore may read from, separated by : (; on Windows). By default only the backup destination and the manifest path are allowed
BACKUP_SSH_KNOWN_HOSTSNoknown_hosts next to the rsync SSH keyknown_hosts file with the independently verified host key of the rsync destination. Unknown or changed host keys are refused; nothing is trusted on first use
PICPEAK_IMPORT_MAX_ENTRIESNo2000000Most entries (files and directories) a .picpeak import may contain; more is refused before extracting
PICPEAK_IMPORT_MAX_EXPANDED_BYTESNo1 TiBMost bytes a .picpeak import may unpack to. Extraction also stops when the extraction directory runs out of free space
PICPEAK_IMPORT_MAX_MANIFEST_BYTESNo256 MiBLargest manifest.json a .picpeak import may contain
PICPEAK_IMPORT_MAX_ROW_BYTESNo64 MiBLargest single database row in a .picpeak import
PICPEAK_RESTORE_DRAIN_GRACE_MSNo60000How long running requests may finish before a .picpeak restore closes their connections. If work still does not end, the restore is aborted
PICPEAK_IMPORT_WORKER_MEMORY_MIB / PICPEAK_IMPORT_WORKER_TIMEOUT_MS / PICPEAK_IMPORT_WORKER_CPU_SECONDSNoscaled to the archive; no CPU limitOverride the restore worker’s budgets
LOG_TO_CONSOLENofalseWhen true, the backend also logs to the container console, so errors such as a failed database dump appear in docker compose logs. Otherwise they go to logs/error.log and logs/combined.log

BACKUP_MANIFEST_REQUIRE_KEYED is gone: every manifest now needs a key, and a manifest without a valid signature fails closed. .picpeak imports need Linux, the native build (included in the Docker images) and STORAGE_PATH on a local filesystem; where the host does not qualify, only the import is switched off and one startup log line says why.

Keep the backup manifest key separately from the backups, ideally off the host. Without it a backup cannot be authenticated on a fresh host. With Compose, BACKUP_MANIFEST_KEY set in .env replaces the generated key file; the replaced value is kept in backup_manifest_key.previous in the secrets volume so you can list it in BACKUP_MANIFEST_KEYS_OLD.

Last updated on