Deployment guide

Self-Host Actual Budget on a VPS: Your Private YNAB Alternative

Deploy on a VPS Cloud →

Tutorial

Self-hosting

Self-Host Actual Budget on a VPS: Your Private YNAB Alternative

YNAB proved that zero-based (envelope) budgeting works, but its $99/year subscription and cloud-only model aren't for everyone. Actual Budget (MIT, ~26 k GitHub stars) brings the same methodology — assign every dollar a job before you spend it — to a self-hosted Node.js server that runs on a single Docker container. Your data stays in an SQLite file on your own VPS; every device syncs, with optional end-to-end encryption. Here is how to deploy it in minutes.

12 min read11 steps
Contents· Why self-host your budget instead of using YNAB1/13
  1. 01Why self-host your budget instead of using YNAB
  2. 02Deploy Actual Budget with Docker in two minutes
  3. 03What you get out of the box
  4. 04Requirements in figures for Actual Budget
  5. 05Add HTTPS so you can sync from anywhere
  6. 06Setup in six steps
  7. 07Bank sync: Enable Banking, an experimental feature
  8. 08Set up Enable Banking in Actual in five steps
  9. 09How to back up your budget
  10. 10End-to-end encryption: what it covers and what it does not
  11. 11Update Actual without losing your data
  12. 12Troubleshooting: real messages and known causes
  13. 13Never post “sensitive” logs as they are

Why self-host your budget instead of using YNAB

The envelope budgeting method is straightforward: allocate every incoming dollar to a named category (rent, food, savings, emergency fund) before spending it. When a category runs out, you either stop spending or consciously move money from another envelope. YNAB popularised this approach in a polished app — but it costs $99/year, stores your financial data on its servers, and can't be audited. Actual Budget replicates the method (including the coveted 'roll with the punches' flexibility) with full source code on GitHub under the MIT licence. Self-hosting it on a VPS means your transactions, balances and budget history never leave your infrastructure — and the annual cost is whatever a small VPS costs.

Deploy Actual Budget with Docker in two minutes

Actual ships as a single Docker image — no database server, no message broker, no configuration files. One container, one volume, one port. On your Ubuntu 22.04 VPS, run: docker run -d --restart=always -p 5006:5006 -v actual-budget:/data --name actual-budget actualbudget/actual-server. The server is ready in under two seconds on port 5006. Open http://<your-vps-ip>:5006 in a browser, click 'Create new budget', and you are budgeting. The entire SQLite database is stored in the actual-budget Docker volume — back it up with a single docker run --rm -v actual-budget:/data alpine tar -czf - /data. The folder mounted on /data holds two subfolders, server-files and user-files: that is where the server’s entire state lives. For a lasting deployment, prefer a docker-compose.yml file (the official documentation points to the one in the repository, packages/sync-server/docker-compose.yml) and start it with docker compose up --detach.

What you get out of the box

  • Zero-based envelope budgeting — assign every dollar to a category before spending; Actual flags overspending in real time.
  • Multi-device sync — phone, tablet and desktop all share the same live budget via the encrypted server.
  • Optional end-to-end encryption — once enabled in the settings, the server only stores ciphertext; your transactions are readable only on your devices.
  • Optional bank sync: Enable Banking (European banks, experimental feature), SimpleFIN Bridge (North America), Akahu (New Zealand) or Pluggy.ai (Brazil); per the documentation, GoCardless no longer accepts new accounts. Manual import of OFX/QFX/CSV files remains possible.
  • Reporting: net-worth chart, cash-flow, spending by category, and fully custom reports.
  • YNAB 4 and nYNAB import — migrate your full history in about five minutes.

Requirements in figures for Actual Budget

Actual is one of the lightest services to host. On the machine side: a VPS with 1 GB of RAM running Ubuntu 22.04 is plenty, the application using around 80 MB at rest (check this on your own instance with docker stats). On the network side: a domain name, a DNS record pointing to the VPS, and ports 80 and 443 open for the reverse proxy; port 5006 can stay closed to the public once Caddy is in place. As for the server’s own figures: the default port is 5006, the data folder is /data (variable ACTUAL_DATA_DIR), and file uploads are capped by default at 20 MB for sync and 50 MB for encrypted sync. A very old or large budget can exceed these thresholds; a sync reset then compresses the history. For bank sync, add an account with your chosen provider, its API keys and, for Enable Banking, a Production application whose redirect URL uses HTTPS.

