New releases
Recipe version compared with the latest known upstream release. A version number, not a reading of what changed.
Actual Budget assigns every rouble or dollar to a category for the month ahead, imports transactions from files or bank integrations, and keeps a full local copy of the budget in each browser and desktop client. The server in this recipe is the sync endpoint that merges those copies and stores the file in SQLite.
Actual Budget is envelope budgeting that runs entirely on your own machines. Every client — the browser, the desktop app, a phone opening the same address — keeps a full local copy of the budget and works offline; the server this recipe runs is a sync endpoint that merges those copies and stores the file. Money is assigned to categories for the month ahead, transactions are imported from files or through bank integrations you configure yourself, and reports and schedules are computed on the client.
This recipe runs pinned Actual 26.9.0 as a single container with SQLite in one
volume: server-files for the account database and user-files for the budget
blobs. The web port binds to localhost, and only password login is enabled —
the header and OpenID methods stay closed until an operator turns them on.
The full application smoke test has passed on amd64 and arm64, and practical
backup and restore have passed on amd64.
The server password is set in the browser on the first visit, so the recipe
holds no secret of its own — which also means the first person to reach an
unprotected instance owns it. Put HTTPS in front of it before the first visit
and do not expose the port directly. Actual’s default configuration accepts
password, header, and OpenID logins at once; the recipe narrows
ACTUAL_ALLOWED_LOGIN_METHODS to password, because a trusted-header login
behind a proxy that does not strip the header is an open door. End-to-end
encryption is a per-budget setting inside the application: without it, budget
files sit on the server unencrypted and land unencrypted in every backup.
These are stored results of separate checks, not real-time data. An unknown status means the check has not been performed yet.
Recipe version compared with the latest known upstream release. A version number, not a reading of what changed.
Result of a dedicated Docker image scan, not a Compose configuration check.
A new major version is found by comparing version numbers. A compatibility verdict is entered by hand after reading the release notes and the migration steps.
Upstream Compose changes relative to the verified recipe.
Pinned image tags checked for newer versions.
From observation to action
The recipe already pins the latest known release. The steps below show the evidence retained for that version.
26.9.026.9.0Open this release on GitHub ↗ The link is evidence to review, not proof of compatibility.
The version numbers do not cross a major compatibility boundary. No compatibility verdict has been recorded.
No image-tag change is currently planned.
Change the tag and restart. Release-specific migration steps still come from the release notes and installation guide.
No version change is currently planned. The recipe includes a backup procedure. Restore has been tested.
An exact Fully verified check for this target was recorded on 2026-09-03.
The status shows how fully the published recipe meets each level's requirements.
A minimal setup for a local network or access through a VPN.
You still need to add: single server, local access, minimum components.
A public deployment with protected access and backups.
You still need to add: domain, https via reverse proxy, off-server backup.
A setup designed for predictable operations and observability.
You still need to add: separate database, external monitoring, cpu and memory limits.
Tailored to your server
Answer five questions and download a ready-to-run Actual Budget bundle.
Screenshots have not been added yet.
Read the full guide first: it explains external access, backups, and data locations.
cp .env.example .envdocker compose up -ddocker compose psUse Ubuntu 22.04+ or Debian 12+ with Docker Engine and Compose v2.24+. One CPU core, 512 MB RAM, and 2 GB disk are enough: the budget is a few megabytes and the heavy work happens in the client. The official image declares amd64 and arm64.
docker --version
docker compose version
mkdir -p ~/services/actual-budget
cd ~/services/actual-budget
cp .env.example .env
chmod 600 .env
There is no secret to generate here: Actual asks for the server password in the
browser during the first visit and stores its hash in server-files. Keep the
password in a password manager — it is the only thing standing between the
internet and the budget.
Every .env variable:
ACTUAL_PORT is the local web port, default 5006;ACTUAL_LOGIN_METHOD selects password, openid, or header;ACTUAL_ALLOWED_LOGIN_METHODS lists the methods the server accepts at all, password only in this recipe;ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB, ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB, and ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB cap sync and upload sizes;ACTUAL_TIME_ZONE is an IANA time zone;ACTUAL_DATA_VOLUME names the volume holding server-files and user-files;ACTUAL_BACKUP_DIR selects the host backup directory.docker compose config
docker compose pull
docker compose up -d --wait --wait-timeout 600
docker compose ps
curl --fail http://127.0.0.1:5006/health
/health answers {"status":"UP"} once the API and its SQLite store are ready.
Open the public address and set the server password on the bootstrap screen
before anyone else does, then create or import a budget file. A browser that has
opened the budget keeps a full local copy, so the first sync uploads the whole
file.
Keep 127.0.0.1:${ACTUAL_PORT}:5006; only an HTTPS proxy on the host can reach
the server. Allow SSH, HTTP, and HTTPS through the firewall and nothing else.
Set the server password immediately after the first start: an instance that is
reachable and not yet bootstrapped hands the budget to whoever finds it. Bank
synchronisation through GoCardless or SimpleFIN is configured inside the
application and requires outbound HTTPS.
Prefer ssh -L 5006:127.0.0.1:5006 user@server and open http://localhost:5006.
For permanent LAN access, replace the localhost bind with one specific private
IP and restrict the port with a firewall. Note that a browser needs a secure
context for the installable PWA and for the Web Crypto used by end-to-end
encryption, so plain HTTP over the LAN limits both; treat it as a temporary
route rather than the normal one.
Point budget.example.com at the server, replace the host in the Caddy, Nginx,
or Traefik example, and open the address once to set the password. The proxy
must allow uploads as large as the budget file — the Nginx sample raises
client_max_body_size to 100 MB — and must pass WebSocket upgrades for the sync
connection. Actual builds no absolute URLs of its own, so no base-URL variable
has to match the domain.
chmod +x backup.sh restore.sh
./backup.sh
The script stops the server, archives the whole /data volume, and stores .env
and compose.yaml next to it. Stopping matters: a live SQLite file can be
captured mid-write. The archive holds every budget file and the server password
hash, so encrypt it and copy it off the server. Actual also writes its own
periodic copies inside the volume and the desktop client keeps local backups;
neither is a substitute for an off-server archive.
Restore irreversibly replaces the data volume. Use the same Actual version with
the active .env:
./restore.sh ./backups/actual-budget-YYYYMMDDTHHMMSSZ.tar
curl --fail http://127.0.0.1:5006/health
The script first backs up the state being replaced, recreates the volume, unpacks the archive, and starts the server. Afterwards every client is ahead of the restored server: open each browser and desktop app, and if a client refuses to sync, remove its local file and download the budget from the server again. This procedure has not passed a practical restore test; rehearse it on a separate server first.
Create a backup and read the release notes — Actual ships a release most months and occasionally migrates the budget format:
./backup.sh
docker compose pull
docker compose up -d --wait --wait-timeout 600
curl --fail http://127.0.0.1:5006/health
docker compose logs --tail=200 actual
Replace the exact actualbudget/actual-server:26.9.0 tag with a reviewed
version; never use latest, edge, or nightly. After a format migration the
clients update their local copies on the next sync, so update the desktop apps
in the same maintenance window.
Never start an older server over budget files a newer version has migrated. Restore the previous exact tag together with the pre-update archive:
docker compose down --timeout 60
./restore.sh ./backups/actual-budget-BEFORE-UPDATE.tar
Clients that already migrated their local copy must re-download the budget from the restored server.
docker compose down preserves the data volume. After verifying an off-server
backup, remove everything irreversibly:
docker compose down
docker volume rm actual-data
rm -rf ~/services/actual-budget
Substitute the actual name when ACTUAL_DATA_VOLUME differs. Budget copies also
live in each browser and desktop client; clear them separately if the data must
be gone everywhere.
Sources: Docker installation, server configuration, backup and restore, and release v26.9.0.
docker compose ps
docker compose logs --tail=200 actual
docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q actual)"
The health check runs node scripts/health-check.js inside the container, which
calls /health. A container that starts and then fails the check almost always
has a permission problem on the volume or a ACTUAL_LOGIN_METHOD value that is
not one of password, openid, or header.
/account/needs-bootstrap reports whether a password has been set:
curl --fail http://127.0.0.1:5006/account/needs-bootstrap
"bootstrapped":false on an instance you already configured means the server is
looking at an empty server-files — usually a renamed or recreated volume.
Check ACTUAL_DATA_VOLUME and docker volume ls before setting a new password:
the old volume still holds the budget.
There is no reset command. Stop the server, delete account.sqlite from
server-files, and bootstrap again — the budget files in user-files survive:
docker compose stop actual
docker run --rm -v actual-data:/data alpine:3.22 rm -f /data/server-files/account.sqlite
docker compose start actual
Take a backup first. If the budget itself is end-to-end encrypted, its own encryption password is separate and cannot be recovered this way.
Each client holds a complete local copy, so a server that was restored from a backup is behind the clients. Open the budget in the client, use “Reset sync” in the file settings, or remove the local file and download the budget from the server again. Do this on one device at a time to avoid two clients re-uploading divergent copies.
The upload limits are explicit: ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB,
ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB, and
ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB. Raise them together with the proxy’s body
limit — in the Nginx sample that is client_max_body_size — and recreate the
container.
GoCardless and SimpleFIN are configured inside the application, not in this recipe, and both need outbound HTTPS from the container. Check the log for the provider’s error, then verify egress:
docker compose exec actual node -e "fetch('https://bankaccountdata.gocardless.com/api/v2/').then(r=>console.log(r.status)).catch(e=>{console.error(e.message);process.exit(1)})"
On the host, run curl -I http://127.0.0.1:5006/. If that answers, the proxy is
the problem: inside a proxy container 127.0.0.1 is the proxy itself, so use a
host gateway address or a shared Docker network. A page that loads but never
syncs usually means the proxy drops WebSocket upgrades — both sample configs
forward them.
Fossary is an independent informational catalog and is not affiliated with the developers of the listed applications. We collect information from public sources and verify published recipes, but we do not develop, distribute, or control these applications.
A listing or validation status is not a requirement, endorsement, or personal recommendation to use an application. You decide whether it is suitable and use it at your own risk. We do not warrant an application's security, availability, or fitness for purpose and, to the extent permitted by law, are not liable for resulting loss or damage. Review its license, security settings, data processing, and backup procedures before installation.