Running & Deployment

Heym provides two scripts for different environments: run.sh for local development and deploy.sh for production deployments using Docker Compose.

Heym provides two scripts for different environments: run.sh for local development and deploy.sh for production deployments using Docker Compose.

Video walkthrough: Set Up Heym Locally in Under 2 Minutes — clone the repository, start PostgreSQL, fill in the required environment variables, and create your first account on a local instance.

Prerequisites

Both scripts require the following tools to be installed:

ToolPurpose
DockerDatabase container (dev) and full stack (prod)
uvPython package manager for the backend
bunJavaScript runtime for the frontend

Environment Setup

Both run.sh and deploy.sh read from a .env file in the project root. If it does not exist, the script automatically creates one from .env.example — for both local development and production.

SECRET_KEY and ENCRYPTION_KEY ship empty in .env.example. When either is empty, run.sh and deploy.sh generate a cryptographically strong value automatically and write it back to .env, so a fresh setup needs no manual key handling. If an existing .env still contains the legacy ENCRYPTION_KEY placeholder (change_this_to_a_random_32_byte_hex_value), the scripts stop with an explicit error instead of overwriting it — rotating that key would make previously-encrypted credentials unreadable. The backend itself also refuses to start if either key is empty or left at a known placeholder.

To create the file manually (optional — the scripts do this for you):

cp .env.example .env

Key environment variables:

For every supported variable, default, and production note, see Environment Variables.

