Skip to content

How to Deploy Actual Budget on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Sep 2026
We earn commissions when you shop through the links below. Full disclosure →

Self-host the Actual Budget sync server on a small VPS: one container, one data folder, and the HTTPS step the app will not run without.

Before you start
  • A small VPS: 1 vCPU / 1 GB RAM is plenty
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain you can point at the server. HTTPS is required, not optional
  • Docker Engine + Compose installed (see the base guide below)
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Actual Budget is

Actual Budget is a local-first envelope budgeting app under the MIT licence. "Local-first" is literal: your budget lives in a SQLite database inside the browser or desktop app, and every change is applied there first. The piece you host is the sync server, a small Node.js service that stores each budget file and relays changes between your devices. It also serves the web app itself, so one container gives you both.

That design shapes the whole deploy. The server is not doing heavy work, so it fits on the smallest box you can buy. But the web app runs its database engine in your browser, and browsers only allow that on a secure origin. Without HTTPS, Actual does not start. Most of the care in this guide goes into that one step.

If you are choosing between budgeting tools, Actual Budget vs Firefly III covers the trade-off: Actual is envelope budgeting you do by hand, and Firefly III is double-entry bookkeeping with rules and imports.

Server sizing

The catalog lists 512 MB RAM as the minimum. On our test box (GCP e2-standard-2, Ubuntu 26.04, Docker 29.8.1) the idle container measured about 241 MB of RAM and about 505 MB of disk for the image and data.

  • 1 vCPU / 1 GB RAM is enough for a household, with room for Caddy in front.
  • Disk is set by the size of your budget files, which are small. 10–20 GB covers the OS, the image and years of history.

Put it on a reliable host. This box holds your financial history, so a provider with dependable uptime and disk snapshots is worth more here than a rock-bottom plan.

Prepare the server

This guide assumes Docker Engine and the Compose plugin are installed, along with a non-root user and a ufw firewall. If not, work through Docker & Compose on Ubuntu first.

Open SSH and the reverse proxy ports only. Actual's own port stays on the loopback interface:

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo ufw status verbose
Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Install Actual (Docker Compose)

Upstream publishes the image as actualbudget/actual-server on Docker Hub and as ghcr.io/actualbudget/actual on GitHub's registry. The latest tag is the most recent stable release and is what the docs recommend for most people.

Create a project folder:

mkdir -p ~/actual && cd ~/actual

Write the compose file. It is the one from the Actual repository, with the port bound to 127.0.0.1 so that only the reverse proxy can reach it:

cat > docker-compose.yml <<'YAML'
services:
  actual_server:
    image: docker.io/actualbudget/actual-server:latest
    container_name: actual_server
    restart: unless-stopped
    ports:
      # Loopback only: Caddy is the only way in from outside.
      - "127.0.0.1:5006:5006"
    volumes:
      - ./actual-data:/data
    healthcheck:
      test: ["CMD-SHELL", "node scripts/health-check.js"]
      interval: 60s
      timeout: 10s
      retries: 3
      start_period: 20s
YAML

The ./actual-data folder is the whole application state. The server creates server-files (the account database, including your server password) and user-files (the synced budget files) inside it.

Start it and wait for the server to answer:

docker compose up -d
timeout 120 bash -c 'until curl -fsS -o /dev/null http://127.0.0.1:5006/; do sleep 3; done'
docker compose ps

At this point the server is running but only reachable from the box itself. That is intended.

HTTPS + domain (required)

Actual's upstream docs are blunt about this: the app needs a browser feature called SharedArrayBuffer, browsers only enable it on HTTPS pages, and Actual will not run unless the server meets those conditions. The server already sends the other two headers it needs (Cross-Origin-Embedder-Policy and Cross-Origin-Opener-Policy), so a valid certificate is the only thing you have to add.

Point an A record such as budget.example.com at the server's public IP and wait for it to resolve. Then put Caddy in front of the loopback port, as in Automatic HTTPS with Caddy:

budget.example.com {
    encode gzip zstd
    reverse_proxy 127.0.0.1:5006
}

