See .env.dev.example in the workspace root for local development and .env.server.example for the server stack. Those files intentionally list only the vars a typical install sets; every other key has a working default and can simply be added to your workspace-root .env to override. This table is the complete, overridable reference:
Email delivery is not configured here. Point PrintStream at your own SMTP server in the app itself (Settings > Plugins > the SMTP email plugin); email-backed features (password reset, email notifications) need no environment variables.
| Variable | Default | Description |
|---|---|---|
API_PORT |
4000 |
API HTTP port. |
NODE_ENV |
development |
Runtime mode (development / production / test). The Compose stacks set production. |
DATABASE_URL |
postgresql://postgres:postgres@db:5432/printstream?schema=public |
Prisma PostgreSQL URL. Devkit replaces this with the checkout's Compose database URL. |
DATABASE_CONNECTION_LIMIT |
(Prisma default: CPUs×2+1) | Prisma connection-pool size for this process. Set it to match expected concurrency against the Postgres max_connections budget; the CPU-count default can bottleneck a busy single-process multi-workspace API. Appended to DATABASE_URL as connection_limit. |
DATABASE_POOL_TIMEOUT |
(Prisma default: 10s) | Seconds a query waits for a free pooled connection before erroring (P2024). Appended to DATABASE_URL as pool_timeout. |
DB_WAIT_TIMEOUT_MS |
60000 |
Max time npm run dev:db waits for Postgres to accept connections before failing. |
DB_WAIT_RETRY_MS |
1000 |
Poll interval npm run dev:db uses while waiting for Postgres. |
CLIENT_ORIGIN |
(unset) | Comma-separated list of extra browser origins allowed CORS access. Only needed for a split topology where the web app is served from a different origin than the API; same-origin deployments (the stock single-container stack, the native app) never consult it. |
BRIDGE_SERVER_URL |
http://api:4000 |
PrintStream server URL the bridge registers with. The default expects the bridge to reach the API over the same Docker network by service name; override to http://localhost:4000 when the bridge runs outside Docker. (BRIDGE_CLOUD_URL is accepted as a legacy alias.) |
BRIDGE_LIBRARY_DIR |
./data/bridge-library |
Bridge-local directory for library-owned files and dispatch replicas. |
BRIDGE_NAME |
PrintStream Bridge |
Human-readable name used when the bridge registers and is connected to a workspace. |
MANAGED_BRIDGE |
true in the Docker example and the native app; false otherwise |
Enables managed-bridge mode for single-host self-hosting: the bundled bridge auto-pairs into the sole workspace and the Bridges settings page is hidden. Set false to keep manual connect-code pairing (cloud, multiple bridges, or a bridge on a separate host). |
MANAGED_BRIDGE_TOKEN_FILE |
/run/provision/managed-bridge-token |
Path to the managed-bridge provisioning token. In managed mode the API generates this token on first start; the bundled bridge reads it from the same path over a shared mount to authenticate its auto-pairing. Operators never set the token value, only the file location, which must resolve to the same file in both containers. |
BRIDGE_STATE_FILE |
./data/bridge-state.json |
Path where the bridge persists its connected identity and runtime token. |
BRIDGE_BACKUP_DIR |
(unset: backups disabled; native bridge builds default to <data dir>/backups) |
Directory for the bridge's on-disk backups (snapshots of its identity file + library). Point it OUTSIDE the bridge's data dir: its own bind mount in Docker (./backups in the bridge-only example, ./bridge-backups in the combined server example), ideally on another disk, so wiping or recreating the app cannot take the backups with it. See docs/operations.md for the snapshot format and restore steps. |
BRIDGE_BACKUP_INTERVAL_HOURS |
24 |
Scheduled bridge-backup cadence in hours. 0 disables the schedule and keeps backups manual-only ("Back up now" in Settings → Bridges). Retention is automatic: everything kept for 7 days, then one per week for 4 weeks, then one per month for 12 months. |
BRIDGE_AUTO_UPDATE |
false in source runs and the combined app image; true in the slim Docker bridge image and standalone builds |
When true, a bundle-capable slim Docker bridge or standalone executable installs an available compatible app update after registration. Set false in the bridge-only Compose .env to require a manual update. The combined app image's bridge role has no in-place update driver. |
BRIDGE_DISABLE_SELF_UPDATE |
false |
Pin a bundle-capable slim Docker bridge or standalone executable to its current build, including against server-initiated installs. The bridge-only Compose example passes this setting from .env. |
BRIDGE_RELEASES_DIR |
/data/releases in the slim Docker bridge image; a path under the data directory in standalone builds |
Directory for staged bridge updates. The slim Docker bridge keeps its signed app bundle and release pointers here on the persistent /data volume; standalone builds stage executable updates here. The combined app image's bridge role does not use it. |
BRIDGE_UPDATE_PUBLIC_KEY |
official PrintStream key | Optional Ed25519 public key override for verifying signed standalone binaries and slim Docker app bundles. Normal bridge installs should leave this unset. Compose .env files may use a one-line PEM with \n escapes when overriding. |
API BRIDGE_RELEASES_DIR |
./data/bridge-releases |
API-container directory containing signed bridge release JSON fragments, standalone binaries, and slim Docker app bundles served to bridges. |
SECRETS_KEY |
(unset) | Master key for encrypting sensitive stored settings at rest (including OAuth client secrets, SMTP passwords, and Bambu Cloud access tokens) via AES-256-GCM. Any non-empty string works (it is hashed to a 32-byte key). When unset, those settings are stored unencrypted and a warning is logged; set this in production. Existing plaintext values keep working and are re-encrypted on next write. Treat it as a secret; rotating it makes previously-encrypted values unreadable (re-enter them). |
AUDIT_LOG_RETENTION_DAYS |
365 |
How long durable audit-log rows are retained before scheduled maintenance prunes them. Raise for stricter compliance retention. |
CSP_ENFORCE |
false |
When true, the Content-Security-Policy is enforced (Content-Security-Policy); when false it is sent report-only (Content-Security-Policy-Report-Only). Either way, browsers POST violations to the built-in /api/csp-report sink, which logs them as [csp-report] lines (visible in the in-app Logs tab). Roll out report-only first, confirm the sink stays quiet, then set true. |
CSP_ANALYTICS_ORIGIN |
(unset) | Origin of a first-party analytics tracker (e.g. a self-hosted Umami at https://analytics.example.com), added to the CSP's script-src and connect-src so the tracker script and its event beacons work under enforcement. Leave unset when no cross-origin analytics is embedded. |
SELF_HOSTED |
(derived from the build) | Forces the deployment to identify as self-hosted/OSS (true) or cloud (false), overriding the default (derived from whether the private cloud modules are present). Self-hosted registers the email/password provider (auth-password) and hides the cloud platform-admin, marketing, and support-access surfaces; cloud registers passkeys + email codes (auth-local) and OIDC SSO (auth-oauth). Leave unset in real deployments; set true to run the OSS build from the full source tree. |
LICENSE_REFRESH_ORIGIN |
taken from the key | Overrides the cloud origin named by a signed license key. Normally leave this unset: the key already knows which deployment issued it. Subscription keys refresh there automatically; Lifetime keys contact it only for an explicit refresh, update download, or use of the optional in-app support and Suggestions connection. Community keys never use that connection. License refresh sends the key and installation id, no printer counts or telemetry, and retries silently because the signed run window decides when access ends. Support and Suggestions relay through the same signed origin only while those surfaces are used. Set an override only when a proxy rewrites the issuer address; a disagreement is logged. |
LIBRARY_DIR |
./data/library |
Directory where uploaded model files (.3mf, .gcode, .stl, .step, .obj, .gltf/.glb, .amf, .fbx) are stored. |
BACKUPS_DIR |
(unset: server backups disabled; the native build defaults to <data dir>/backups) |
Directory for the built-in whole-install server backups (database dump + persistent files). Must point OUTSIDE the app's own storage: the compose example bind-mounts a host ./backups directory, so recreating the app cannot take the backups with it. Unset keeps backups off rather than defaulting to a path inside the container. See docs/operations.md. |
BACKUP_INTERVAL_HOURS |
24 |
Scheduled server-backup cadence in hours. 0 disables scheduled creation. Self-hosted retention keeps scheduled backups for 7 days, then one per week for 4 weeks, then one per month for 12 months; manual backups remain until deleted. Cloud backups, including manual/pre-restore snapshots, have a 30-day maximum age; cloud retention continues even when scheduled creation is disabled. |
PG_DUMP_PATH / PG_RESTORE_PATH |
(unset: resolved from the embedded Postgres bin dir, then PATH) | Explicit paths to the Postgres client tools the backup system shells out to. Only needed when the tools are installed somewhere unusual; the Docker image ships a matching postgresql-client. The tool major must EQUAL the database server's major: a newer pg_dump's output does not restore into an older server, so a mismatched backup is refused up front. |
LIBRARY_MAX_UPLOAD_BYTES |
1073741824 |
Maximum accepted library upload size in bytes (default 1 GiB). |
LIBRARY_TRANSIENT_RETENTION_DAYS |
7 |
How long hidden transient library uploads are retained before scheduled cleanup removes them. |
LIBRARY_RECYCLE_RETENTION_DAYS |
30 |
How long recycle-bin (soft-deleted) library files stay restorable before scheduled cleanup removes them permanently. |
LIBRARY_UNREFERENCED_SLICE_RETENTION_HOURS |
24 |
How long unreferenced sliced outputs and unused unchanged-slice cache entries are kept before cleanup removes them. Cache hits refresh this window. |
SLICER_SERVICE_URL |
(unset) | Optional URL for the standalone slicer container. Set to enable server-side slicing orchestration. Accepts a comma-separated list of identical slicer instances; slices are assigned to the least-busy instance and progress follows the instance that owns the job. |
SLICER_SERVICE_TOKEN |
(unset) | Bearer token shared between the API and slicer container. Required by the shipped server Compose deployment. Treat this as a secret. |
SLICER_REQUIRE_AUTH |
false (true in server Compose) |
Refuse slicer startup unless SLICER_SERVICE_TOKEN is set. Leave false only for an explicitly loopback-only standalone runtime. |
SLICER_MAX_INPUT_BYTES / SLICER_MAX_INFLATED_BYTES / SLICER_MAX_ARCHIVE_ENTRIES |
536870912 / 2147483648 / 4096 |
Independent hostile-archive limits enforced by the slicer service before invoking its native engine. |
SLICER_MAX_OUTPUT_BYTES |
1073741824 |
Maximum native output file size. The API may request a smaller per-job ceiling but cannot widen this limit. |
SLICER_ENGINE_UID / SLICER_ENGINE_GID |
1001 / 0 |
Fallback Unix identity for non-job engine probes. Hostile jobs derive private high-numbered uids and gids from their server-issued ids, so concurrent engines cannot traverse files, signal processes, or inspect process state across jobs. |
SLICER_MEMORY_LIMIT / SLICER_CPU_LIMIT / SLICER_PIDS_LIMIT |
8g / 4.0 / 512 |
Server Compose resource ceilings for the native slicer container. |
SLICER_WORK_LIMIT |
4g |
Hard tmpfs ceiling for all disposable slicer work files in the server Compose deployment. This prevents native code from filling the host disk even by spreading output across many files. |
SLICER_BIND_HOST |
127.0.0.1 |
Host interface for the published slicer port in Compose (127.0.0.1 keeps it private to the server). |
SLICER_BIND_PORT |
4010 |
Host port mapped to slicer container port 4010 in Compose. |
SLICING_MAX_CONCURRENT_JOBS |
(one per slicer URL) | Maximum slicing jobs the API runs at once across all slicer instances. Defaults to the number of configured SLICER_SERVICE_URL entries. Each invocation has its own BambuStudio home, but concurrent native engines still contend for CPU and memory; prefer adding instances instead. |
SLICING_MAX_QUEUED_JOBS |
25 |
Maximum number of queued slicing jobs waiting for a concurrency slot. |
SLICING_REQUEST_TIMEOUT_MS |
1800000 |
Timeout for API-to-slicer requests. |
PUBLIC_SLICING_ENABLED |
cloud: true |
Enables anonymous slicing from /3mf-editor on the hosted cloud. The editor and its API routes are absent from self-hosted, native, and OSS deployments regardless of this value. |
PUBLIC_SLICING_MAX_CONCURRENT_JOBS |
1 (capped below total slicing concurrency in production) |
Maximum anonymous slices running at once. Production always reserves at least one total slot for workspace work, so public slicing is unavailable there when only one slicing slot exists. Development may lend its single slot to the public editor because no competing users share that process. |
PUBLIC_SLICING_MAX_QUEUED_JOBS |
20 |
Global cap covering incomplete uploads and queued anonymous slices. |
PUBLIC_SLICING_MAX_QUEUED_JOBS_PER_IP / PUBLIC_SLICING_MAX_ACTIVE_JOBS_PER_IP |
2 / 1 |
Per-address anonymous upload, queue, and execution limits. Set TRUST_PROXY correctly before relying on client addresses behind a proxy. |
PUBLIC_SLICING_MAX_INPUT_BYTES / PUBLIC_SLICING_MAX_INFLATED_BYTES / PUBLIC_SLICING_MAX_OUTPUT_BYTES |
268435456 / 536870912 / 536870912 |
Compressed input, inflated archive, and downloaded artifact limits for anonymous jobs. |
PUBLIC_SLICING_MAX_RUNTIME_MS |
1200000 |
Maximum execution time for one anonymous slice. |
PUBLIC_SLICING_UPLOAD_TTL_MINUTES / PUBLIC_SLICING_JOB_TTL_MINUTES |
30 / 60 |
Retention for abandoned uploads and completed or failed anonymous jobs. |
SLICER_DEFAULT_TARGET_ID |
(first installed target) | Override the default slicer version shown in the slice dialog. Must match an id from the built-in target manifest (/opt/printstream-slicers/targets.json). |
SLICER_ENABLE_PIPE_PROGRESS |
true |
When true, append Bambu/Orca CLI --pipe progress JSON frames into slicing job output so the UI can render determinate progress updates. |
SLICER_BAMBUSTUDIO_HOME_DIR |
under slicer work dir | Base home used by non-job probes. Every slice invocation receives a fresh home inside its disposable job directory. |
SLICER_BAMBUSTUDIO_DATA_DIR |
under slicer work dir | Persistent --datadir used for slicer presets and first-run state. Subdirectories are created per target id. |
SLICER_KEEP_WORK_DIR |
false |
Debugging aid: keep each slice job's work directory (the rewritten input 3MF and the materialized profiles the CLI loaded) instead of deleting it when the job ends. The work dirs are large and are never swept, so leave this off outside an investigation. |
PRINT_JOB_THUMBNAIL_RETENTION_DAYS |
90 |
How long completed-job thumbnail PNGs and persisted final-frame snapshot JPGs are retained before scheduled cleanup removes them. |
PLUGINS_DIR |
./data/plugins |
Directory for installed external plugins. |
TRUST_PROXY |
(unset) | Express trust proxy setting. Set only when a reverse proxy actually fronts the API (e.g. 1 for one nginx hop); with no proxy, trusting X-Forwarded-* lets clients spoof their address to the rate limiter. |
SERVE_WEB_DIR |
/app/apps/web/dist (combined image) |
Directory of the built web SPA the API serves on its own port, alongside /api and /ws: the single-container topology. The Docker image bakes this in. Set to empty to run the API alone behind a separate web tier (the split topology). |
DISCOVERY_PORT |
2021 |
UDP port the bridge uses for SSDP printer auto-discovery. |
PUBLIC_BASE_URL |
(unset) | Absolute URL used for notification media embeds (e.g. camera snapshots). |
MQTT_DEBUG_LOGS |
false |
Set to true or 1 to log raw MQTT publish/receive traffic for protocol debugging. |
CAMERA_DEBUG_LOGS |
false |
Set to true or 1 to re-enable verbose RTSP camera readiness logs such as snapshot ready and first frame. |
METRICS_ENABLED |
false |
When true, expose a Prometheus /metrics endpoint (OpenTelemetry) on METRICS_PORT for an internal scraper. Off by default: the app runs no telemetry stack unless you opt in. See docs/observability.md. |
METRICS_PORT |
9464 |
Port the Prometheus metrics endpoint binds when METRICS_ENABLED is set. Internal only: keep it on a private interface/network; do not proxy it publicly. |
NTFY_TOPIC_URL |
(unset) | Fallback ntfy topic for the notifications-ntfy plugin when a workspace has not configured its own. Honored only in single-box managed-bridge self-hosting (MANAGED_BRIDGE); ignored in multi-workspace mode, where one shared topic would leak every un-configured workspace's notifications to a single operator topic. |
VITE_API_BASE_URL |
(unset) | Build-time API base URL for the web app. Leave blank in dev (the Vite proxy handles /api) and for the combined Docker image (the web is served same-origin by the API). Only set it when building the web SPA to be hosted separately from the API. |