DockerSelf-Hosting

Actual Budget with Docker: self-hosted envelope budgeting (Linux, Windows, Mac)

Before you start

  • 20 min read

Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#

Actual Budget is a free, open-source envelope budgeting app that you host yourself: you give every unit of money a job in a category, and a small server keeps one budget in sync across your browser and desktop apps.

Actual Budget official logo

Logo: the official Actual Budget project (github.com/actualbudget/actual)

The project calls itself "a local-first personal finance tool". Your data lives on each device and the server only passes changes around, so the app works without a network. The project is active and has not been archived or renamed. The old actual-server repository was merged into the main one in February 2025, and the original paid hosted service closed in 2024, so ignore guides that mention either.

WhatValue (official docs, read November 2026)
Current versionActual 26.11.0, released November 2026 (latest stable)
Image to pinactualbudget/actual-server:26.11.0 on Docker Hub (the same image as latest in November 2026)
Time neededAbout 15 minutes for the Compose route (our estimate, not measured)
Download sizeAbout 116 MiB compressed (amd64) on Docker Hub; the -alpine variant is about 66 MiB
RAMNot documented by the project; measure with docker stats
PortTCP 5006 (web app and sync server)
LicenceMIT (LICENSE.txt at the release tag)
Official docsactualbudget.org/docs and the repository

Screenshot of the Actual Budget budget screen with a category table, monthly To Budget amounts and an account list in the sidebar

Image: the official Actual Budget project (README at v26.11.0). It shows an older look, so 26.11.0 may differ slightly.

At a glance:

  • What it is: envelope (zero-sum) budgeting. Categories are the envelopes, and "To Budget" is the money still without a job.
  • Server: one Docker container, one folder of data, one password. You set the password in the browser on first start.
  • HTTPS is required for the browser app unless you open it as localhost. The docs say Actual cannot run without it.
  • End-to-end encryption: optional, one-way, and you cannot recover your data if you forget the password.
  • Bank sync: optional and third-party. You bring your own provider account, and the provider's tokens are not covered by the encryption.
  • Mobile apps are deprecated. Use the web app, which you can install on a phone as a web app.
  • Privacy: the project's privacy policy says nothing is collected automatically and there is no tracking.

How it fits together#

Each device keeps its own copy of the budget and syncs to your server over HTTPS. Bank sync is the only part that talks to someone else.

Two-lane diagram: a browser, installed web app or desktop app with its own local copy of the budget connects over HTTPS through Caddy on port 443 to the Actual server container on port 5006, which saves to a data folder on the host; a dashed arrow shows the optional fetch from a bank sync provider outside your server

Drawn for this guide from the official docs and compose file. Dashed parts are optional.

Before you start#

You need a computer to run the server, Docker with Compose, and a modern browser.

SystemWhat to installNotes
Linux serverDocker Engine and the Compose pluginx86_64 or arm64 for the main image. The -alpine image also lists arm v7 and v6
WindowsDocker DesktopServer and browser can be the same PC
MacDocker DesktopIntel and Apple Silicon both have an image

Check Docker on any system:

Run in terminalbash
docker --version
docker compose version

The docs ask for a recent Chrome, Firefox, Safari or Edge, because Actual needs a browser feature called SharedArrayBuffer. To install Docker, see the official Docker install pages or our Docker on Ubuntu guide.

Checklist:

  • [ ] Docker and Compose work (docker compose version)
  • [ ] Port 5006 is free on the server (ss -ltn | grep 5006 on Linux prints nothing)
  • [ ] You know where your backups will go (Step 5 and "Updates and backups")
  • [ ] For the HTTPS section only: a DNS name for the server, and ports 80 and 443 free

Tested by DIY Lab Hub: not yet. The commands come from the official docs, the official compose file and the published image's own files, which we read without running them. We did not start Actual.

Step 1: Create the data folder#

Actual keeps everything in one folder, which the container sees as /data. We use ~/docker_volumes/actual, as all DIY Lab Hub guides do.

Linux#

Run in terminalbash
mkdir -p ~/docker_volumes/actual/{data,backups}
chmod 700 ~/docker_volumes/actual
cd ~/docker_volumes/actual

