Sthora
Reference

Configuration

Every environment variable the control plane reads, what it defaults to, and what happens if you get it wrong.

The control plane's configuration is one validated schema. Values are read in configuration factories only — nothing else in the codebase touches process.env.

On a self-hosted install these live in /etc/sthora/.env, written by the installer at mode 0600. In development they live in apps/backend/.env; only .env.example is ever committed.

Two values have no safe default

DATABASE_URL and AUTH_SECRET. Generate the secret with openssl rand -hex 32 and never reuse a placeholder.

Service

VariableDefault
NODE_ENVdevelopmentdevelopment | test | production
APP_NAMEcloud-apiReported by /health and in the OpenAPI document
APP_VERSION0.0.1Same
PORT4600HTTP port
APP_ROLEallall | api | worker. One container runs both on a small box; the cloud deployment scales them separately from the same image.

Database and Redis

VariableDefault
DATABASE_URLRequired. Must use postgresql:// or postgres://.
REDIS_URLredis://localhost:6379BullMQ, the live metric cache, and the log bus.

Redis maxmemory-policy must be noeviction

BullMQ stores job state in Redis. An LRU policy silently evicts it and deployments disappear with no error anywhere. If you want a caching Redis with eviction, that is a second instance or a second logical database with its own policy — never the queue's.

Authentication and sessions

VariableDefault
AUTH_SECRETRequired, minimum 32 characters. Signs sessions and one-time tokens, and keys API-token digests.
AUTH_SESSION_TTL_HOURS720Absolute session lifetime.
AUTH_SESSION_RENEW_AFTER_HOURS24How old a session must be before a request slides its expiry forward.
AUTH_RESET_TOKEN_TTL_MINUTES60Password reset. Capped at 60 by the schema.
AUTH_VERIFICATION_TOKEN_TTL_MINUTES1440Email verification.
AUTH_INVITATION_TTL_HOURS168Invitation validity.
AUTH_COOKIE_NAMEcloud.sessionThe __Host- prefix is applied automatically in production.
AUTH_TRUSTED_ORIGINSEmptyCSRF allow-list — origins whose unsafe-method requests carrying the session cookie the API will act on. Empty means same-origin only, which is the self-hosted default.
SIGNUP_ENABLEDfalseSelf-service signup. Off answers 404, not 403. See signup.

HTTP surface

VariableDefault
CORS_ALLOWED_ORIGINSEmptyComma-separated. Empty disables CORS, correct for a self-hosted install serving the dashboard and API on one origin.
THROTTLE_TTL_SECONDS60Global rate-limit window.
THROTTLE_LIMIT120Requests per window per client.
TRUSTED_PROXY_HOPS0How many reverse proxies sit in front. Set to 1 for the shipped compose topology, which puts Traefik in front.

TRUSTED_PROXY_HOPS matters more than it looks

It configures Express's trust proxy, which is what makes the rate limiter and every audit record key on the real client's address rather than the proxy's — or, left at 0, protects against a client forging its own address via X-Forwarded-For.

Secret encryption

VariableDefault
SECRET_ENCRYPTION_KEY_FILEUnsetPreferred. Path to the 32-byte hex master key.
SECRET_ENCRYPTION_KEYUnsetThe key inline. Readable through /proc, through docker inspect, and out of any crash dump — prefer the file.

With neither set, the API starts and every route that does not touch a secret keeps working. The ones that do say exactly what is missing.

Metrics and monitoring

VariableDefault
HOST_PROC_PATHUnsetWhere the host's /proc is mounted.
HOST_SYS_PATHUnsetThe host's /sys.
HOST_ROOT_PATHUnsetThe host's /.
DOCKER_SOCKET_PATH/var/run/docker.sock
LOCAL_SERVER_NAMElocalName the control plane's own machine is registered under on first boot.
METRICS_SAMPLE_INTERVAL_SECONDS15
METRICS_MINUTE_RETENTION_DAYS7Minute rows before hourly rollup.
METRICS_RETENTION_DAYS90

Leave the three HOST_* paths unset outside a container. Unset inside one means every metric describes that container rather than the machine — which looks entirely plausible on a dashboard and is wrong.

Platform database backup

VariableDefault
PLATFORM_DATABASE_CONTAINERUnsetContainer name of the platform's own PostgreSQL. Unset means the database is external and its backups are its owner's job.
PLATFORM_DATABASE_USERsthora
PLATFORM_BACKUP_RETENTION_COUNT7

Deploy pipeline

VariableDefault
DEPLOY_DATA_ROOT/var/lib/sthoraWhere the platform writes on the managed server: source trees, staged archives, capped build logs.
DEPLOY_UPLOAD_STAGING_PATH/var/lib/sthora/uploadsWhere uploads land on the control plane before the driver moves them.
DEPLOY_MAX_ARCHIVE_BYTES209715200200 MB.
DEPLOY_MAX_ARCHIVE_ENTRIES20000
DEPLOY_MAX_EXTRACTED_BYTES21474836482 GB.
DEPLOY_LOG_MAX_BYTES16777216Build log cap on disk.
DEPLOY_LOG_TAIL_BYTES4096How much of the tail the history list keeps on the row.
DEPLOY_MIN_FREE_DISK_FRACTION0.15Refuse to start a build below this.
DEPLOY_BUILD_TIMEOUT_SECONDS1800
DEPLOY_PROXY_NETWORKsthora-proxyThe external network. The installer creates it; Compose never does.
DEPLOY_HOST_PORT_MIN30000
DEPLOY_HOST_PORT_MAX32000
DEPLOY_CONTAINER_LOG_MAX_SIZE_MB10Written into every rendered compose file.
DEPLOY_CONTAINER_LOG_MAX_FILES3
DEPLOY_BUILD_CONCURRENCY1Leave it at 1. Two builds at once on a 4 GB box is an OOM kill.
DEPLOY_DEPLOY_CONCURRENCY2
DEPLOY_LIFECYCLE_CONCURRENCY4
DEPLOY_NIXPACKS_ENABLEDtrueSee build types.
DEPLOY_NIXPACKS_IMAGEghcr.io/railwayapp/nixpacks:v1.39.0Pinned.
DEPLOY_BLUE_GREEN_MIN_FREE_MEMORY_MB512Free memory a blue/green deployment needs beyond the running version's usage.