VariableDescription
SECRET_KEYRequired. JWT signing secret. Auto-generated by run.sh/deploy.sh when empty.
ENCRYPTION_KEYRequired. Credential encryption key. Auto-generated by run.sh/deploy.sh when empty.
DATABASE_URLOptional database connection string override. If empty, Heym builds it from POSTGRES_* settings.
BACKEND_PORTBackend API port — defaults to 10105
FRONTEND_PORTFrontend port — defaults to 4017
FRONTEND_URLRequired in production. Public URL of the app (scheme + host, e.g. https://heym.example.com). Used for OAuth redirect URIs (Google Sheets, BigQuery, Notion, and similar), and for the review, Codex follow-up and file links that background runs mint; must match the URL users use in the browser. In a cluster, set the same value on every instance.
ALLOW_REGISTEROpen user registration (false in prod, true in dev). Flip it to false only after your admin account exists — there is no first-user bootstrap, so an empty database plus disabled registration leaves no way to create one.
DOCKER_LOGS_ENABLEDEnables Docker-backed Logs tab access when set to true; also requires Docker socket access
DOCKER_LOGS_ALLOWED_EMAILSComma-separated list of trusted user emails allowed to access Docker logs when DOCKER_LOGS_ENABLED=true
REQUEST_BODY_MAX_SIZE_MBMaximum HTTP request body size accepted before endpoint handlers run; defaults to 100, one MB above FILE_MAX_SIZE_MB to leave room for multipart overhead
HEYM_PYTHON_TOOL_SANDBOXHow user-defined Python tools run: auto (default — hardened, isolated Docker container; fail closed if Docker is unavailable), docker (same, never falls back), or subprocess (in-process local fallback; not a security boundary, trusted/dev only). run.sh sets subprocess for native dev. See Security.
HEYM_PYTHON_TOOL_IMAGEImage used for the Python tool Docker sandbox. Empty = auto-detect the running backend image.

Database connection defaults (POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB) are documented in Environment Variables and .env.example.


Development: run.sh

run.sh starts all three services locally — the database (as a Docker container), the FastAPI backend, and the Vite dev server — with a single command.

./run.sh

What it does, step by step:

  1. Checks that docker, bun, and uv are available
  2. Creates .env from .env.example if missing, and generates a random SECRET_KEY and ENCRYPTION_KEY when either is empty
  3. Starts (or creates) a database Docker container on port 6543, storing its data in the heym-postgres-data Docker volume rather than a host directory
  4. Installs Python dependencies via uv sync
  5. Runs Alembic database migrations (alembic upgrade head)
  6. Frees the backend and frontend ports if occupied
  7. Starts the FastAPI backend with --reload (hot-reload on code changes)
  8. Installs frontend dependencies via bun install
  9. Starts the Vite dev server

Options:

FlagDescription
(none)Start with debug logging enabled (LOG_LEVEL=DEBUG)
--no-debugStart with default log level (no debug output)
--helpShow usage information

Service addresses (dev):

ServiceAddress
Frontendlocalhost on FRONTEND_PORT (default: 4017)
Backend APIlocalhost on BACKEND_PORT (default: 10105)
Interactive API Docslocalhost:10105/docs
Databaselocalhost:6543

Press Ctrl+C to gracefully stop all services.


End-to-End Tests: run_e2e.sh

The frontend Playwright suite runs against the real Vue application, FastAPI backend, and an isolated PostgreSQL database:

./run_e2e.sh

The suite is a key-path smoke and regression suite, not exhaustive product coverage. It covers authentication, core workflow lifecycle and execution, selected dashboard resources, public routes, and a mocked HITL review UI. Provider integrations, every node type, sharing/permission matrices, and full HITL resume execution remain covered primarily by backend tests or require dedicated integration environments.

Each run gets its own temporary PostgreSQL 16 container, random available database/backend/frontend ports, authentication state, test results, and HTML report. This allows multiple run_e2e.sh processes to run concurrently without sharing state or overwriting artifacts. The script removes its database container when the run finishes and prints the artifact directory and report path.

Useful commands:

./run_e2e.sh --ui         # Interactive Playwright runner with an isolated database
cd frontend
bun run test:e2e:report   # Open the most recently completed local run

Direct bun run test:e2e and bun run test:e2e:ui runs require an explicit DATABASE_URL so they cannot silently start against the local development database. Use ./run_e2e.sh for normal local runs.

./check.sh runs lint, typecheck, formatting, and backend tests without the E2E suite, keeping the default local check path fast. Run Playwright E2E tests separately with ./run_e2e.sh. Pull requests always run the Chromium E2E suite in GitHub Actions and retain traces, screenshots, videos, and the HTML report when failures occur.


Production: deploy.sh

deploy.sh builds and runs the full stack using Docker Compose. All three services — the database, the backend, and the frontend — run as containers. The backend entrypoint automatically runs migrations before starting the server with 8 workers.

Initial deploy (build + start):

./deploy.sh

This performs a zero-downtime deploy: images are built first while the existing containers keep running, then the new version is swapped in.

Available commands:

CommandDescription
./deploy.shBuild images and start/update all services
./deploy.sh --statusShow container status
./deploy.sh --logsStream logs from all containers
./deploy.sh --restartRestart all containers
./deploy.sh --downStop and remove all containers
./deploy.sh --migrate-pgdataOne-time migration: copy an existing data/postgres directory into the heym-postgres-data Docker volume and rebuild its indexes
./deploy.sh --helpShow usage information

Database storage: a Docker named volume, not a host directory.

Compose stores PostgreSQL in the heym-postgres-data Docker volume. Earlier versions bind-mounted the host directory ./data/postgres instead. Host bind mounts on macOS (virtiofs) and Windows/WSL2 do not give PostgreSQL the fsync and file-close guarantees it requires: clusters there hit PANIC: could not close file … Input/output error on WAL segments and corrupt over time. Linux bind mounts were never affected, but every platform now uses the same layout.

Upgrading a deployment that still has data in ./data/postgres is a one-time step. ./deploy.sh detects it, stops before building, and asks you to migrate first:

./deploy.sh --migrate-pgdata   # stop services, copy into the volume, rebuild indexes
./deploy.sh                    # deploy as usual

The migration also rebuilds indexes. The copy is byte-for-byte, so index damage the old bind mount caused travels with it. --migrate-pgdata therefore starts the postgres service alone on the new volume, runs reindexdb against it, and stops it again before the rest of the stack comes up. This matters because this class of corruption leaves index entries pointing past the end of the heap — a state pg_amcheck reports as clean, so a passing check is not evidence of a healthy table. REINDEX repairs those entries; it cannot restore heap pages the old filesystem already lost. On a large, known-healthy database you can bypass the rebuild with ./deploy.sh --migrate-pgdata --skip-reindex.

The copy is non-destructive. ./data/postgres stays in place as a backup, and the migration refuses to overwrite a volume that already holds a database. If any step fails, the new volume is removed, so a half-migrated cluster can never be deployed by mistake. Delete the old directory once you have confirmed your workflows are intact. The postgres container carries the same guard, so a manual docker compose up cannot silently initialise an empty database over your data either. Fresh installs need no action.

Service addresses (production):

The frontend container is exposed on FRONTEND_PORT (default: 4017). The backend API is served under the /api path, proxied through the frontend container — so there is only one public-facing port in production.

Container overview:

ContainerDescription
heym-dbRelational database
heym-backendFastAPI API server (8 workers, built from backend/Dockerfile)
heym-frontendFrontend preview container serving the built Vue app (built from frontend/Dockerfile)

Version update badge:

The app header shows the running Docker build version. When that version is behind the latest Heym GitHub release, a purple Update badge appears next to the version. Click the version or badge to open the Heym GitHub releases page.


Prebuilt Image: docker run

If you prefer not to build the app locally, you can pull the published container image and run it directly.

The image starts the frontend and backend together in one container. PostgreSQL is still external, but you can provide either DATABASE_URL or the POSTGRES_* variables from .env.example.

Store your database in a Docker named volume. The release image ships no database, so the PostgreSQL behind DATABASE_URL is yours to run. Mount it as -v heym-postgres-data:/var/lib/postgresql/data rather than a host path: on macOS (virtiofs) and Windows/WSL2 a bind-mounted PostgreSQL data directory does not honour the fsync guarantees the database requires and corrupts the cluster over time.

Vector store backend. run.sh and deploy.sh run the official postgres:16 image and auto-install the postgresql-16-pgvector package at startup, so the Postgres (pgvector) RAG backend works out of the box there with no change to your data. The prebuilt single-container image, however, connects to a PostgreSQL you provide — that database must support the vector extension to use the Postgres backend. Heym cannot install pgvector into a database it does not manage. Without it, the startup migration skips the pgvector table gracefully — the deploy still succeeds, Qdrant RAG keeps working, and creating or uploading to a Postgres vector store returns a clear "backend unavailable" message until pgvector is enabled.

Set the keys yourself for direct image runs. Unlike run.sh/deploy.sh, the prebuilt image does not auto-generate keys. After cp .env.example .env, populate the two empty keys (replacing in place avoids duplicate entries):

SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))")
ENCRYPTION_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
sed -i.bak "s|^SECRET_KEY=.*|SECRET_KEY=${SECRET_KEY}|; s|^ENCRYPTION_KEY=.*|ENCRYPTION_KEY=${ENCRYPTION_KEY}|" .env && rm -f .env.bak
docker pull ghcr.io/heymrun/heym:latest
 