Windows (PowerShell)#

Codepowershell
New-Item -ItemType Directory -Force "$HOME\docker_volumes\actual\data","$HOME\docker_volumes\actual\backups" | Out-Null
Set-Location "$HOME\docker_volumes\actual"

Mac (Terminal)#

Same commands as Linux:

Run in terminalbash
mkdir -p ~/docker_volumes/actual/{data,backups}
chmod 700 ~/docker_volumes/actual
cd ~/docker_volumes/actual

Check it worked. The data folder exists and is empty. The server creates server-files and user-files inside it on first start.

Tip: Keep this folder on a local disk. A network share (NFS) can break SQLite, the database Actual uses. A maintainer said exactly that in issue 4426.

Step 2: Write the Compose file and start Actual#

This is the official compose file with three changes: the image tag is pinned to 26.11.0 (the docs suggest latest), the data goes to ./data, and port 5006 is published on 127.0.0.1 only, so nobody else on your network can reach the password screen before you do. The health check is the official one.

Linux#

Run in terminalbash
cd ~/docker_volumes/actual
cat > docker-compose.yml << 'EOF'
services:
  actual_server:
    image: actualbudget/actual-server:26.11.0
    container_name: actual_server
    restart: unless-stopped
    ports:
      - "127.0.0.1:5006:5006"
    volumes:
      - ./data:/data
    healthcheck:
      test: ['CMD-SHELL', 'node scripts/health-check.js']
      interval: 60s
      timeout: 10s
      retries: 3
      start_period: 20s
EOF
docker compose up -d
docker compose ps
docker compose logs --tail=50 actual_server

Windows (PowerShell)#

Codepowershell
Set-Location "$HOME\docker_volumes\actual"
@'
services:
  actual_server:
    image: actualbudget/actual-server:26.11.0
    container_name: actual_server
    restart: unless-stopped
    ports:
      - "127.0.0.1:5006:5006"
    volumes:
      - ./data:/data
    healthcheck:
      test: ['CMD-SHELL', 'node scripts/health-check.js']
      interval: 60s
      timeout: 10s
      retries: 3
      start_period: 20s
'@ | Set-Content -Encoding ascii docker-compose.yml
docker compose up -d
docker compose ps
docker compose logs --tail=50 actual_server

Mac (Terminal)#

Same commands as Linux.

Check it worked. docker compose ps lists actual_server as running, and its status turns healthy after the first health check. The docs publish no expected log text, so read the log for errors. You can also ask the health endpoint (the health check expects the status UP):

Run in terminalbash
curl -s http://127.0.0.1:5006/health

On Windows use curl.exe instead of curl.

Tip: The Docker Hub name is actualbudget/actual-server. GitHub's registry uses ghcr.io/actualbudget/actual. Both are the same software, so pick one and keep it.

Step 3: Open Actual and set the server password#

On a fresh start there is no password yet, so Actual asks you to create one. Do this straight away.

Linux#

On the server, open http://localhost:5006 in a browser. If the server has no screen, open an SSH tunnel from your own computer and leave it running:

Run in terminalbash
ssh -N -L 5006:127.0.0.1:5006 <your-user>@<your-server-address>

Then open http://localhost:5006 on that computer.

Windows (PowerShell)#

If Docker runs on this PC:

Codepowershell
Start-Process "http://localhost:5006"

For a Linux server, run the same ssh -N -L 5006:127.0.0.1:5006 <your-user>@<your-server-address> tunnel in PowerShell, then open http://localhost:5006.

Mac (Terminal)#

Run in terminalbash
open http://localhost:5006

For a remote server, use the SSH tunnel command from the Linux part.

Choose a long, unique password and store it in a password manager. If you forget it, you can reset it without losing data (see Troubleshooting).

Check it worked. After you set the password you see your budget list, and Actual creates your first budget file for you.

Tip: localhost counts as a safe address for the browser, so plain HTTP is fine there. Opening http://<server-ip>:5006 from another computer is not, and fails with a SharedArrayBuffer message. Use the tunnel or the HTTPS section.

Step 4: Add your accounts and install the apps you use#

