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.
How configuration is loaded
Section titled “How configuration is loaded”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.
Environment variables
Section titled “Environment variables”The table lists every environment variable supported by the current application.
| Variable | Default | Purpose | Secret | Restart |
|---|---|---|---|---|
ACTUAL_BENCH_AUTH | password | none turns sign-in off, for when your own proxy or VPN already controls access. Anything else asks for the password. | No | Yes |
ACTUAL_BENCH_PASSWORD | (unset: the first visitor sets it) | Sets the password at start, and replaces a forgotten one. See Sign-in. | Yes | Yes |
ACTUAL_BENCH_DB_PATH | /data/actual-bench.sqlite | Path to the metadata SQLite database. | No | Yes |
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. | Yes | Yes |
ACTUAL_BENCH_WORKERS_MAX | 2 | How many automation runs may be going at once. | No | Yes |
ACTUAL_BENCH_WORKER_HEAP_MB | 512 | Memory limit for each automation run, in MB. | No | Yes |
ACTUAL_BENCH_AUTOMATION_EXECUTOR | worker | Where automation runs execute: worker (their own thread) or in-thread (the server’s own thread). | No | Yes |
ACTUAL_BENCH_RUNTIME_DIR | actual-runtime, next to the metadata database | Where automation runs keep the temporary copy of a budget they open themselves. | No | Yes |
LOG_LEVEL | info | Server log verbosity. | No | Yes |
NEXT_DEV_ALLOWED_ORIGINS | (unset) | Dev-only: allow the HMR WebSocket through a reverse proxy (comma-separated hostnames). | No | Yes |
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.
Minimum configuration
Section titled “Minimum configuration”A persistent /data volume. That is the whole requirement. Set ACTUAL_BENCH_DB_PATH only if you
want the metadata database somewhere else.
Sign-in
Section titled “Sign-in”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_PASSWORDso 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_PASSWORDto 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_PASSWORDis set, it is the password: change it there, not in the app. - Sign-in off:
ACTUAL_BENCH_AUTH=nonelets 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 mode
Section titled “Direct mode”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.
Metadata database
Section titled “Metadata database”- Default path:
/data/actual-bench.sqlite(override withACTUAL_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 (your password)
Section titled “Remembered-server secrets (your password)”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.
The credential vault
Section titled “The credential vault”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:
ACTUAL_BENCH_VAULT_KEY, if you set one.SYNC_VAULT_KEY, the old name. It still works; App Health and the log ask you to rename it.secrets/vault.keybeside the metadata database (/data/secrets/vault.keyin Docker). Bench generates it on first start: 32 random bytes, file mode0600.
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.
Automation workers
Section titled “Automation workers”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(default2) - 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(default512) - 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(defaultworker) - set toin-threadonly 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: anactual-runtimefolder 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.
Logging
Section titled “Logging”LOG_LEVEL defaults to info. Drop it to warn or error for a quiet production log, raise it when
you are chasing something.
Secret handling
Section titled “Secret handling”- A
.envwith 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.
Configuration examples
Section titled “Configuration examples”Minimal (every feature, automations included):
environment: ACTUAL_BENCH_DB_PATH: /data/actual-bench.sqliteHolding 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 managerPlaceholders in the file, real secrets from your platform. Never the other way round.
Validation
Section titled “Validation”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.