docker run --rm \
  --env-file .env \
  -p 4017:4017 \
  --shm-size 2g \
  -e FILE_STORAGE_DIR=/app/data/files \
  -e HEYM_PLUGINS_DIR=/app/data/plugins \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v "$(pwd)/data/files:/app/data/files" \
  -v "$(pwd)/data/plugins:/app/data/plugins" \
  -v heym-codex-workspaces:/app/data/codex-workspaces \
  -v heym-opencode-workspaces:/app/data/opencode-workspaces \
  ghcr.io/heymrun/heym:latest

Make the file and plugin mounts absolute. FILE_STORAGE_DIR and HEYM_PLUGINS_DIR default to the relative paths ./data/files and data/plugins, and the release image starts the backend from /app/backend. Left relative, they resolve to /app/backend/data/files and /app/backend/data/plugins, so the two bind mounts above receive nothing and uploads are lost when the container is replaced. The -e flags override the relative values .env.example supplies through --env-file, because command-line -e wins over --env-file. Compose (./deploy.sh) does not need this: that image runs the backend from /app, where the relative defaults already land on the mounts.

Docker socket access. Mounting /var/run/docker.sock gives the backend broad control over the host Docker daemon. The default Docker Compose service and the direct docker run example include it for Docker-based MCP stdio tools that run docker commands. The Logs tab still requires DOCKER_LOGS_ENABLED=true and DOCKER_LOGS_ALLOWED_EMAILS with a comma-separated list of trusted user emails. Create the trusted admin account before enabling Docker logs, or keep ALLOW_REGISTER=false, so an unverified self-registration cannot claim an allow-listed email. User-defined Python tools do not get this socket: they run in a separate hardened container with no Docker socket. See Security.