Add an account with + Add account at the bottom of the sidebar. For an existing account, start with a correct opening balance on a recent date. The FAQ recommends this before you set up bank sync, because only newer transactions then arrive and reconciliation stays easy.

The browser is a full client. You can also install the web page on a device, and on a phone the installed page works offline.

Linux#

Use the browser, or the desktop app from actualbudget.org/download: Flathub, or an AppImage for x64 or arm64.

Windows (PowerShell)#

Use the browser, or install the desktop app from the Microsoft Store or the manual x64 or arm64 download on the same page.

Mac (Terminal)#

Use the browser, or download the desktop app for Intel or Apple Silicon from the same page.

In the desktop app, choose your own server on the "Where's the server?" screen and enter its URL. Do not use "Set up sync" in the desktop app: the docs warn that its built-in server beside your own can cause version mismatches.

Check it worked. A second device that logs in with the server password lists the same budget and downloads it.

Step 5: Turn on end-to-end encryption (optional)#

Encryption means the server only holds data it cannot read. It is a one-way decision, so read this before you click. It happens inside the app, so it is the same on every system.

Linux#

Open the budget in your browser or the desktop app.

Windows (PowerShell)#

Open the budget in your browser or the desktop app. No PowerShell command is needed.

Mac (Terminal)#

Open the budget in your browser or the desktop app. No Terminal command is needed.

QuestionAnswer from the docs
Where do I enable it?Open the budget, go to Settings, then the Encryption section and choose enable encryption
Which password?A second one, different from the server password
Can I recover a forgotten password?No. With a local copy you can reset the key, which does a sync reset and makes a new key
Can I turn it off?No. Export and re-import into a new, unencrypted file instead
What is not covered?The copy on each device stays unencrypted, and bank sync tokens are not covered

Check it worked. A new device asks for the encryption password after the server password when it downloads the budget.

Tip: Write the encryption password into your password manager before you click enable. The server cannot help you if it is lost.

How envelope budgeting works in Actual#

The docs explain it in three ideas:

  1. Each category is a virtual envelope. You move money into it from "To Budget".
  2. Zero-sum: assign all income, so "To Budget" reaches zero and every unit has a job. Fixed bills such as rent get an envelope too.
  3. Credit cards are accounts holding negative money. A purchase lowers the category you spent from, and paying the card does not change your budget.

Read the project's envelope budgeting page and Starting Fresh guide next.

Optional: connect your bank#

Bank sync is the one feature that leaves your server, so it is off until you set it up.

ProviderRegion (per the docs)Note
SimpleFIN BridgeNorth AmericaThe docs quote a paid plan, $1.50 a month or $15 a year
Enable BankingEuropeListed in the current docs
GoCardless BankAccountDataEuropeNot accepting new accounts
AkahuNew ZealandListed in the current docs
Pluggy.aiBrazilListed in the current docs

You create the provider account yourself and paste its token into Actual (for SimpleFIN: + Add account, then Link bank account with SimpleFIN). The token is stored on your server, and end-to-end encryption does not cover it.

Version 26.11 added automatic sync. It is off by default, runs only while Actual is open, and the shortest custom interval is 15 minutes. A private SimpleFIN bridge on your own network is blocked unless you list it in ACTUAL_SIMPLEFIN_ALLOWED_NETWORKS.

Tip: Skip old history on the first bank sync: set the opening balance on a recent date, then sync.

Put it behind HTTPS (with or without Traefik)#

The docs say HTTPS is needed to safely use all of Actual's features, and the browser app will not run without it unless you use localhost. Both routes give you https://budget.<your-domain> with no host port. Single-machine users can skip this section.

Both routes change the Compose file from Step 2: remove the ports: block and join the shared proxy network. Create it once:

Run in terminalbash
docker network create proxy

The command is the same on Windows and Mac, and an "already exists" error is harmless.

With Traefik#

On a server that already runs Traefik v3 with the Docker provider, add labels and remove the host port. This assumes an external network called proxy and an entrypoint called https; the official examples name it websecure, so use yours.

Replace docker-compose.yml with:

