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_AUTHpasswordnone turns sign-in off, for when your own proxy or VPN already controls access. Anything else asks for the password.NoYes
ACTUAL_BENCH_PASSWORD(unset: the first visitor sets it)Sets the password at start, and replaces a forgotten one. See Sign-in.YesYes
ACTUAL_BENCH_DB_PATH/data/actual-bench.sqlitePath to the metadata SQLite database.NoYes
ACTUAL_BENCH_VAULT_KEY(unset: a generated key file)Your own key for the credential vault. Optional; see below. SYNC_VAULT_KEY is its old name and still works.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

DEMO_MODE=1 enables the demo connection flow and disables sign-in on any host; do not set it on a self-hosted instance. The other demo and analytics variables - DEMO_BASE_URL, DEMO_API_KEY, and the two budget Sync IDs - are for the public demo. NEXT_PUBLIC_ANALYTICS is a build-time flag with no host guard; leave it unset outside the Vercel demo, where 1 enables Web Analytics. The demo UI runs on Vercel; /api/* requests are rewritten to the Hugging Face Space, which holds the shared Actual budgets and Bench metadata. See the deployment guide for general hosting guidance and the public demo deployment guide for the architecture and cutover checks.

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

Actual Bench asks for one password. It signs you in, and it also encrypts the connections you save, so signing in opens them too. There is no separate vault password.

  • First visit: the first person to open a new install sets the password. Open Actual Bench right after deploying it, or set ACTUAL_BENCH_PASSWORD so the question never comes up.
  • Upgrading: if you had already set a password for saved connections, that is your password now. Otherwise Actual Bench asks you to set one once.
  • Staying signed in: choose 8 hours, 24 hours, 7 days or 30 days when you sign in; the time resets while you use the app. Restarting Actual Bench signs everyone out.
  • Wrong password: after five wrong tries, that browser waits a short while before it can try again, and the wait grows with more wrong tries. Other browsers are not held up by it; only a flood of wrong passwords from many places at once makes everyone wait briefly.
  • Forgot it: set ACTUAL_BENCH_PASSWORD to a new password and restart. Saved connections were encrypted with the old password, so they are cleared and you save them again. Automations, backups and their stored credentials use the separate vault key and are kept.
  • While ACTUAL_BENCH_PASSWORD is set, it is the password: change it there, not in the app.
  • Sign-in off: ACTUAL_BENCH_AUTH=none lets anyone who can reach Actual Bench use it. Only do this behind your own authentication. App Health shows a warning while it is off.

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

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 your Actual Bench password (see Sign-in). Credentials are server-scoped (one saved server opens any of its budgets); budget encryption passwords are sealed separately, per budget. The password and derived key are never stored (only the ciphertext and a salt), so the server cannot decrypt on its own; signing in unlocks them for that session. This is separate from the credential 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 forgotten password is replaced with ACTUAL_BENCH_PASSWORD, which clears these saved servers.

Automations that run with Bench closed (unattended sync, scheduled bank sync, backups) use credentials enrolled in the credential vault: stored AES-256-GCM encrypted in the metadata database, with the key never stored there. The vault needs no setup. Bench finds its key in this order:

  1. ACTUAL_BENCH_VAULT_KEY, if you set one.
  2. SYNC_VAULT_KEY, the old name. It still works; App Health and the log ask you to rename it.
  3. secrets/vault.key beside the metadata database (/data/secrets/vault.key in Docker). Bench generates it on first start: 32 random bytes, file mode 0600.

Bench generates a key only when no credentials are stored, and it never overwrites a key file. To use a Docker or Kubernetes secret, mount it at that path.

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.
  • If you set ACTUAL_BENCH_VAULT_KEY, use your container platform’s secret management for it.
  • 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 (every feature, automations included):

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

Holding the vault key yourself:

environment:
ACTUAL_BENCH_DB_PATH: /data/actual-bench.sqlite
ACTUAL_BENCH_VAULT_KEY: ${ACTUAL_BENCH_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 the credential vault ready, with its key coming from where you expect.