Ingress and TLS

VariableDefault
PROXY_ENABLEDfalseOff by default so a developer's laptop does not bind port 80. The installer turns it on.
PROXY_IMAGEtraefik:v3.6Pinned — latest on a reverse proxy is a change nobody scheduled.
PROXY_PROJECT_NAMEsthora-proxy
PROXY_HTTP_PORT80
PROXY_HTTPS_PORT443
PROXY_ACME_EMAILUnsetCertificates are not issued at all until this is set, and the CA rejects reserved domains such as example.com.
PROXY_ACME_STAGINGfalseStaging issues untrusted certificates from a directory with far looser limits.
PROXY_HTTPS_REDIRECTtrueOnly takes effect once ACME is configured.
PROXY_BASE_DOMAINUnsetWildcard base domain. See domains and TLS.
SERVER_PUBLIC_IPDetectedChecked against a hostname's DNS before a router is attached. Detection is wrong behind NAT — set it explicitly there.

Git sources

VariableDefault
GIT_CLONE_TIMEOUT_SECONDS600
PUBLIC_BASE_URLUnsetPublic address of this control plane. Without it the portal shows webhook paths only, and server enrolment refuses rather than generating a command containing undefined.

GitHub App

Optional. Deploy keys work against every host with nothing registered anywhere.

VariableDefault
GITHUB_APP_IDUnset
GITHUB_APP_PRIVATE_KEY_FILE/etc/sthora/github-app.pem
GITHUB_APP_WEBHOOK_SECRETUnset
GITHUB_API_BASE_URLhttps://api.github.com

Alerts and notifications

VariableDefault
ALERT_DISK_USED_FRACTION0.85
ALERT_MEMORY_USED_FRACTION0.9
ALERT_MEMORY_SUSTAINED_MINUTES5
ALERT_RESTART_COUNT3
ALERT_RESTART_WINDOW_MINUTES5
ALERT_CERTIFICATE_EXPIRY_DAYS14
NOTIFY_MAX_ATTEMPTS4
NOTIFY_BACKOFF_MS10000
NOTIFY_CONCURRENCY4
NOTIFY_WEBHOOK_TIMEOUT_MS10000

Outbound email

With no transport configured the platform still runs: sends are skipped and logged, and alerts remain in-app and by webhook.

VariableDefault
EMAIL_TRANSPORTsmtp
EMAIL_FROM_ADDRESSUnsetMust align with the DKIM signing domain, or DMARC fails even when both records exist.
EMAIL_FROM_NAMESthora
EMAIL_LIST_IDUnset
EMAIL_SMTP_HOSTUnset
EMAIL_SMTP_PORT587
EMAIL_SMTP_SECUREfalsetrue for port 465; 587 upgrades with STARTTLS and wants false.
EMAIL_SMTP_USER, EMAIL_SMTP_PASSWORDUnsetOptional — an unauthenticated relay on a private network is a configured transport.
EMAIL_SMTP_POOLtrue
EMAIL_SMTP_MAX_CONNECTIONS2
EMAIL_BATCH_SIZE25
EMAIL_BATCH_DELAY_MS1000
EMAIL_MAX_ATTEMPTS3
EMAIL_RETRY_BACKOFF_MS5000

Managed services

VariableDefault
BACKUP_TIMEOUT_SECONDS3600Ceiling on one backup step. Killing a dump halfway produces a file that looks like a backup and is not one.
SERVICES_CONCURRENCY1

Agent channel

VariableDefault
AGENT_CHANNEL_ENABLEDtrueOff turns the channel away entirely — correct for an install that manages only its own machine.
AGENT_CALL_TIMEOUT_SECONDS300Ceiling on one agent call. Builds carry DEPLOY_BUILD_TIMEOUT_SECONDS instead.
AGENT_UPLOAD_CHUNK_BYTES262144base64 inflates by a third, so the wire cost is this times 1.34.
AGENT_OFFLINE_AFTER_SECONDS90Must comfortably exceed the agent's heartbeat interval.
AGENT_IMAGEghcr.io/asynccoders/agent:latestWhat the enrolment command tells an operator to run.

Plans and limits

VariableDefault
BILLING_ENFORCE_LIMITSfalseWhether limits refuse anything. Usage is metered either way.
BILLING_DEFAULT_PLANself-hostedMust match a slug in the seeded catalogue.

See plans and limits.

The other apps

The dashboard, the marketing site, and this documentation site each own their own .env.example. Only VITE_-prefixed variables reach a browser bundle, and none of the three holds a secret.

AppKey variables
DashboardPORT, VITE_APP_TITLE, VITE_API_URL (empty on a self-hosted install, where the dashboard and API share an origin)
Marketing sitePORT, VITE_SITE_NAME, VITE_SITE_URL, VITE_PORTAL_URL, VITE_API_URL
DocumentationPORT, VITE_SITE_NAME, VITE_SITE_URL, VITE_PORTAL_URL, VITE_WEBSITE_URL, VITE_REPOSITORY_URL

VITE_-prefixed values are inlined at build time. Supplying one at runtime does nothing — they are build arguments to the image, not runtime environment.

On this page