File contentyaml
services:
  actual_server:
    image: actualbudget/actual-server:26.11.0
    container_name: actual_server
    restart: unless-stopped
    volumes:
      - ./data:/data
    healthcheck:
      test: ['CMD-SHELL', 'node scripts/health-check.js']
      interval: 60s
      timeout: 10s
      retries: 3
      start_period: 20s
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.actual.rule=Host(`budget.<your-domain>`)"
      - "traefik.http.routers.actual.entrypoints=https"
      - "traefik.http.routers.actual.tls=true"
      - "traefik.http.services.actual.loadbalancer.server.port=5006"

networks:
  proxy:
    external: true

If your Traefik issues certificates through a named resolver, add the label traefik.http.routers.actual.tls.certresolver=<your-resolver>. Then run docker compose up -d in ~/docker_volumes/actual.

Check it worked. curl -I https://budget.<your-domain>/ returns an HTTP status line over HTTPS, and the browser shows a valid certificate.

Without Traefik: Caddy#

Choose this if you have no Traefik. Caddy gets and renews its own certificates. For a public domain it uses Let's Encrypt. At home, with no public domain, tls internal makes Caddy sign a local certificate. Our Caddy guide covers it in depth.

Create the Caddy folders and put the Actual Compose file from the Traefik example in place, but without the labels: block:

Run in terminalbash
mkdir -p ~/docker_volumes/caddy/{conf,data,config}
cd ~/docker_volumes/caddy

On Windows use New-Item -ItemType Directory -Force "$HOME\docker_volumes\caddy\conf","$HOME\docker_volumes\caddy\data","$HOME\docker_volumes\caddy\config". Save this as ~/docker_volumes/caddy/docker-compose.yml. It mounts the conf folder, not a single file, because Caddy's Docker docs warn that single-file mounts can break reloads. 2.11.8 is the newest Caddy image tag we found in November 2026:

File contentyaml
services:
  caddy:
    image: caddy:2.11.8
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80/tcp"
      - "443:443/tcp"
      - "443:443/udp"
    volumes:
      - ./conf:/etc/caddy
      - ./data:/data
      - ./config:/config
    networks:
      - proxy

networks:
  proxy:
    external: true

