Environment Variables

This is the full reference for configuring a self-hosted Heym instance. Every variable is optional unless marked required.

This is the full reference for configuring a self-hosted Heym instance. Every variable is optional unless marked required. Values can be set in a .env file (copy .env.example) or passed directly to the process / container.

./run.sh and ./deploy.sh auto-generate SECRET_KEY and ENCRYPTION_KEY when they are empty, so a plain ./run.sh works with no manual setup.

Defaults below are the values used when the variable is unset. Note that .env.example ships a few tighter starter values (for example shorter JWT lifetimes) that override these code defaults when you copy it.

Required

VariableDescriptionDefault
SECRET_KEYJWT signing key. Must be cryptographically random and at least 32 characters. Startup fails if missing or a known placeholder.— (required)
ENCRYPTION_KEYKey used to encrypt stored credentials at rest. Generate with python -c "import secrets; print(secrets.token_hex(32))". Startup fails if missing, the known placeholder, or not the key this database was encrypted with (see Rotating ENCRYPTION_KEY).— (required)
ENCRYPTION_KEYS_EXTRAComma-separated keys accepted for decryption only, never to encrypt. Used while rotating ENCRYPTION_KEY. Empty entries are ignored; the placeholder is refused.—
HEYM_ACCEPT_CURRENT_ENCRYPTION_KEYStart the main instance once with true to accept ENCRYPTION_KEY as this database's key although nothing stored decrypts with it. Ignored on workers. See Rotating ENCRYPTION_KEY. Unset it afterwards.false

Rotating ENCRYPTION_KEY

ENCRYPTION_KEY encrypts every stored credential, the SSO client secret and short-lived sign-in state. It can be replaced without downtime and without re-entering credentials. Rotation takes two configuration changes and one admin action, because during a rolling restart old and new configuration run side by side: no process may encrypt with the new key before every other one can decrypt it.

PhaseConfigurationThen
1. IntroduceENCRYPTION_KEY=old, ENCRYPTION_KEYS_EXTRA=newRestart every instance. Nothing is encrypted with new yet.
2. PromoteENCRYPTION_KEY=new, ENCRYPTION_KEYS_EXTRA=oldRestart every instance. New and changed values are encrypted with new.
3. Re-encryptunchangedIn Settings → Encryption, start the re-encryption once every instance encrypts with new. It converts existing values in the background; everything keeps working.
4. RetireENCRYPTION_KEY=new, no oldOnly once phase 3 shows Completed, and no database backup taken before it will ever need restoring.

A single container restarts all its processes together, so it can go straight to phase 2. The phases matter for clusters and rolling updates on Kubernetes.

  • The re-encryption pauses itself when any live instance encrypts with another key, and says which, so an instance that rolled back can't rotate values back.
  • Values no configured key reads (from an earlier key change) are listed in Settings → Encryption, never counted as remaining, so the job can finish. Their owners see them as Unreadable in Credentials, and can enter the value again in place (workflows that use it keep pointing at it) or delete it.
  • After phase 3, the old key is refused as ENCRYPTION_KEY. An instance restarted with an old .env fails to start with an error that says so, instead of writing values nobody else can read.

What rotation protects. Rotation protects backups taken afterwards. A leaked key together with a copy of the database still exposes every credential in that copy, so after a leak, also rotate the credentials themselves at their providers.

The key check

Before the server starts, python -m app.key_check compares the keys with a check value stored in the database, and refuses to start with an error naming the cause. The image entrypoints run it after migrations. Worker definitions that start uvicorn directly must run it too: docker-compose.worker.yml, the worker-a service in docker-compose.cluster.yml and deploy/kubernetes/overlays/orbstack-cluster/worker.yaml do, as uv run python -m app.key_check && exec uv run python -m uvicorn …. If you copied one of them before this change, make the same edit. A worker left on the old command still runs without the check, and the cluster still keeps it from receiving work or leading if its keys don't fit.

Only the main instance writes the check value. On its first start after upgrading, it creates it once one of the 20 most recently stored values decrypts, or when there are none. A worker only reads it, and starts without checking until main has written it. If none of them decrypts, because the key was changed at some point and nothing has been re-entered since, main refuses to start. If ENCRYPTION_KEY is the key you want to keep, start main once with HEYM_ACCEPT_CURRENT_ENCRYPTION_KEY=true, then remove it. A worker ignores the flag, so a forgotten one can't make the cluster accept a wrong key.

If the check refuses after phase 4, the old key was most likely removed before phase 3 completed: put it back in ENCRYPTION_KEYS_EXTRA, and finish the re-encryption first. Values stored under the lost key stay unreadable and are listed as described above.