Codex runner. The Codex node uses the same ghcr.io/heymrun/heym image as a sibling runner container (--entrypoint codex) so Codex's Linux sandbox can create namespaces. Keep the heym-codex-workspaces volume mount if you want Codex workflows in the direct image setup; the runner does not receive the Docker socket or backend secrets.

OpenCode Go runner. The OpenCode Go node uses the same image as a hardened sibling runner (--entrypoint opencode) sharing heym-opencode-workspaces. Keep that volume mount if you want OpenCode workflows in the direct image setup; the runner does not receive the Docker socket or the GitHub token.

Skill sandbox. Python skills on the Agent node run in a hardened sibling container that shares the heym-codex-workspaces volume, so keep that volume mount for skills too — not just Codex. Each run gets an isolated per-run subpath, and the sibling receives neither the Docker socket nor backend secrets. This needs Docker Engine 25.0+; on older engines, or without the volume, HEYM_PYTHON_TOOL_SANDBOX=auto fails closed — set HEYM_PYTHON_TOOL_SANDBOX=subprocess only for trusted single-user setups. See Security.

--shm-size 2g is required for Playwright. Docker gives a container 64 MB of /dev/shm and Chromium crashes its renderer under that. Step-based Playwright nodes run Chromium as a subprocess inside this container, so the limit is this container's; custom playwrightCode runs in a sibling container that sets its own. docker-compose.yml already declares shm_size: "2gb", so run.sh and deploy.sh are unaffected — only a plain docker run needs the flag.

Playwright Run Code. Custom Playwright Python needs HEYM_PLAYWRIGHT_CUSTOM_CODE_ENABLED=true and the Docker socket mount above. The release image sets HEYM_PLAYWRIGHT_SANDBOX_IMAGE / HEYM_PLAYWRIGHT_SANDBOX_PYTHON for the GHCR layout (/app/backend/.venv). Compose ./deploy.sh defaults the sandbox image to heym-backend:local. Keep --no-sandbox in Chromium launch args inside sandbox containers.

Minimum environment variables for direct image runs:

VariableRequiredPurpose
DATABASE_URLOptionalFull PostgreSQL connection string override
POSTGRES_HOSTYes, if DATABASE_URL is emptyPostgreSQL host
POSTGRES_PORTYes, if DATABASE_URL is emptyPostgreSQL port
POSTGRES_USERYes, if DATABASE_URL is emptyPostgreSQL username
POSTGRES_PASSWORDYes, if DATABASE_URL is emptyPostgreSQL password
POSTGRES_DBYes, if DATABASE_URL is emptyPostgreSQL database name
SECRET_KEYYesJWT signing secret
ENCRYPTION_KEYYesCredential encryption key
FRONTEND_URLRecommendedPublic browser URL, especially for OAuth callbacks
CORS_ORIGINSRecommendedAllowed browser origins
FILE_STORAGE_DIRRecommendedSet it to an absolute /app/data/files when you mount that path; the relative ./data/files default resolves under /app/backend in this image and misses the mount
ALLOW_REGISTERRecommendedSet false in production unless open signup is intended, but only once your admin account exists — there is no first-user bootstrap
DOCKER_LOGS_ENABLEDOptionalSet true to allow the Logs tab to use Docker socket access
DOCKER_LOGS_ALLOWED_EMAILSRequired when DOCKER_LOGS_ENABLED=trueComma-separated list of trusted user emails allowed to access Docker logs
HEYM_PLUGINS_ENABLEDOptionalSet true to enable the plugin subsystem (custom nodes installed as zip). Off by default
HEYM_PLUGIN_ADMIN_EMAILSRequired when HEYM_PLUGINS_ENABLED=trueComma-separated operator emails allowed to install/uninstall plugins
HEYM_PLUGINS_DIROptionalWhere plugin files are stored. Set it to an absolute /app/data/plugins when you mount that path; the relative data/plugins default resolves under /app/backend in this image and misses the mount
HEYM_PYTHON_TOOL_SANDBOXOptionalPython tool isolation mode; defaults to auto (hardened Docker sandbox, fail closed). See Security
HEYM_PYTHON_TOOL_IMAGEOptionalOverride the Python tool sandbox image; empty = auto-detect the backend image
HEYM_CODEX_DOCKER_WORKSPACE_VOLUMEOptionalDocker volume used by sibling Codex runner containers; defaults to heym-codex-workspaces in Docker deployments
HEYM_CODEX_NETWORK_ACCESSOptionalAllow Codex's sandboxed commands to download files/dependencies; Docker deployments enable this by default
HEYM_OPENCODE_DOCKER_WORKSPACE_VOLUMEOptionalDocker volume used by sibling OpenCode runner containers; defaults to heym-opencode-workspaces in Docker deployments

