Skip to content

Configuration

Use this reference if you operate Actual Bench. A basic self-hosted configuration needs no environment variables; persist /data so Actual Bench metadata survives restarts. See Installation for the complete prerequisites. Set an environment variable when you need the feature it enables.

Everything is an environment variable on the container. The Node server reads the plain ones. Anything prefixed NEXT_PUBLIC_ is a build value and reaches the browser, so nothing secret belongs there. The version comes from the build; you do not set it.

The table lists every environment variable supported by the current application.

VariableDefaultPurposeSecretRestart
ACTUAL_BENCH_DB_PATH/data/actual-bench.sqlitePath to the metadata SQLite database.NoYes
SYNC_VAULT_KEY(unset)Enables unattended server-side sync; derives the encryption key for stored credentials.YesYes
SYNC_SCHEDULER_SECRET(unset)Enables the external scheduler trigger endpoint; required in its request header.YesYes
ACTUAL_BENCH_WORKERS_MAX2How many automation runs may be going at once.NoYes
ACTUAL_BENCH_WORKER_HEAP_MB512Memory limit for each automation run, in MB.NoYes
ACTUAL_BENCH_AUTOMATION_EXECUTORworkerWhere automation runs execute: worker (their own thread) or in-thread (the server’s own thread).NoYes
ACTUAL_BENCH_RUNTIME_DIRactual-runtime, next to the metadata databaseWhere automation runs keep the temporary copy of a budget they open themselves.NoYes
LOG_LEVELinfoServer log verbosity.NoYes
NEXT_DEV_ALLOWED_ORIGINS(unset)Dev-only: allow the HMR WebSocket through a reverse proxy (comma-separated hostnames).NoYes

The demo and analytics variables - NEXT_PUBLIC_ANALYTICS and friends - belong to the public demo build and do nothing in a self-hosted one.

A persistent /data volume. That is the whole requirement. Set ACTUAL_BENCH_DB_PATH only if you want the metadata database somewhere else.

Direct Actual Server mode and HTTP API Server mode are both always offered; there is no setting to turn either one off. Actual Bench sets the cross-origin isolation headers Direct mode needs on every response, unconditionally. If your environment genuinely can’t support Direct mode (the browser can’t reach the Actual Server, or a proxy strips the isolation headers), Bench reports that clearly at the point of connecting so you can just use HTTP API Server mode instead. See Deployment for the browser reachability and cross-origin requirements Direct mode needs.

  • Default path: /data/actual-bench.sqlite (override with ACTUAL_BENCH_DB_PATH).
  • Requires a writable persistent volume.
  • Schema migrations run on boot; App Health shows the current and latest schema version.
  • Stores Actual Bench workflow metadata only - not Actual credentials or budget data, except two explicit, encrypted opt-ins: remembered-connection secrets and unattended-sync credentials (below).

Remembered-server secrets (user passphrase)

Section titled “Remembered-server secrets (user passphrase)”

When a user opts in to remembering a server, its secret is sealed AES-256-GCM in this database, encrypted with a key derived from a passphrase the user sets - no environment variable is involved. Credentials are server-scoped (one saved server opens any of its budgets); budget encryption passwords are sealed separately, per budget. The passphrase and derived key are never stored (only the ciphertext and a salt), so the server cannot decrypt on its own; the user unlocks once per session. This is separate from the SYNC_VAULT_KEY unattended-sync vault below - the two never share ciphertext or a key. Back up and persist /data to preserve remembered servers across container replacement; a writable path by itself does not prove durability. A lost passphrase means the user resets the vault and re-saves.

These gate the whole automation engine, not just Budget File Sync — scheduled bank sync and backups depend on them too:

  • SYNC_VAULT_KEY - a strong operator secret. When set, the in-process scheduler runs and enrolled credentials can be stored/decrypted. When unset, the feature is fully disabled: nothing is persisted and the scheduler never runs. Enrolled credentials are stored AES-256-GCM encrypted in the metadata database; the key is derived from this variable and never stored in the database.
  • SYNC_SCHEDULER_SECRET (optional) - enables an external trigger endpoint at POST /api/sync/scheduler/tick, which requires the header x-scheduler-secret. Without it, that endpoint returns 403. Use this only if you want to drive the scheduler from an external cron instead of (or in addition to) the built-in one.

Generate strong values with, for example:

Terminal window
openssl rand -base64 48

See Budget Sync → Automation for how flows opt in, and the repository’s docs/UNATTENDED_SYNC.md for the operator walkthrough.

By default, every automation run gets a worker thread of its own inside the Bench server, with its own memory limit. A run that hangs can be stopped, and a run that reaches its memory limit or crashes is normally ended alone while the rest of Bench carries on. The limit covers the run’s own working memory, not everything the server process uses, so the container still needs enough memory overall. Automations explains what you see when that happens. The defaults suit most installs.

  • ACTUAL_BENCH_WORKERS_MAX (default 2) - how many runs may be going at once. A scheduled run that finds no free slot waits for the next minute without counting as a failure; Run now says Bench is busy.
  • ACTUAL_BENCH_WORKER_HEAP_MB (default 512) - the memory limit for each run. Raise it if a large budget’s runs stop with “Ran out of memory”. Give the container room for this much per run that can go at once; as a guide, a container running automations on budgets of around 30,000 transactions wants at least 1 GB.
  • ACTUAL_BENCH_AUTOMATION_EXECUTOR (default worker) - set to in-thread only if your platform cannot start worker threads. Runs then execute in the server’s own thread, as they did before: a time limit can only ask a run to stop, and a run that runs out of memory takes the whole server with it.
  • ACTUAL_BENCH_RUNTIME_DIR (default: an actual-runtime folder next to the metadata database) - where a run keeps the copy of a budget it downloads to work on. Each run gets a private folder, readable only by the Bench process, that is deleted when the run ends. A folder left behind by a crash is removed automatically once it has gone 15 minutes untouched. There is nothing here to back up. Put it on fast local disk with room for a copy of each budget being worked on at once.

At startup Bench checks that a worker thread can start. App Health → Automations → Workers shows the result. If the check fails, automations do not run and the reason is shown there; they never quietly fall back to the server’s own thread.

LOG_LEVEL defaults to info. Drop it to warn or error for a quiet production log, raise it when you are chasing something.

  • A .env with real secrets never goes into version control.
  • Use your container platform’s secret management for SYNC_VAULT_KEY and SYNC_SCHEDULER_SECRET.
  • Nothing secret goes in a NEXT_PUBLIC_* variable. Those are shipped to every browser.
  • Redact logs before you share them. Rotate anything you even suspect leaked.

Minimal (Direct or HTTP mode, no unattended sync):

environment:
ACTUAL_BENCH_DB_PATH: /data/actual-bench.sqlite

With unattended server-side sync:

environment:
ACTUAL_BENCH_DB_PATH: /data/actual-bench.sqlite
SYNC_VAULT_KEY: ${SYNC_VAULT_KEY} # provided via your secret manager

Placeholders in the file, real secrets from your platform. Never the other way round.

Restart, then open App Health. The database should be writable, the schema current, and - if you configured it - the scheduler should report the vault state you expect.