Database

VariableDescriptionDefault
DATABASE_URLFull async SQLAlchemy connection string. Overrides the POSTGRES_* values when set.auto-built from POSTGRES_*
POSTGRES_HOSTDatabase host (used when DATABASE_URL is empty).localhost
POSTGRES_PORTDatabase port.6543
POSTGRES_USERDatabase user.postgres
POSTGRES_PASSWORDDatabase password.postgres
POSTGRES_DBDatabase name.heym
AUTO_REWRITE_LOCAL_DATABASE_HOSTSingle-image runtime helper: rewrite a localhost DB host to the in-container address.true

Authentication & sessions

VariableDescriptionDefault
JWT_ALGORITHMJWT signing algorithm.HS256
JWT_ACCESS_TOKEN_EXPIRE_MINUTESAccess-token lifetime in minutes.1440
JWT_REFRESH_TOKEN_EXPIRE_DAYSRefresh-token lifetime in days.30
ALLOW_REGISTERAllow new user self-registration. Set false to lock down production, but only after your admin account exists: registration is refused for everyone when this is off and there is no first-user bootstrap, so an empty database plus false leaves no way to create an account.true
HEYM_ADMIN_EMAILSComma-separated email addresses of instance administrators, matched case-insensitively. Administrators manage instance settings (Settings → SSO, Settings → Instances, the Heym Work integration), can open, edit, and delete any user's workflows, see every owner's schedules and execution history, and pause or resume a workflow's automatic triggers. They also keep password sign-in when it is disabled for SSO, so a misconfigured provider cannot lock the instance. Empty grants nobody this access. Restart the backend after changing it. Plugin installation uses the separate HEYM_PLUGIN_ADMIN_EMAILS.—
HEYM_REQUIRE_MFATurn on two-factor sign-in with an authenticator app (TOTP). Every password sign-in then asks for a 6-digit code, and an account that has none sets one up by scanning a QR code before it reaches anything. Password sessions that began without a code end, so everyone enrols at their next sign-in. SSO sign-in is never affected: the identity provider owns the second factor. Off means no account is asked for a code; stored authenticators are kept and apply again when it is turned back on. Restart the backend after changing it.false
TRUST_PROXY_HEADERSTrust X-Forwarded-* headers (enable only behind a trusted proxy).false

Data retention

VariableDescriptionDefault
HEYM_DATA_RETENTION_DAYSDays of execution history and LLM traces to keep. Older rows are deleted once a day after 05:00 (in TIMEZONE) by the scheduler leader, in batches. A run still waiting on a human review or a Codex follow-up is kept whatever its age. 0 keeps everything. Takes effect on existing installs at the next daily pass after upgrading. In a cluster any instance can lead the scheduler, so set the same value on every instance.90

OAuth (MCP / API clients)

