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 # ACTUAL_BENCH_VAULT_KEY: "..." # optional: hold the vault key yourself 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.
Public demo: Vercel and Hugging Face
Section titled “Public demo: Vercel and Hugging Face”The public demo uses a split deployment: Vercel serves the UI and Web Analytics, while its
/api/* requests are rewritten to the Hugging Face Space. The Space keeps the shared Actual
budgets and Bench metadata together in its stateful container. Merges to main deploy the UI
through Vercel’s Git integration; backend, seed, or metadata-schema changes also require updating
the Space. Visitors share disposable sample data, which resets when the Space restarts. Never use
real financial data in the public demo. The current project-level redirect also catches Vercel
preview URLs, so they continue to show the Space until that rule is changed.
See the public demo deployment guide for environment variables, routing, and cutover checks.
Fly.io
Section titled “Fly.io”Follow Deploy on Fly.io: a step-by-step guide from creating a Fly account to signing in,
with the repository’s fly.toml template (one always-on machine, a volume at /data, a health check
on /api/health). Open the app and set your password as soon as it starts, since it is on the
internet from that moment.
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, Cross Budget 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 (sidebar footer) and confirm:
- Writable is
Yes. - Schema shows a version (migrations run on boot).
- Persistence reads Persistent when /data is mounted.
- The Automations section shows the credential vault’s state and where its key comes from, enrolled connections, and each automation’s health.
Access control
Section titled “Access control”Actual Bench asks for a password before anything else (see Sign-in).
The first visitor to a new install sets it, so open Actual Bench right after deploying it, or set
ACTUAL_BENCH_PASSWORD in advance.
- For an internet-facing install, an extra layer in front (your identity provider, or a VPN such as
Tailscale) is still worth having. If that layer already controls access, you can turn Bench’s own
sign-in off with
ACTUAL_BENCH_AUTH=none. - When a proxy is the public entry point, don’t also publish port
3000on the host. Anyone could reach it directly and bypass the proxy.
Actual Bench refuses state-changing API requests sent from other websites, so a page you visit can’t act on a Bench instance it can reach through your browser, even one that is only on your LAN.
Production hardening
Section titled “Production hardening”- Terminate HTTPS at the proxy. Plain HTTP stops at localhost.
- Enable HSTS at the proxy (for example
Strict-Transport-Security: max-age=31536000) once the hostname is HTTPS-only, and use its modern TLS defaults. Actual Bench doesn’t send HSTS itself, because it also runs over plain HTTP on a LAN. - Actual Bench sends its own browser security headers (
X-Frame-Options,X-Content-Type-Options,Referrer-Policy,Permissions-Policy, and aContent-Security-Policywith a fresh nonce on every page, so only Actual Bench’s own scripts run). Don’t strip or replace these headers at the proxy. A proxy that adds its ownContent-Security-Policywould block the app’s scripts, because it can’t carry the app’s per-page nonce. If a CDN in front of Actual Bench rewrites pages to add its own scripts (Cloudflare’s Rocket Loader, for example), switch that feature off for the Actual Bench hostname in the CDN’s settings; the policy would block those scripts. Plain reverse proxies (Traefik, Caddy, Nginx) are not affected. - Actual Bench’s server refuses to connect to the cloud metadata addresses (such as
169.254.169.254) when a connection or backup destination names one, or redirects to one. LAN addresses, localhost and your container runtime’s host address are fine. - When a server Actual Bench connects to redirects it to a different address, or from
httpstohttp, Actual Bench stops there rather than send your API key on, and shows the address it was sent to. Use that address in the connection instead. (http://tohttps://on the same address is followed.) - If you set
ACTUAL_BENCH_VAULT_KEY, keep it out of the image and out of git. Use your platform’s secret manager. - Persist
/data, back it up, and lock down its permissions. It holds the generated vault key (secrets/vault.key) as well as the database. - 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.
Deployment checklist
Section titled “Deployment checklist”- Pinned image
:<version>chosen. -
/datavolume mounted, writable, and backed up. - Password set (first visit or
ACTUAL_BENCH_PASSWORD); port3000not published beside the proxy. - 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. -
ACTUAL_BENCH_VAULT_KEYstored securely, if you set one; otherwise/data/secrets/vault.keyis backed up with the volume. - App Health shows Writable = Yes, a valid schema version, and the credential vault Ready.
