The part that makes this safe: local-first
Most cloud apps are the opposite of local-first. The server holds the truth; your screen shows a rendered view of it. Lose your connection and you are locked out; the company can read everything because the data lives on their side in a form they can query.
Actual inverts that. The full budget file lives in the client — your browser or the desktop app — and that is where every calculation happens. It works completely offline. The sync server exists only to pass changes between your laptop and your phone so both stay current. When you turn on end-to-end encryption, the server stores nothing but ciphertext: it cannot read a single transaction, because the key never leaves your devices.
So the threat model is refreshingly small. A compromise of your server leaks an encrypted blob, not your spending. That is the whole reason self-hosting Actual is worth doing rather than just trusting someone's cloud.
Standing up the sync server
You need very little. Actual's server is tiny — it will run on the smallest VPS you can rent, with room to spare. Ubuntu 24.04 and a subdomain pointed at the server's IP is the whole shopping list.
Run the official server image behind Caddy so HTTPS is automatic. Save as docker-compose.yml:
services:
actual:
image: actualbudget/actual-server:latest
restart: always
volumes:
- actual_data:/data
expose:
- "5006"
caddy:
image: caddy:2-alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
depends_on:
- actual
volumes:
actual_data:
caddy_data:Note the sync server is not exposed directly to the internet — only Caddy publishes ports, and it proxies inward. The Caddyfile next to the compose file:
budget.example.com {
reverse_proxy actual:5006
}Bring it up:
docker compose up -dOpen https://budget.example.com. On first load Actual asks you to set a server password. This password protects access to the server itself — it is the gate on who can reach the sync API. It is not your encryption key, which comes next and matters more.
Turn on end-to-end encryption
This is the step that makes the server blind, and it is off by default, so do it deliberately. When you create or upload your budget file, Actual offers to enable encryption and asks for a separate encryption password. From that it derives a key that stays on your client. The server only ever receives already-encrypted data.
Two things to burn into memory:
- The encryption password is not recoverable. There is no reset link, no support email that can help. If you forget it, the encrypted file on the server is permanently unreadable — that is what "the server can't read it" cuts both ways to mean. Put it in your password manager the moment you set it.
- The server password and the encryption password are different jobs. One controls access; one controls readability. Use two distinct strong passwords so a leak of one does not hand over the other.
Connecting your devices
Point clients at your server and they sync through it:
- Desktop and mobile: install the Actual app, and on setup enter
https://budget.example.complus your server password. It downloads the (encrypted) file and prompts for the encryption password to unlock it locally. - Browser: just visit the URL. Actual runs entirely as a web app; there is nothing else to install.
Each device keeps its own local copy and reconciles through the server. Edit your budget on a plane with no signal, and it merges cleanly the next time you are online.
Backups — the step people skip and regret
Here is the catch of local-first design: if your devices each hold a copy, you might feel backed up. You are not, reliably. Reinstall the app, wipe a laptop, or lose your phone, and the local copies go with them. The server's copy is your durable one — and if the server dies without a backup, an encrypted blob you cannot restore is the same as no blob at all.
Everything the server needs lives in one Docker volume, actual_data. Back it up off-site nightly. Configure an rclone remote first with rclone config, then:
docker run --rm -v actual_data:/data -v /root:/backup alpine \
tar czf /backup/actual-backup.tar.gz -C /data .
rclone copy /root/actual-backup.tar.gz remote:actual-backupsDrop that in a cron job. Because the data at rest is already end-to-end encrypted, the backup you ship off-site is encrypted too — you can store it on ordinary cloud storage without exposing your finances, which is a quiet nice consequence of turning encryption on earlier.
Actual also has its own trick worth knowing: from the app, File → Export downloads the whole budget as a single .zip you can open on any Actual install. Run that manually every so often and keep it with your important documents. It is the belt to the server backup's suspenders, and it is the copy that survives even if you walk away from self-hosting entirely.
What running this costs
Almost nothing in resources — Actual's sync server is one of the lightest things you can self-host, comfortable on a 1 vCPU / 1 GB box with the CPU mostly asleep. On NoctHost you can pay for that box hourly from a prepaid balance topped up with crypto, which is a fitting way to keep the machine that holds your budget out of a card-linked account. The heavier cost is discipline: set the encryption password, save it somewhere independent, and let the backup cron run. Do those three things and you have private money tracking that no company can read, pivot away from, or lose on your behalf.