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); must match the URL users use in the browser.
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
  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 --helpShow usage information

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.

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 \
  -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 \
  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.

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.

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

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)

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