Fully verified28,528

Personal finance

Actual Budget

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.

Overview

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.

Security and recipe boundaries

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.

Update tracking

These are stored results of separate checks, not real-time data. An unknown status means the check has not been performed yet.

New releases

Recipe version compared with the latest known upstream release. A version number, not a reading of what changed.

Current version 26.9.0Sep 2, 2026, 7:40 PM UTC

Critical vulnerabilities

Result of a dedicated Docker image scan, not a Compose configuration check.

Not checkedNot checked

Breaking updates

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.

Not checkedNot checked

compose.yaml

Upstream Compose changes relative to the verified recipe.

Upstream Compose is not trackedNot checked

Outdated Docker images

Pinned image tags checked for newer versions.

Not checkedNot checked

From observation to action

Update plan

The recipe already pins the latest known release. The steps below show the evidence retained for that version.

26.9.026.9.0
  1. Open the release evidence

    Open this release on GitHub ↗ The link is evidence to review, not proof of compatibility.

  2. Review the version boundary

    The version numbers do not cross a major compatibility boundary. No compatibility verdict has been recorded.

  3. Prepare image-tag changes

    No image-tag change is currently planned.

  4. Follow the documented migration shape

    Change the tag and restart. Release-specific migration steps still come from the release notes and installation guide.

  5. Protect the rollback point

    No version change is currently planned. The recipe includes a backup procedure. Restore has been tested.

  6. Verify the updated recipe

    An exact Fully verified check for this target was recorded on 2026-09-03.

Read the update procedure in the installation guide ↓

Specifications

Security and exposure

Installation levels

The status shows how fully the published recipe meets each level's requirements.

Partial

For home

A minimal setup for a local network or access through a VPN.

  • Single server
  • Local access
  • Minimum components

You still need to add: single server, local access, minimum components.

Partial

For a VPS

A public deployment with protected access and backups.

  • Domain
  • HTTPS via reverse proxy
  • Off-server backup

You still need to add: domain, https via reverse proxy, off-server backup.

Partial

Reliable

A setup designed for predictable operations and observability.

  • Separate database
  • Healthcheck
  • External monitoring
  • CPU and memory limits

You still need to add: separate database, external monitoring, cpu and memory limits.

Tailored to your server

Configuration generator

Answer five questions and download a ready-to-run Actual Budget bundle.

Runs locally
01 Do you have a domain?
02 Which reverse proxy?
03 Server architecture
04 Where should data be stored?

Use an absolute path for a separate disk, such as /srv/actual-budget.

05 Is VPN access required?

Your bundle is ready

The archive is created in your browser. Your answers are never sent anywhere.

Screenshots

Screenshots have not been added yet.

Quick start

Read the full guide first: it explains external access, backups, and data locations.

Prepare settingscp .env.example .env
Launchdocker compose up -d
Check statusdocker compose ps

Step-by-step guide

1. Check the Ubuntu or Debian server

Use 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

2. Prepare the recipe

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.

3. Start and set the server password

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.

VPS deployment

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.

Trusted LAN access

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.

Domain and HTTPS

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.

Backup

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

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.

Update Actual

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.

Rollback

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.

Stop and remove

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.

Troubleshooting

The container never becomes healthy

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.

The bootstrap screen asks for a password again

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

The server password is lost

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.

A client refuses to sync or reports a conflict

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.

Sync fails on a large budget

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.

Bank synchronisation does nothing

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)})"

The reverse proxy returns 502 or the page loads without syncing

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.

Official sources

Latest release: 2026-09-01 · GitHub Stars: 28,528 · metadata checked Sep 2, 2026, 7:40 PM UTC

Disclaimer

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.