VariableDescriptionDefault
OAUTH_ACCESS_TOKEN_EXPIRE_SECONDSOAuth access-token lifetime.3600
OAUTH_REFRESH_TOKEN_EXPIRE_DAYSOAuth refresh-token lifetime.30
OAUTH_AUTH_CODE_EXPIRE_MINUTESOAuth authorization-code lifetime.10
OAUTH_ISSUERPublic OAuth issuer URL (e.g. https://api.example.com). Empty uses the request base URL.—

Networking & CORS

VariableDescriptionDefault
CORS_ORIGINSComma-separated allowed origins.http://localhost:4017
FRONTEND_URLPublic frontend URL used in generated links.http://localhost:4017
BACKEND_PORTBackend server port.10105
FRONTEND_PORTFrontend server port.4017
BACKEND_BIND_HOSTAddress the backend binds to (single-image runtime).127.0.0.1
BACKEND_PROXY_HOSTHost the frontend proxies API calls to (single-image runtime).127.0.0.1
TIMEZONEIANA timezone for scheduling/display. Empty falls back to TZ, then UTC.—
HEYM_HTTP_ALLOW_PRIVATE_URLSDisable the instance-wide SSRF egress guard for the HTTP, WebSocket Send, and WebSocket Trigger nodes, plus credential-derived destinations used by Slack, Discord, Jira, Sentry, GitHub, Grist, Supabase, ClickHouse, custom LLM execution and model discovery, the AI assistant, guardrails, and RAG embeddings. The Crawler node's FlareSolverr URL is deliberately held to a narrower address rule: loopback and private addresses stay reachable, because FlareSolverr is normally deployed beside Heym, and the refused set is 169.254.0.0/16, fe80::/10, the Alibaba Cloud and AWS IPv6 metadata addresses, and the IPv6 transition forms carrying one of them. That is a known-endpoint list rather than a guarantee about every provider, and it leaves other internal services on those addresses reachable through that field by design; the boundary there is who may author workflows plus your network rules. When false (default), target hosts must resolve only to public addresses; loopback, private, link-local, multicast, and cloud-metadata (169.254.169.254) targets are refused, and resolved IPs are pinned at dial time to defeat DNS rebinding. While the guard is on, guarded HTTP connections ignore HTTP_PROXY and HTTPS_PROXY so a proxy cannot route around the pin; setting this to true removes the pin and restores your proxy configuration. ClickHouse is the one exception to the pin: clickhouse-connect brings its own urllib3 transport, so its host is checked before the connection opens but is not pinned at dial time. Operator CA bundles configured through SSL_CERT_FILE or SSL_CERT_DIR remain supported. Guarded WebSocket connections also refuse redirects. URL scheme checks remain enabled when the address-policy opt-out is on. Set true only on trusted self-hosted instances that intentionally call internal services. Upgrade note for v0.0.105: existing integration credentials that point to loopback, private, or link-local addresses are now refused unless this setting is enabled. Upgrade note for v0.0.109: Slack and Discord credentials join that list, so a webhook URL pointing at an internal receiver now needs this setting. Crawler credentials are unaffected unless the FlareSolverr URL is link-local or a metadata address. This setting is also now forwarded by docker-compose.yml; before v0.0.109 Compose did not declare it, so setting it in the project .env had no effect. Keep false on hosted or multi-tenant deployments.false

Files & storage

VariableDescriptionDefault
FILE_STORAGE_DIRDirectory for Drive uploads and generated files. Relative values resolve against the backend's working directory, which is /app under Compose but /app/backend in the single-image release. Set it to an absolute /app/data/files whenever you bind-mount /app/data/files into ghcr.io/heymrun/heym, or the mount receives nothing../data/files
FILE_MAX_SIZE_MBMaximum single-file size in MB.99
REQUEST_BODY_MAX_SIZE_MBMaximum HTTP request body size; kept one MB above FILE_MAX_SIZE_MB for multipart overhead.100
DOCS_DIROverride path to docs content. Empty uses frontend/src/docs/content.—

MCP (Model Context Protocol)

VariableDescriptionDefault
MCP_PROTOCOL_MAX_CONCURRENCYMax concurrent MCP protocol operations.20
MCP_SSE_MAX_SESSIONSMax concurrent MCP SSE sessions.100
HEYM_MCP_ALLOW_PRIVATE_URLSDisable the SSRF egress guard for sse/streamable_http MCP servers. When false (default) MCP HTTP URLs must resolve to a public address; loopback, private, link-local, multicast, and cloud-metadata targets are refused, and the resolved IP is pinned at dial time to defeat DNS rebinding. Set true only on trusted self-hosted instances that intentionally connect to internal MCP servers. Keep false on hosted/multi-tenant deployments.false

While the guard is on, MCP HTTP/SSE tool fetches connect directly (they do not honor HTTP_PROXY/HTTPS_PROXY) so the pinned target IP is authoritative, matching the Drive download guard.

Sandbox backend

Every sandbox (Agent Python tools, skills, the Code node and its formatter, file analysis, custom Playwright, MCP stdio, Codex, OpenCode) runs through one sandbox backend: Docker sibling containers, or pods created through the Kubernetes API. Docker stays the default. The per-sandbox variables below (HEYM_PYTHON_TOOL_SANDBOX, HEYM_PLAYWRIGHT_SANDBOX, HEYM_MCP_STDIO_SANDBOX) still choose between a sandbox and subprocess; HEYM_SANDBOX_BACKEND chooses where sandboxes run. All three variables are optional: a Docker deployment needs none of them, and a backend pod with a ServiceAccount picks Kubernetes on its own.

VariableDescriptionDefault
HEYM_SANDBOX_BACKENDauto uses Docker when a daemon answers, else Kubernetes when the backend runs in a pod with a ServiceAccount, else fails closed. docker and kubernetes force one backend and never fall back to the other.auto
HEYM_SANDBOX_K8S_NAMESPACENamespace for sandbox pods. Sandboxes that need a workspace (skills, Code node dependencies, file analysis, Codex, OpenCode) need the workspace PersistentVolumeClaim in that namespace.the backend's namespace
HEYM_SANDBOX_K8S_POD_OVERRIDESJSON object with any of nodeSelector, tolerations, runtimeClassName (for example gVisor), imagePullSecrets, priorityClassName. Any other key disables the Kubernetes backend, so an override can never loosen a pod's security context.none

On Kubernetes every sandbox is one pod under Pod Security restricted: non-root, all capabilities dropped, no privilege escalation, RuntimeDefault seccomp, and no ServiceAccount token. It needs Kubernetes 1.30+ and a CNI that enforces NetworkPolicy: before the first sandbox that must be offline, Heym starts a test pod and refuses offline sandboxes if that pod can still connect. A sandbox's workspace is mounted as a subPath of the PersistentVolumeClaim behind the backend's own workspace directory, never the whole claim. With a ReadWriteOnce claim, sandboxes run on the backend's node. HEYM_MCP_STDIO_FILES_VOLUME names a PersistentVolumeClaim there and HEYM_MCP_STDIO_FILES_SUBPATH its subPath; HEYM_MCP_STDIO_FILES_HOST_DIR is refused. Requirements, RBAC and example manifests: deploy/kubernetes.

Custom CA bundles need no extra setting. When SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, NODE_EXTRA_CA_CERTS or CODEX_CA_CERTIFICATE names a file, every sandbox that receives the variable also gets that file, read-only at the same path. A backend running on the host binds the file itself. A backend in a container or pod copies it to _ca/ under HEYM_CODEX_WORKSPACE_DIR and mounts only that copy, so the workspace volume must be present; without it the sandbox runs without the file and the backend logs a warning.

Playwright custom code sandbox

The Playwright node's Run Code mode (playwrightCode) is off by default. When enabled it runs in a hardened throwaway sibling container (Compose / GHCR) or, for native ./run.sh, falls back to subprocess. Step-based Playwright nodes are unaffected.

VariableDescriptionDefault
HEYM_PLAYWRIGHT_CUSTOM_CODE_ENABLEDAllow executing custom playwrightCode / Run Code mode.false
HEYM_PLAYWRIGHT_SANDBOXauto/docker (sibling container, fail-closed) or subprocess (in-process; trusted/local only). ./run.sh sets subprocess when no image is configured.auto
HEYM_PLAYWRIGHT_SANDBOX_IMAGESibling runner image. Compose defaults to HEYM_BACKEND_IMAGE; GHCR release image defaults to ghcr.io/heymrun/heym:<version>. Empty falls back to HEYM_CODEX_DOCKER_IMAGE, then container inspect.—
HEYM_PLAYWRIGHT_SANDBOX_PYTHONInterpreter inside the sibling image. Empty auto-detects /app/backend/.venv/bin/python (GHCR) or /app/.venv/bin/python (Compose).auto

Agent Python tool sandbox

VariableDescriptionDefault
HEYM_PYTHON_TOOL_SANDBOXHow user-defined Agent Python tools and skills run: auto (sandbox on the sandbox backend, fail-closed), docker (sandbox required, never falls back), or subprocess (no security boundary; trusted/local only). Governs both paths. MCP stdio isolation is not affected by this and has its own HEYM_MCP_STDIO_SANDBOX.auto
HEYM_PYTHON_TOOL_IMAGEImage used for the Python tool Docker sandbox (also the skill sandbox fallback when HEYM_SKILL_IMAGE is empty). Empty auto-detects the running backend image.—

Agent skill sandbox

Untrusted Agent Python skills run in a hardened, throwaway sibling container (non-root, cap-drop ALL, no-new-privileges, read-only root fs, resource limits, no Docker socket) selected by HEYM_PYTHON_TOOL_SANDBOX above. Unlike Python tools, skills keep network egress and a per-run writable workspace, shared with the sibling through the same volume the Codex runner uses. Requires Docker Engine 25.0+ (per-run volume-subpath mounts); otherwise auto fails closed.

VariableDescriptionDefault
HEYM_SKILL_IMAGEImage used for the skill Docker sandbox. Empty falls back to HEYM_PYTHON_TOOL_IMAGE, then auto-detects the running backend image.—
HEYM_SKILL_NETWORKDocker network mode for skill containers. Egress is intentionally allowed (skills call APIs / install deps via uv).bridge
HEYM_SKILL_MEMORYMemory limit (and swap cap) for skill containers.512m
HEYM_SKILL_CPUSCPU limit for skill containers.1
HEYM_SKILL_PIDSPID limit for skill containers.256
HEYM_SKILL_USERNon-root uid:gid the skill runs as inside the container.65534:65534
HEYM_SKILL_WORKSPACE_MOUNTMount point of the shared workspace volume inside the backend. Falls back to HEYM_CODEX_WORKSPACE_DIR./app/data/codex-workspaces
HEYM_SKILL_WORKSPACE_DIRDirectory (under the mount) where per-run skill workspaces are created.<mount>/_skill-workspaces
HEYM_SKILL_DOCKER_WORKSPACE_VOLUMEDocker volume shared with each skill runner. Falls back to HEYM_CODEX_DOCKER_WORKSPACE_VOLUME (heym-codex-workspaces in Compose), else the backend's own mount is inspected.—
HEYM_SKILL_HOST_WORKSPACE_DIRAbsolute host path for the workspace mount when using a bind mount instead of a Docker volume.—

Code node sandbox

The Code node adds no variables of its own, and deliberately has no subprocess escape hatch. It runs arbitrary user Python with arbitrary dependencies, so it always requires a reachable sandbox backend (Docker or Kubernetes) and fails closed without one — HEYM_PYTHON_TOOL_SANDBOX does not apply to it. Its limits (512m memory, 1 CPU, 256 PIDs, 120s install timeout, 60s execution timeout) are constants in app/services/code_python_executor.py, not configuration.

It reuses existing variables only: HEYM_PYTHON_TOOL_IMAGE then HEYM_CODEX_DOCKER_IMAGE for image resolution (falling back to inspecting the backend's own image), and HEYM_CODEX_WORKSPACE_DIR / HEYM_CODEX_DOCKER_WORKSPACE_VOLUME for the per-run dependency workspace when the backend is containerised. A native run.sh backend needs neither variable: it bind-mounts a local temporary directory instead. Code nodes with an empty requirements.txt mount nothing at all, since the install phase is skipped.

MCP stdio sandbox

The MCP stdio transport starts a server process from a command supplied in the node configuration (Agent MCP connections, the MCP Call node, and the editor's "Fetch tools" preview). That process runs in a hardened, throwaway sibling container rather than on the backend host: non-root, cap-drop ALL, no-new-privileges, read-only root fs, resource limits, and no Docker socket. Network egress is allowed, because npx / uvx must fetch their package and MCP servers exist to call APIs.

HEYM_MCP_STDIO_SANDBOX is deliberately separate from HEYM_PYTHON_TOOL_SANDBOX: an operator who selects subprocess there for Python tool compatibility (as run.sh does automatically for native dev) must not silently lose MCP stdio isolation as a side effect. Unknown values fall back to auto, never to host execution.

A docker run [flags] IMAGE [args] command keeps working: Heym starts IMAGE itself with the hardening applied, carrying over -e, -w and --entrypoint. Flags that would dissolve the boundary (--privileged, --cap-add, --device, --security-opt, --network host, --pid/--ipc/--uts host, --env-file, --volumes-from) are refused with an explanatory error.

Caller-supplied mounts (-v, --volume, --mount, --volumes-from) are refused outright. Validating a mount source is not a winnable game: //var/run/docker.sock survives os.path.normpath because POSIX preserves exactly two leading slashes, and relative paths, symlinks and parent directories each defeat a denylist differently.

No application storage is mounted by default either. The sandbox sees no Heym files unless you name a volume or directory explicitly, and that mount is read-only unless you also opt into writes. The skill/Codex/OpenCode workspace volumes are never auto-mounted: they hold every tenant's workspaces, so exposing one to a single caller's MCP process would be a cross-tenant read. If a file-oriented MCP server (for example @modelcontextprotocol/server-filesystem) needs data, point HEYM_MCP_STDIO_FILES_VOLUME at a volume you scope yourself, optionally narrowing it with HEYM_MCP_STDIO_FILES_SUBPATH.

Inside the container the root filesystem is read-only and /tmp is a throwaway tmpfs, so the sandbox sets HOME and the npm/uv cache directories to paths under /tmp and mounts that tmpfs with exec. Without those, npx and uvx servers cannot run at all: the sandbox user's passwd home is /nonexistent, and Docker applies noexec to every --tmpfs that does not name exec explicitly. Values you set on the connection are applied after these defaults, so you can override them.

The working directory is that same tmpfs, not the runner image's WORKDIR: /app in the backend image is the Heym backend project itself, so a uv run … server would try to sync it and fail on the read-only root filesystem. A docker run IMAGE command keeps its own image's WORKDIR unless it passes -w.

These variables are wired through docker-compose.yml, so setting them in .env works for ./deploy.sh.

Only the env vars set on the connection reach the server. The backend's own environment (SECRET_KEY, ENCRYPTION_KEY, DATABASE_URL, provider API keys) is never forwarded; the MCP SDK supplies a safe default PATH/HOME/SHELL/TERM/USER/LOGNAME set on top.

Operator-only escape hatches. HEYM_MCP_STDIO_SANDBOX=subprocess and HEYM_MCP_STDIO_FILES_HOST_DIR are never settable by a workflow author, only by whoever runs the deployment. Both can remove the isolation this section describes, so treat them as trusted single-user / single-tenant options and leave them unset on anything multi-user.

VariableDescriptionDefault
HEYM_MCP_STDIO_SANDBOXIsolation for MCP stdio servers: auto (sandbox backend required, fail-closed), docker (sandbox required, never falls back), or subprocess. Independent of HEYM_PYTHON_TOOL_SANDBOX. ⚠️ subprocess removes the boundary entirely and runs the caller's command on the backend host, which is the GHSA-378x-q589-34mv condition: trusted single-user instances only, never on a shared or hosted deployment.auto
HEYM_MCP_STDIO_IMAGEImage used to run non-docker stdio commands (npx, uvx, node, python, …). Compose defaults it to HEYM_BACKEND_IMAGE, the GHCR image to its own release tag; set it only for native run.sh (heym-backend:local) or a custom runner. Falls back to HEYM_PYTHON_TOOL_IMAGE, then HEYM_CODEX_DOCKER_IMAGE, then container inspect, then fails closed. The docker run <image> form names its own image.—
HEYM_MCP_STDIO_FILES_PATHMount point inside the sandbox for the optional file mount below./mnt/heym-files
HEYM_MCP_STDIO_FILES_VOLUMEDocker volume to expose to MCP servers. Nothing is mounted unless you set this (or the host-dir variant); there is no fallback to application volumes.—
HEYM_MCP_STDIO_FILES_SUBPATHMount only this subpath of the volume, so a shared volume can be scoped per tenant or per purpose.—
HEYM_MCP_STDIO_FILES_HOST_DIRAbsolute host path to expose instead of a volume. No fallback to FILE_STORAGE_DIR. ⚠️ This bind-mounts a backend host path into the sandbox, so a wide path (or one holding several tenants' data) weakens the boundary: scope it deliberately, and prefer a volume with HEYM_MCP_STDIO_FILES_SUBPATH on shared deployments.—
HEYM_MCP_STDIO_FILES_WRITABLEMount the file mount read-write. Read-only otherwise.false
HEYM_MCP_STDIO_TMPFS_SIZESize of the writable /tmp tmpfs, which holds the npm/uv caches and anything the server installs at startup. Raise it for large packages.512m
HEYM_MCP_STDIO_MEMORYMemory limit for MCP stdio containers.512m
HEYM_MCP_STDIO_CPUSCPU limit for MCP stdio containers.2
HEYM_MCP_STDIO_PIDSPID limit for MCP stdio containers.256
HEYM_MCP_STDIO_USERNon-root user for the MCP server, as a canonical uid:gid of two positive numbers. Anything else falls back to the default: a bare uid (Docker would put it in group root), a zero uid or gid in any spelling (0, 00, +0, :0, 1000:0), a user name, or a value with whitespace or extra fields. The value is re-rendered from the parsed numbers, so what is validated is what Docker receives.65534:65534

Docker log access

VariableDescriptionDefault
DOCKER_LOGS_ENABLEDExpose container log access in the UI. Grants broad host visibility.false
DOCKER_LOGS_ALLOWED_EMAILSComma-separated emails allowed to read Docker logs. Create the admin account before enabling.—

Plugins (custom nodes)

VariableDescriptionDefault
HEYM_PLUGINS_ENABLEDEnable installing custom nodes from zip packages. Install/uninstall runs server-side code, so it is operator-restricted.false
HEYM_PLUGIN_ADMIN_EMAILSComma-separated operator emails allowed to install/uninstall plugins.—
HEYM_PLUGINS_DIRDirectory where installed plugins are stored. Same relative-path caveat as FILE_STORAGE_DIR: use an absolute /app/data/plugins when bind-mounting it into the single-image release.data/plugins

Codex node

The Codex node runs the OpenAI Codex CLI in an isolated workspace. It needs the codex CLI and git on PATH. Local ./run.sh uses the native codex CLI. Docker deployments use the bundled heym-codex-docker wrapper, which starts a sibling container from the same Heym image so Codex's bubblewrap sandbox can create Linux namespaces outside the backend container. That container also passes --security-opt systempaths=unconfined (Docker Engine 25+). Docker's default masked /proc paths otherwise reject bubblewrap's fresh proc mount, and every Codex command fails with bwrap: Can't mount proc on /newroot/proc: Operation not permitted. Each run mounts only its own workspace and its <workspace>.codex-home directory, never the rest of the workspace volume (Docker Engine 25+ for a named volume). On Kubernetes the run is a restricted pod, where bubblewrap cannot run, so Codex uses --sandbox danger-full-access and the pod is the boundary.

VariableDescriptionDefault
HEYM_CODEX_CLI_COMMANDPath/name of the Codex CLI binary.codex
HEYM_CODEX_WORKSPACE_DIRDirectory for cloned repo workspaces (its <workspace>.codex-home sibling holds the auth bundle, outside the repo)../data/codex-workspaces
HEYM_CODEX_NETWORK_ACCESSAllow outbound network access for commands inside Codex's workspace-write sandbox. Docker deploys set this to true so Codex can download files/dependencies while still writing only inside the workspace.false
HEYM_CODEX_DOCKER_IMAGEImage used by heym-codex-docker. Compose defaults to the locally built backend image (heym-backend:local). The release image defaults to the same single GHCR image (ghcr.io/heymrun/heym:<version>).auto
HEYM_CODEX_DOCKER_WORKSPACE_VOLUMEDocker volume holding HEYM_CODEX_WORKSPACE_DIR; each Codex runner mounts only its run's two directories from it. Docker Compose uses heym-codex-workspaces.—
HEYM_CODEX_HOST_WORKSPACE_DIRAbsolute host path for HEYM_CODEX_WORKSPACE_DIR when using bind mounts instead of a Docker volume.—
HEYM_CODEX_DOCKER_NETWORKDocker network mode for Codex runner containers.bridge
HEYM_CODEX_DOCKER_CPUSCPU limit passed to Codex runner containers.2
HEYM_CODEX_DOCKER_MEMORYMemory limit passed to Codex runner containers.4g
HEYM_CODEX_DOCKER_PIDSPID limit passed to Codex runner containers.1024
HEYM_CODEX_GIT_AUTHOR_NAMEAuthor name for commits Codex creates.Heym Codex
HEYM_CODEX_GIT_AUTHOR_EMAILAuthor email for Codex commits. The GitHub avatar shown next to it is derived from this email (matching GitHub account, else Gravatar).support@heym.run
HEYM_CODEX_OAUTH_CLIENT_IDOpenAI OAuth client id for "Sign in with ChatGPT". Defaults to the public Codex CLI client.app_EMoamEEZ73f0CkXaXp7hrann
HEYM_CODEX_OAUTH_ISSUEROpenAI OAuth issuer base URL.https://auth.openai.com
HEYM_CODEX_OAUTH_REDIRECT_URIOAuth redirect URI (fixed by OpenAI's Codex client; used for the paste-back flow).http://localhost:1455/auth/callback

HEYM_CODEX_CLI_VERSION is a Docker build arg (not a runtime env var) that pins the @openai/codex npm version installed into the image. Default latest.

OpenCode Go node

The OpenCode Go node runs the OpenCode CLI (Go) in an isolated workspace against a GitHub repository. It needs the opencode CLI and git on PATH. Local ./run.sh uses the native opencode CLI (host subprocess). Docker deployments use the bundled heym-opencode-docker wrapper, which starts a hardened sibling container from the same Heym image (all capabilities dropped, no-new-privileges, read-only root, resource limits) sharing the workspace named volume. All git/GitHub operations run host-side, so the GitHub token is never placed inside the OpenCode process/container. Each run mounts only its own workspace and its <workspace>.oc-home directory, never the rest of the workspace volume. With a named volume this needs Docker Engine 25+ (volume-subpath).

VariableDescriptionDefault
HEYM_OPENCODE_CLI_COMMANDPath/name of the OpenCode CLI binary (or the heym-opencode-docker wrapper in Docker deployments).opencode
HEYM_OPENCODE_WORKSPACE_DIRDirectory for cloned repo workspaces (its <workspace>.oc-home sibling holds the OpenCode config/auth for the run)../data/opencode-workspaces
HEYM_OPENCODE_GIT_AUTHOR_NAMEAuthor name for commits OpenCode creates.Heym OpenCode
HEYM_OPENCODE_GIT_AUTHOR_EMAILAuthor email for OpenCode commits. The GitHub avatar shown next to it is derived from this email (matching GitHub account, else Gravatar).support@heym.run
HEYM_OPENCODE_DOCKER_IMAGEImage used by heym-opencode-docker. Compose defaults to the locally built backend image (heym-backend:local). The release image defaults to the same single GHCR image (ghcr.io/heymrun/heym:<version>).auto
HEYM_OPENCODE_DOCKER_WORKSPACE_VOLUMEDocker volume holding HEYM_OPENCODE_WORKSPACE_DIR; each OpenCode runner mounts only its run's two directories from it. Docker Compose uses heym-opencode-workspaces.—
HEYM_OPENCODE_HOST_WORKSPACE_DIRAbsolute host path for HEYM_OPENCODE_WORKSPACE_DIR when using bind mounts instead of a Docker volume.—
HEYM_OPENCODE_DOCKER_NETWORKDocker network mode for OpenCode runner containers (egress is required to reach the model gateway).bridge
HEYM_OPENCODE_DOCKER_CPUSCPU limit passed to OpenCode runner containers.2
HEYM_OPENCODE_DOCKER_MEMORYMemory limit passed to OpenCode runner containers.4g
HEYM_OPENCODE_DOCKER_PIDSPID limit passed to OpenCode runner containers.1024
HEYM_OPENCODE_DOCKER_ENTRYPOINTEntrypoint binary run inside the OpenCode runner container.opencode
HEYM_OPENCODE_DOCKER_EXTRA_ARGSExtra docker run arguments appended to the OpenCode runner invocation (shell-split).—

HEYM_OPENCODE_CLI_VERSION is a Docker build arg (not a runtime env var) that pins the opencode CLI GitHub release installed into the image. Default latest.

OpenTelemetry tracing

VariableDescriptionDefault
HEYM_OTEL_ENABLEDEmit a root span per workflow run and a child span per node over OTLP/HTTP.false
HEYM_OTEL_EXPORTER_OTLP_ENDPOINTOTLP/HTTP base endpoint, e.g. http://collector:4318 (spans posted to /v1/traces).—
HEYM_OTEL_EXPORTER_OTLP_HEADERSComma-separated key=value exporter headers for auth.—
HEYM_OTEL_SERVICE_NAMEservice.name resource attribute.heym
HEYM_OTEL_TRACES_SAMPLER_RATIOParent-based head sampling ratio (0.0–1.0).1.0
HEYM_OTEL_CAPTURE_NODE_IOAttach truncated node input/output to node spans (may contain user data).false

Load distribution (multi-instance)

Point a second Heym instance at the same database and it joins as a worker. Background runs are shared between the instances by a percentage set under Settings → Instances. Postgres is the only channel between them: a worker needs no open port and no route back to the main instance.

Two rules the cluster cannot enforce for you:

  • SECRET_KEY and ENCRYPTION_KEY must be identical on every instance, except while rotating ENCRYPTION_KEY. A worker with a different ENCRYPTION_KEY cannot decrypt credentials. Heym compares digests of the keys on each heartbeat and marks an instance incompatible when it can't read what main writes, or what main can't read from it, or what another live instance writes. An incompatible instance receives no work and can't become the leader that runs cron and the pollers.
  • Point ingress at the main instance only. Round-robining user traffic across instances would send a file upload to one machine and its download to another.

Worker instances also have a different outbound IP than main. Any API that allowlists source addresses sees the new one. sendEmail runs on main for exactly this reason; for the HTTP node, give the cluster one NAT egress address or allowlist every instance.

VariableDescriptionDefault
HEYM_CLUSTER_ENABLEDTurn load distribution on. Off means every run executes in-process exactly as before.false
HEYM_INSTANCE_ROLEmain or worker. Main owns file storage, plugins and ingress.main
HEYM_INSTANCE_NAMEDisplay label for this instance; the admin UI can rename it later.—
HEYM_INSTANCE_IDStable id shared by all of this instance's processes. Derived from the name when empty.—

Credentials from Kubernetes Secrets

VariableDescriptionDefault
HEYM_K8S_CREDENTIALSRead credentials from Secrets labelled heym.run/credential: "true". Needs get, list and watch on secrets, and refuses to start while any sandbox switch is subprocess. See deploy/kubernetes.false
HEYM_K8S_CREDENTIALS_NAMESPACENamespace to watch.Heym's own namespace

Miscellaneous

VariableDescriptionDefault
HEYM_LLM_PRICING_SYNC_ENABLEDPeriodically sync model pricing data.true
APP_VERSIONOverride the reported app version. Empty reads the VERSION file.—
HEYM_BACKEND_IMAGECompose only (./deploy.sh): tag the backend is built and run as. Every sibling sandbox image (MCP stdio, Playwright, Codex, OpenCode) defaults to it.heym-backend:local

Frontend build-time

These are read at build time by Vite and baked into the frontend bundle.

VariableDescriptionDefault
VITE_API_URLBase URL the frontend calls for the API.same-origin
VITE_APP_VERSIONVersion string shown in the UI.from build
VITE_HEYM_WEB_URLMarketing/site URL used for external links.—