Add HTTPS so you can sync from anywhere

To access your budget from outside the VPS — on mobile or from another country — you need HTTPS. The simplest path is Caddy. Install it on the same VPS and add a one-line Caddyfile: budget.yourdomain.com { reverse_proxy localhost:5006 }. Caddy handles the Let's Encrypt certificate automatically. Once the domain resolves, open https://budget.yourdomain.com on your phone, tap the browser's 'Add to Home Screen' button, and Actual becomes a full PWA icon on your home screen. All sync traffic between your devices and the server goes through this TLS channel. This is not just a convenience. Actual needs a secure context to enable SharedArrayBuffer, which its SQLite engine uses: without HTTPS, the app refuses to start in the browser. The official documentation lists three causes for this block: no HTTPS, Cross-Origin-Embedder-Policy and Cross-Origin-Opener-Policy headers set incorrectly, or a browser that is too old. With the default actual-server, both headers are already sent; if you go through Nginx, do not duplicate them (see troubleshooting below). Bank sync adds a second reason: the redirect URL registered with the provider must use https.

Setup in six steps

  1. Create a VPS (1 GB RAM, Ubuntu 22.04)

    Actual idles under 80 MB RAM. Even the smallest VPS plan gives you plenty of headroom.

  2. Deploy the container

    docker run -d --restart=always -p 5006:5006 -v actual-budget:/data --name actual-budget actualbudget/actual-server

  3. Create your budget

    Open port 5006 in a browser, click 'Create new budget', add your bank accounts with opening balances.

  4. Set up envelope categories

    Create categories (Housing, Food, Transport, Savings…) and allocate your expected monthly income across them.

  5. Secure with HTTPS

    Install Caddy, add budget.yourdomain.com { reverse_proxy localhost:5006 } to your Caddyfile, and reload.

  6. Install on mobile

    Open the HTTPS URL on your phone and use 'Add to Home Screen' — Actual Budget becomes a PWA icon.

Bank sync: Enable Banking, an experimental feature

Actual’s official documentation lists Enable Banking among its sync providers, for European banks, and calls it an experimental feature: it may have bugs, missing functionality, incomplete documentation, and may be removed in a future release. It may also be absent from the latest stable release; the page advises enabling experimental features and using the nightly images for the most recent implementation. User feedback is gathered in GitHub issue no. 7799 (opened in May 2026, labelled “experimental feature”, still open when we reviewed it). It contains no list of guaranteed banks: it is up to you to check, in Actual’s wizard, that your country and bank appear, then to test on a single account before relying on it. Also remember that the provider’s keys live on your server: a properly hardened VPS and protected backups are part of the setup.

Set up Enable Banking in Actual in five steps

  1. Create the application at Enable Banking

    Sign in to your Enable Banking account, create a new application and choose the Production type.

  2. Declare the redirect URL over HTTPS

    Use your domain followed by /enablebanking/auth_callback, for example https://budget.example.com/enablebanking/auth_callback, then click Register. The scheme must be https.

  3. Link your accounts on the Enable Banking side

    First link the accounts you want in Enable Banking’s interface; otherwise, the documentation warns, you will see neither a list of banks nor a list of accounts in Actual.

  4. Enter the application ID and the key

    Copy the Application ID (format 12345678-1234-1234-1234-123456789012) with no space before or after. In Actual: More → Bank Sync → Set up Enable Banking, paste the ID and provide the .pem private key file.

  5. Link an Actual account to your bank

    In an Actual account, choose Link account → Enable Banking, select your country and bank, then follow the bank’s authorization flow. Check the first imported transactions before moving on to another account.

How to back up your budget