Notes:

  • The image exposes port 4017
  • The backend stays internal and is proxied under /api
  • When POSTGRES_HOST=localhost, the release image automatically rewrites it to host.docker.internal when needed so the same .env works with a host-level PostgreSQL container on macOS Docker/Desktop tools
  • Keep the data/files mount if you want Drive uploads and skill-generated files to survive container restarts
  • Plugins: to enable them, set HEYM_PLUGINS_ENABLED=true and HEYM_PLUGIN_ADMIN_EMAILS, and mount data/plugins so installed plugin files persist across container recreates. Plugin metadata lives in your PostgreSQL, so it also survives. A plugin's declared pip dependencies are installed into the container at install time; because the image filesystem is ephemeral, they are reinstalled automatically on startup for every installed plugin (the release image's uv venv is writable, so this works in the single-container image too)

Moving an existing database to a named volume

./deploy.sh --migrate-pgdata only knows about the Compose stack. If you run the release image against your own PostgreSQL container and that container bind-mounts a host directory, migrate it yourself.

Check what your database container uses first:

docker inspect <your-postgres-container> | grep -A8 Mounts

If "Type" reads bind, move it to a named volume:

# 1. Stop the database. Heym can stay down for this.
docker stop <your-postgres-container>
docker rm <your-postgres-container>
 
# 2. Copy the data directory into a named volume. Give each instance its own
#    volume name if this Docker host runs more than one Heym deployment.
docker volume create heym-postgres-data
docker run --rm \
  -v "/path/to/your/data/postgres:/legacy:ro" \
  -v heym-postgres-data:/pgdata \
  --entrypoint sh <your-postgres-image> -c 'cp -a /legacy/. /pgdata/'
 
# 3. Start PostgreSQL again on the volume, reusing the flags you had before
#    but replacing the bind mount.
docker run -d --name <your-postgres-container> --restart always \
  ... your original -e / -p / --network flags ... \
  -v heym-postgres-data:/var/lib/postgresql/data \
  <your-postgres-image>
 
# 4. Once it accepts connections, rebuild indexes.
docker exec <your-postgres-container> reindexdb -U <user> -d <database>

Use your own image in steps 2 and 3, not postgres:16. If your database runs pgvector/pgvector:pg16 or another variant, starting the copied cluster with the stock image can fail because the extension libraries it expects are missing.

Your original data directory is untouched by this procedure, so it remains your rollback path. Delete it only after confirming the migrated database serves your workflows. Step 4 matters for the same reason it does in the Compose migration: the copy carries any existing index damage with it.


Running More Than One Instance

Everything above deploys a single Heym instance. When one machine is no longer enough, point a second instance at the same PostgreSQL database and it joins as a worker: background runs — cron, webhooks, MCP tool calls, chat triggers — are then shared between the instances by a percentage you set under Settings → Instances.

The instances never talk to each other. Postgres carries the work, so a worker needs no open port and no route back to the main instance, and no message broker is involved. Work that touches local disk — Drive files, coding-agent workspaces, installed plugins — always runs on the main instance, so nothing has to be shared over a filesystem.

Two rules the cluster cannot enforce for you: every instance must use the same SECRET_KEY and ENCRYPTION_KEY, and ingress must point at the main instance only. Both are explained, along with the placement rules and how to choose the percentages, in Load Distribution.

This is load distribution, not high availability. The main instance remains a single point of failure for ingress, file storage and the editor.

Common Workflows

First-time setup:

./run.sh          # development — creates .env from .env.example and generates SECRET_KEY/ENCRYPTION_KEY automatically
# or
./deploy.sh       # production — same automatic .env and key generation

For the prebuilt docker run image, generate the keys manually first (see the Prebuilt Image section above).

Update production after a code change:

git pull
./deploy.sh       # rebuilds images, zero-downtime swap

Check production logs:

./deploy.sh --logs

Stop production services:

./deploy.sh --down