Deployment
Use this page after completing Installation. It covers production networking, HTTPS, persistent storage, and hardening.
A typical deployment uses one container, a persistent /data volume, an HTTPS endpoint, and network
access for the selected connection mode.
Deployment architecture
Section titled “Deployment architecture”It is a Next.js app. Where the traffic goes depends entirely on the connection mode:
- Direct mode - the browser talks to your Actual Server itself, running Actual’s engine in a worker. Bench serves the app and nothing else. No budget data passes through it.
- HTTP API mode - the browser only ever talks to Bench, which forwards to
actual-http-apiserver-side. The browser never sees that URL.
Either way, Bench keeps its own metadata database - SQLite, under /data.
Choose an image tag
Section titled “Choose an image tag”Multi-architecture images are published for linux/amd64 and linux/arm64:
xrous/actual-bench:latest- stable, for normal production.xrous/actual-bench:<version>(e.g.:1.2.4) - a pinned, unchanging version.xrous/actual-bench:edge- unstable, rebuilt on every merge; for testing only.
Pin a :<version> in production and upgrades happen when you decide, not when you restart.
Docker Compose deployment
Section titled “Docker Compose deployment”services: actual-bench: image: xrous/actual-bench:1.2.4 # pin a version in production container_name: actual-bench ports: - "3000:3000" environment: ACTUAL_BENCH_DB_PATH: /data/actual-bench.sqlite # SYNC_VAULT_KEY: "..." # only if you use unattended server-side sync volumes: - actual-bench-data:/data restart: unless-stopped
volumes: actual-bench-data:See Configuration for the full environment-variable reference.
Hosted platforms
Section titled “Hosted platforms”Docker Compose above is the universal method; these run the same image with a platform’s own tooling instead. The single-instance/SQLite model below applies to all of them equally.
Fly.io
Section titled “Fly.io”fly launch --from https://github.com/x-rous/actual-bench --generate-name --ha=falseThe repository’s fly.toml is a template, copied into a new Fly app of your own — it does not
deploy into the maintainer’s app. What it sets up:
- A Fly Volume mounted at
/data, so the metadata database, remembered-server credentials, and backup/automation history survive restarts and redeploys. - One Machine, always on (
auto_stop_machines = "off"). Bench’s automation scheduler runs in-process; a Machine that scales to zero on inactivity silently stops it.--ha=falseon the launch command prevents Fly from also creating a spare Machine for availability, which this single-SQLite-volume model doesn’t support (see Scaling and the unattended scheduler below). - A health check against
/api/health, matching the same endpoint the Docker image’s ownHEALTHCHECKuses.
Secrets never belong in fly.toml. Set them with:
fly secrets set SYNC_VAULT_KEY="..."fly secrets set SYNC_SCHEDULER_SECRET="..."Scaling to multiple Machines is not supported by the current architecture — see Scaling and the unattended scheduler.
PikaPods
Section titled “PikaPods”Available once the official PikaPods listing is approved. It will run the same image with one
port (3000) and one persistent volume (/data); no environment variables are required.
Network design
Section titled “Network design”There are two different reachability requirements here. Mixing them up is the single most common deployment mistake:
- Browser → service (Direct mode): the user’s browser must reach the Actual Server URL.
- Container → container (HTTP API mode): the Actual Bench container must reach
actual-http-api.
Direct-mode network requirements
Section titled “Direct-mode network requirements”Direct mode runs Actual’s engine in the browser. That has consequences:
- The browser must be able to reach your Actual Server URL.
- For scheduled runs on Direct budgets (backups, bank sync, Budget File Sync), Bench opens the budget on its own server, so the Bench server must be able to reach the Actual Server URL too.
- The response has to satisfy CORS, or come through your proxy as same-origin.
- Actual Bench sets the cross-origin isolation headers it needs on its own assets
(
Cross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corp). - Serve Actual Bench over HTTPS, and the Actual Server URL too. Direct mode sends authenticated
requests to that URL straight from the browser, so
http://there puts the server password on the wire. Plain HTTP is only acceptable on an internal hop behind a same-origin HTTPS proxy.
If your environment genuinely can’t support Direct mode, there’s nothing to configure ahead of time: Bench reports the failure clearly the moment someone tries it, and they can use HTTP API Server mode instead.
HTTP API network requirements
Section titled “HTTP API network requirements”- The Actual Bench container must reach the
actual-http-apibase URL. - Only Bench needs that path. The browser never uses it.
- If both run in Docker, attach them to a shared network and use the API’s service name.
fetch failed or 502 Bad Gateway in HTTP mode is container-to-container networking, nine times out
of ten.
Reverse proxy and HTTPS
Section titled “Reverse proxy and HTTPS”Put a reverse proxy in front and terminate HTTPS there:
- Forward the correct host and protocol headers.
- HTTPS end to end where you can. Direct mode needs a secure, isolated origin to work at all.
- Serving Actual Server through the same proxy makes it same-origin, which solves Direct mode’s CORS and isolation requirements without opening CORS up.
App Health after deployment
Section titled “App Health after deployment”Open App Health (Tools sidebar) and confirm:
- Writable is
Yes. - Schema shows a version (migrations run on boot).
- Persistence reads Persistent when /data is mounted.
- If you use unattended sync, the Unattended sync scheduler section shows the vault state, enrolled count, and last tick.
Production hardening
Section titled “Production hardening”- Terminate HTTPS at the proxy. Plain HTTP stops at localhost.
- Secrets like
SYNC_VAULT_KEYstay out of the image and out of git. Use your platform’s secret manager. - Persist
/data, back it up, and lock down its permissions. - Pin a specific
:<version>and upgrade deliberately (see Upgrading and Backups). - Don’t publicly expose backend APIs (
actual-http-api, Actual Server) more than necessary. - Redact secrets before sharing logs.
Scaling and the unattended scheduler
Section titled “Scaling and the unattended scheduler”The unattended sync scheduler runs in-process and ticks about once a minute; a single instance is
sufficient and is the intended model. Do not assume distributed-lock behavior across multiple
replicas. If you need external control, drive the trigger endpoint with SYNC_SCHEDULER_SECRET instead
(see Configuration).
Deployment checklist
Section titled “Deployment checklist”- Pinned image
:<version>chosen. -
/datavolume mounted, writable, and backed up. - HTTPS terminated at the proxy; host/protocol headers forwarded.
- Direct mode: browser can reach Actual Server; CORS / cross-origin isolation satisfied.
- HTTP mode: Actual Bench container can reach
actual-http-api. -
SYNC_VAULT_KEYset (only if using unattended sync) and stored securely. - App Health shows Writable = Yes and a valid schema version.
