Actual Budget is a local-first, envelope-budgeting app. This guide sets up its sync server in Docker behind Nginx and HTTPS, with nightly backups. Browsers require a secure origin for Actual's SharedArrayBuffer features; use HTTPS, not plain HTTP.
Prerequisites
| Item | Minimum | Recommended |
|---|---|---|
| VPS | 1 vCPU, 512 MB RAM, 10 GB NVMe | 1 vCPU, 1 GB RAM, 20 GB NVMe |
| OS | Ubuntu 24.04 LTS | Ubuntu 26.04 LTS |
| Domain | An A record for budget.example.com pointing to your VPS | Add an AAAA record if IPv6 is configured |
Create the VPS, set the DNS records and check dig +short budget.example.com returns its IP. Replace the domain and admin@example.com below with your own. Commands assume root access; use su - first if necessary.
Step 1: Prepare the server
ssh root@YOUR_VPS_IP
apt update && apt -y upgrade
apt -y install curl ca-certificates ufw nginx certbot python3-certbot-nginx
hostnamectl set-hostname budget
timedatectl set-timezone America/New_YorkOn a 512 MB plan, add swap:
fallocate -l 1G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstabAllow your SSH port before enabling UFW; if it is not 22, replace OpenSSH with your port.
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw --force enable
ufw statusStep 2: Install Docker
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
docker --version
docker compose versionDocker can bypass UFW for published ports. The app below binds only to 127.0.0.1; Nginx is its public entry point.
Step 3: Configure Actual
mkdir -p /opt/actual/data
cd /opt/actualCreate /opt/actual/docker-compose.yml:
services:
actual:
image: actualbudget/actual-server:${ACTUAL_VERSION:-latest}
container_name: actual
restart: unless-stopped
environment:
ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB: 50
ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB: 50
ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB: 50
ports:
- "127.0.0.1:5006:5006"
volumes:
- ./data:/data/opt/actual/data contains the account database and synced budget files. Create .env with a release tag from the Actual releases instead of latest for controlled production updates:
echo 'ACTUAL_VERSION=latest' > /opt/actual/.envStep 4: Start Actual
cd /opt/actual
docker compose pull
docker compose up -d
docker compose logs -f actualPress Ctrl+C once it is listening on port 5006. Check curl -sI http://127.0.0.1:5006 | head -n 1 for a successful response.
Step 5: Configure Nginx and HTTPS
Create /etc/nginx/sites-available/actual:
server {
listen 80;
listen [::]:80;
server_name budget.example.com;
client_max_body_size 50M;
location / {
proxy_pass http://127.0.0.1:5006;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}The Nginx body limit must match or exceed the Compose upload limits.
ln -s /etc/nginx/sites-available/actual /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
certbot --nginx -d budget.example.com --redirect -m admin@example.com --agree-tos -n
certbot renew --dry-runStep 6: Set a server password
Open https://budget.example.com immediately and set a strong server password. Before this step, anyone who reaches the site could claim the server. Create or import a budget (YNAB 4, nYNAB and Actual exports are supported). Under Settings > Show advanced settings > End-to-end encryption, set a separate encryption password and save it securely: the server cannot recover it.
Step 7: Connect devices
Open the same HTTPS address on each device, enter the server password and open the budget. Changes sync online while each device also works offline. On mobile, use Add to Home Screen. Optional bank connections through supported providers such as GoCardless or SimpleFIN require separate provider accounts.
Backups and restore
The data directory contains SQLite databases; stop Actual briefly for a consistent copy. Create /usr/local/bin/actual-backup:
#!/bin/bash
set -euo pipefail
DEST=/var/backups/actual
STAMP=$(date +%F-%H%M)
mkdir -p "$DEST"
cd /opt/actual
docker compose stop actual
trap 'docker compose start actual' EXIT
tar czf "$DEST/actual-data-$STAMP.tar.gz" -C /opt/actual data
docker compose start actual
trap - EXIT
chmod 600 "$DEST"/*
find "$DEST" -type f -mtime +14 -deletechmod 700 /usr/local/bin/actual-backup
/usr/local/bin/actual-backup && ls -lh /var/backups/actual
echo '15 3 * * * root /usr/local/bin/actual-backup' > /etc/cron.d/actual-backupCopy backups off the VPS regularly and occasionally export a budget from Settings > Export data. To restore a chosen archive:
cd /opt/actual
docker compose stop actual
mv data "data-before-restore-$(date +%F-%H%M)"
tar xzf /var/backups/actual/actual-data-STAMP.tar.gz -C /opt/actual
docker compose start actualUpdating Actual
Back up first, read the release notes, change ACTUAL_VERSION in .env, and run:
/usr/local/bin/actual-backup
cd /opt/actual
docker compose pull
docker compose up -d
docker image prune -fReload each browser after updating and keep Ubuntu patched with apt update && apt -y upgrade.
Troubleshooting
| Symptom | Check |
|---|---|
Blank page or SharedArrayBuffer error | Use HTTPS and check the certificate. |
| 502 Bad Gateway | Run docker compose ps and docker compose logs actual. |
| Sync fails with 413 / PayloadTooLarge | Raise client_max_body_size and the three ACTUAL_UPLOAD_* values together. |
| Password requested on each device | Expected: each device signs in with the server password. |
| Budget cannot open on a new device | Enter the separate end-to-end encryption password; the server cannot recover it. |
| Forgotten server password | Follow the official reset instructions. |
| Certbot challenge fails | Check DNS, ufw status and systemctl status nginx. |