All data lives in the actual-budget Docker volume. A daily cron backup is one command: docker run --rm -v actual-budget:/data -v /backups:/out alpine tar czf /out/actual-budget-$(date +%Y%m%d).tar.gz /data. Restore by reversing the tar into a fresh volume before starting the container. Actual also has a built-in export: from Settings, in the Data section, click “Export data”. This creates a portable ZIP of your entire budget that you can import on any other Actual instance. Two clarifications. First, this volume also holds your bank-sync credentials: the documentation says they are stored on the server, outside end-to-end encryption, so anyone with access to the database can read them. Treat the archive as a secret: restricted permissions, an encrypted copy kept off the VPS. Second, after a restore, the documentation recommends a sync reset (Settings → Advanced → Reset Sync), done from the device holding the most recent changes, so the backup does not sync back to its old state. Test a full restore on a throwaway instance at least once: a backup that has never been restored remains a hypothesis.

End-to-end encryption: what it covers and what it does not

Actual’s end-to-end encryption is optional. To enable it, open the budget, go to the settings, Encryption section, click “enable encryption” and choose a password: a key derived from that password encrypts your data before it leaves the device. A few points to know before clicking. It is irreversible: you cannot turn it off, and going back to an unencrypted file means a backup and restore. If you lose the password, the documentation is clear: your encrypted data is unrecoverable, unless you keep a local copy, with which you can reset the key. Each new device must enter the password once, and it should differ from the server password. Enabling it requires a sync reset, and encryption costs a slight slowdown during sync. Finally, it covers only the budget data: bank-sync keys stay on the server side, and the device’s local data is not encrypted, hence the value of disk encryption.

Update Actual without losing your data

Updating means replacing the image, not the data. With Docker Compose: docker compose pull && docker compose up -d. With this guide’s docker run command, the documentation describes three steps: docker stop, then docker container rm on the container, then the same docker run relaunched with --pull=always. As long as the actual-budget volume is kept, the budget survives the container’s replacement. Before each update, make the backup from the previous section; this matters most if you use a nightly image to try bank sync, as the documentation recommends backing up before trying that branch. One known trap: when going back from a nightly image to a stable tag, you may hit “This budget cannot be loaded with this version of the app”; the restore page then points to a manual SQLite migration, which is best not improvised on the only copy of your data. After the update, open the app and check the last synced transaction and an account balance.

Troubleshooting: real messages and known causes

  • SharedArrayBufferMissing (fatal error on load) — no HTTPS, COOP/COEP headers missing or duplicated, or a browser that is too old. With Nginx, hide the upstream headers (proxy_hide_header Cross-Origin-Embedder-Policy; and proxy_hide_header Cross-Origin-Opener-Policy;) and set them once: require-corp and same-origin.
  • Enable Banking API error: status=403 body={"code":403,"message":"Application is not active"} — the Enable Banking application is not yet activated; the account list stays empty with no error in the interface (reported in issue no. 7799).
  • Application not found although the ID is correct — a leading space in the Application ID is enough to make the connection fail; remove it (a user asked in the issue for the space to be trimmed and the message clarified).
  • URL "app://actual/enablebanking/auth_callback" uses unsupported scheme — Enable Banking support only handles https redirects; it suggested an HTTPS redirect that then forwards to the app scheme.
  • Could not validate the credentials. Please check your Application ID and secret key. — reported only in Firefox with values that worked in Chromium; no workaround is recorded, try another browser for the initial setup.
  • Empty payees or payees identical to the notes, wrong card balance — these behaviours are reported in issue no. 7799 (for example the first balance returned, sometimes the available credit, shown in place of the booked debt). They are limits of the experimental implementation, not an error in your installation: compare with the bank statement.
  • PostError: network-failure, Wrong transactions period requested, several Enable Banking applications — reported in the same issue with no recorded fix. If you created a second application, unlink the accounts and put them all under one: syncing the accounts linked through the first one failed after the second was added.
  • Detailed logs — add DEBUG=actual-sensitive:enable-banking,actual-sensitive:bank-sync to the container environment, restart, reproduce the error, then remove the variable.

Never post “sensitive” logs as they are

The documentation warns that these logs may contain credentials, bank information or other private data: do not paste them into a public issue or on Discord. Also avoid DEBUG=* or DEBUG=actual*, which enable sensitive output too; for ordinary debugging, DEBUG=actual:* is enough. Review and redact the excerpt before passing it to the maintainer helping you, and restore the previous DEBUG value once the diagnosis is done.

Deploy Actual Budget on your VPS

Private, zero-based budgeting with no subscription — your financial data stays on your own server, synced across every device.