Save one of these as ~/docker_volumes/caddy/conf/Caddyfile. The first is for a public domain (the docs' own example adds encode gzip zstd):

File contenttext
budget.example.com {
    encode gzip zstd
    reverse_proxy actual_server:5006
}

The second is for a home-only name. .home.arpa is reserved for home networks, so point it at your server in your router, your DNS or a hosts file:

File contenttext
budget.home.arpa {
    tls internal
    encode gzip zstd
    reverse_proxy actual_server:5006
}

Start both stacks:

Run in terminalbash
cd ~/docker_volumes/caddy && docker compose up -d
cd ~/docker_volumes/actual && docker compose up -d

For tls internal, each device must trust Caddy's root certificate. Caddy's docs describe copying it out of the container. Run the copy on the server, then install the file on each device you own:

Linux#

Run in terminalbash
cd ~/docker_volumes/caddy
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt /tmp/caddy-root.crt
sudo cp /tmp/caddy-root.crt /usr/local/share/ca-certificates/caddy-local-root.crt
sudo update-ca-certificates

Windows (PowerShell)#

Run PowerShell as administrator:

Codepowershell
Set-Location "$HOME\docker_volumes\caddy"
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt "$env:TEMP\caddy-root.crt"
certutil -addstore -f "ROOT" "$env:TEMP\caddy-root.crt"

Mac (Terminal)#

Run in terminalbash
cd ~/docker_volumes/caddy
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt /tmp/caddy-root.crt
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/caddy-root.crt

Firefox and Chrome may keep their own certificate store, so import root.crt there too. For a phone, copy the file over and install it as a CA certificate. Trust it only on devices you own.

Check it worked. curl -I https://budget.example.com/ returns a status line over HTTPS. For a home name, the browser shows a padlock once the root is trusted. Open the site by the name in the Caddyfile, not by IP address.

Tip: Let's Encrypt needs your DNS record to point at the server and ports 80 and 443 to reach it. A certificate error usually means the Caddyfile name differs from the DNS record.

Security and testing#

ControlHow
Server passwordLong and unique. Set it first, before anything else can reach port 5006
PortKeep 127.0.0.1:5006, or no host port behind a proxy. Never forward 5006 on your router
EncryptionOptional end-to-end encryption (Step 5), kept in a password manager
Bank tokensNot covered by end-to-end encryption, so protect the server and backups
Single sign-onOptional OpenID: the first user to log in with it becomes the server owner, permanently. Our authentik guide shows one provider
BackupsContain your whole budget: chmod 600, never in a shared folder

Tests after each change:

Run in terminalbash
cd ~/docker_volumes/actual
docker compose ps
docker compose logs --tail=100 actual_server
curl -I https://budget.<your-domain>/

On Windows use curl.exe. Remote access without opening ports is possible with a private VPN such as our wg-easy guide.

What leaves the machine: nothing by default. The privacy policy (effective January 2024) says the project collects nothing automatically. We did not capture network traffic. Bank sync contacts your provider, and Caddy or Traefik contacts Let's Encrypt for a public domain.

Common mistakes#

These come from the official docs, GitHub issues and common AI assistant answers.

MistakeWhy it happensDo this instead
Opening http://<server-ip>:5006 from another computerActual needs HTTPS or localhost for SharedArrayBuffer; issue 1667 shows the confusion, and issue 7747 asks for a clearer warningUse localhost via an SSH tunnel, or the HTTPS section
Trying the nightly or edge tag, then going back to latestA newer build migrates the budget database. Issue 6819 (closed January 2026) reports Error: Missing migration file, and the maintainer said you cannot go backUse one or the other. Keep a backup before any non-release tag
Putting /data on an NFS shareNFS and SQLite do not play well together. Issue 4426 (closed February 2025) is a Docker NFS volume failingUse a local folder or bind mount
Hitting 413 Content Too Large or PayloadTooLargeErrorActual limits uploads to 20 MB (50 MB for encrypted sync), and your proxy has its own limit. Issue 6326 (closed December 2025) reported a 413 on /sync/sync; the maintainer pointed to the FAQ, though that reporter's own cause was a browser language settingRaise the ACTUAL_UPLOAD_* variables and the proxy's body limit, as the FAQ says, and check the browser console
Running as a non-root user without fixing ownershipA root-owned file in a user-owned folder gives EACCES: permission denied, open '/data/.migrate' (issue 2321, closed February 2024)Give the same user the whole data folder, or leave the image's default user
Forgetting the encryption passwordThe server never holds it. The docs say data cannot be recoveredStore it in a password manager; keep a local copy and an export
Thinking encryption protects bank tokensThe current docs say bank sync tokens are stored separately on the server, unencrypted by E2EEProtect the server and backups; use self-hosting only if you trust it
Copying the Traefik example from the docsIt sets certResolver: le but defines letsencryptUse this guide's labels or fix the name
AI assistants: adding an ACTUAL_PASSWORD variableAssistants copy a pattern from other apps. The image has no such variable; the password is set in the browserSet it on first start, or reset it with the script below
AI assistants: recommending the edge tag, the image actualbudget/actual, or PUID/PGIDOld or invented answers. The live docs say nightly, the Docker Hub image is actualbudget/actual-server, and its entrypoint is a plain init programPin 26.11.0 and use the compose file here

Tip: Before you paste any compose file, check three lines: the image name, the tag and the port. Most failed installs start there.

Tip: Back up before every update, and update by changing the tag, not by trying a different channel.

Tip: When the page does not load, suspect the browser address first. Try localhost through a tunnel before you change the container.

Troubleshooting#

ProblemSolution
Page will not load, or a SharedArrayBuffer messageUse localhost or HTTPS, and a recent Chrome, Firefox, Safari or Edge. The docs list all three conditions
Container shows unhealthydocker inspect --format '{{json .State.Health}}' actual_server, then read docker compose logs --tail=100 actual_server
Forgot the server passwordReset it with the command below. No data is lost
PayloadTooLargeError: request entity too large or 413Raise ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB and the sync size limits, and the proxy's limit
Devices stop syncing, or "Syncing has been reset"Choose the right device and use Reset Sync in Settings, or revert the other devices when asked
Budget file keeps growingReset Sync, or export and re-import: both squeeze the history into one file
This budget cannot be loaded with this version of the app. Make sure the app is up-to-date.Seen after going from edge to a stable release. Follow the docs' migration repair, after you have exported every budget
Config option ignoredAdd DEBUG=actual:config to environment: and read the log

Resetting the server password#

The docs run a script inside the container. It asks for the new password twice:

Run in terminalbash
cd ~/docker_volumes/actual
docker exec -it actual_server node /app/src/scripts/reset-password.js

The docs open a shell first and run the same script; this is the one-line form. Everyone logged in must sign in again.

Known bugs (open, November 2026)#

  • Issue 9423: changes are not immediately synced between browser tabs (opened early November 2026).
  • Issue 9415: an arithmetic error message, "safeNumber: can't safely perform arithmetic with number" (opened early November 2026).

Run docker compose ps, docker compose logs --tail=200 actual_server and docker inspect actual_server first.

Updates and backups#

Update. Read the release notes, back up, change the tag in docker-compose.yml, then:

Run in terminalbash
cd ~/docker_volumes/actual
docker compose pull
docker compose up -d
docker compose logs --tail=100 actual_server

Windows and Mac use the same commands. Update the desktop apps too: a device with an old version may report "Update Required".

Back up, two ways. First, export from the app: Settings, then Data, then Export data. That zip is what the docs tell you to restore from. Second, copy the server folder. Stopping the container first gives a consistent copy of the database.

Linux#

Run in terminalbash
cat > ~/docker_volumes/actual/backup.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd ~/docker_volumes/actual
docker compose down
mkdir -p backups
tar -czf "backups/actual-$(date +%F).tar.gz" data
chmod 600 backups/actual-*.tar.gz
docker compose up -d
EOF
chmod +x ~/docker_volumes/actual/backup.sh

To run it nightly at 03:00, run crontab -e and add this line:

File contenttext
0 3 * * * /bin/bash $HOME/docker_volumes/actual/backup.sh

Windows (PowerShell)#

Codepowershell
Set-Location "$HOME\docker_volumes\actual"
docker compose down
New-Item -ItemType Directory -Force backups | Out-Null
tar -czf "backups\actual-$(Get-Date -Format yyyy-MM-dd).tar.gz" data
docker compose up -d

Save these lines as backup.ps1 and schedule it at 03:00 in Task Scheduler (a daily task that runs powershell.exe -NoProfile -File <path-to-backup.ps1>).

Mac (Terminal)#

Same script and cron line as Linux.

Restore test. Unpack one archive into a new empty folder, never over the live one, and check what is there:

Run in terminalbash
mkdir -p /tmp/restore-test && tar -xzf ~/docker_volumes/actual/backups/actual-<date>.tar.gz -C /tmp/restore-test
ls /tmp/restore-test/data

You should see server-files and user-files. To restore an app export, close the budget, choose Import file on the budget list, pick Actual and select the zip. Keep a copy on another machine; our restic guide automates that.

Performance and resources#

The docs publish no RAM or CPU figures, so this guide gives none. Measure your own server:

  • Live use: docker stats actual_server shows CPU and memory.
  • Image size: about 116 MiB to download for amd64. The docs call the smaller -alpine image good for low-powered devices.
  • Large budgets: the budget file grows because every change is stored. Reset Sync shrinks it. Release 26.11 lists speed-ups for opening large budgets.
  • Limits: uploads are 20 MB, 20 MB for sync files and 50 MB for encrypted sync files by default.

FAQ#

Is Actual Budget free? Yes, under the MIT licence. Self-hosting needs only your own server.

Is there a phone app? The docs call the mobile apps deprecated. Install the web app on your phone through HTTPS instead.

Can several people share one server? Yes, with an OpenID provider. The docs warn that two people editing the same budget at the same time can conflict.

Does it send my data to the project? The privacy policy says no data is collected automatically. Bank sync sends data to the provider you choose.

Can I skip the reverse proxy? Yes, on one machine through localhost or an SSH tunnel. Other devices need HTTPS.

Sources#

Checked against the official docs on 2026-11-05.