This matches the Caddy example in Actual's reverse-proxy docs. The only change is the upstream address, because Caddy here runs on the host. If you run Caddy as a container instead, 127.0.0.1 is the container's own loopback. Put both services in one compose file, proxy to actual_server:5006, and drop the host port mapping.

On nginx, do not add the COOP/COEP headers yourself. Actual sets them. If nginx adds them as well, the browser sees duplicate headers, rejects the policy, and the app stops with a SharedArrayBufferMissing error. Upstream's fix is proxy_hide_header for those two headers, so that one source sets them.

First run: set the server password

Open https://budget.example.com. On a fresh server, Actual first asks you to create the server password. That password gates access to every budget on the server. There are no user accounts to manage, and no open signup to close.

Then create a budget, or import one. Actual's docs cover importing from YNAB 4, nYNAB and another Actual instance.

Two settings are worth making on day one:

  • End-to-end encryption. In a budget's settings, "enable encryption" asks for a second password and encrypts the file before it leaves your device, so the server only stores data it cannot read. Upstream's warning is plain: forget that password and the data cannot be recovered, and encryption cannot be turned off again later. Use a password manager.
  • Connect your other devices to the same server URL. Each device keeps a full local copy and syncs through the server.

Securing it

  • Keep the port on loopback. The 127.0.0.1:5006 mapping means the server is reachable only through Caddy and its certificate.
  • Use a long server password. It is the only login on the server.
  • Leave ACTUAL_LOGIN_METHOD at its default unless you know you need something else. The header method logs in anyone who can send an x-actual-password header, which upstream flags as advanced with "security implications". OpenID login exists but is marked as a preview.
  • Forgot the server password? It can be reset without touching your budgets. Run docker exec -it actual_server /bin/sh, then run node /app/src/scripts/reset-password.js inside the container and answer the prompts.

Backups

Two layers, because the app is local-first.

Server-side: the whole server is the ./actual-data folder. Stop the container for a clean copy of the SQLite files:

cd ~/actual
docker compose stop
tar czf actual-data-$(date +%F).tar.gz actual-data
docker compose start

Copy the archive off the box. Restoring is the same folder put back in place, followed by docker compose up -d. The container writes those files as root, so replacing or deleting the folder needs sudo.

In-app: under Settings, the Export section's Export Data button downloads a copy of the open budget. It is a portable backup that does not depend on this server at all. Make one before any upgrade you are nervous about.

Upgrades

cd ~/actual
docker compose pull
docker compose up -d

This is the update procedure from the Actual docs. latest follows stable releases. The nightly tag tracks the development branch, and upstream asks anyone running it to keep backups, so stay on latest for a budget you rely on. Take the server-side backup above first.

Troubleshooting

The page loads, then shows a fatal error about SharedArrayBuffer. You are on plain HTTP, or on a proxy that duplicates the COOP/COEP headers. Load the site over https:// with a valid certificate. On nginx, hide the upstream headers as described above.

"Cannot reach server" from a device. The server URL on that device must match exactly: https://budget.example.com, with no port. Check that DNS resolves to the box and that ports 80 and 443 are open in ufw and in any cloud firewall.

The container shows unhealthy. Read docker compose logs actual_server. The usual cause is a data folder the container cannot write to. Check ownership of ./actual-data.

Upload fails on a large budget. The server limits sync uploads to 20 MB by default. Raise ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB (and the matching ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB) in the compose file's environment. Or use "Reset Sync" under the advanced settings, which upstream notes compacts the file.

Verification + next steps

You're done when you can load https://budget.example.com over a valid certificate, sign in with the server password, open the same budget on a second device, and see a change on one appear on the other. Then make an off-box copy of actual-data and check that it contains server-files and user-files.

From there: turn on end-to-end encryption if the server is shared or hosted by a provider you don't fully trust, and schedule the backup with cron. If you want investment tracking next to the budget, Ghostfolio fits beside it on the same box. For full double-entry bookkeeping, see Deploy Firefly III on a VPS.

Next steps

How to self-host Actual Budget →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.