Personal Budgeting
    Encrypted Sync

    Deploy Actual Budget on a VPS

    Self-host Actual Budget's sync server on a RamNode VPS with Docker Compose, Nginx HTTPS, and nightly backups.

    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

    ItemMinimumRecommended
    VPS1 vCPU, 512 MB RAM, 10 GB NVMe1 vCPU, 1 GB RAM, 20 GB NVMe
    OSUbuntu 24.04 LTSUbuntu 26.04 LTS
    DomainAn A record for budget.example.com pointing to your VPSAdd 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

    shell
    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_York

    On a 512 MB plan, add swap:

    shell
    fallocate -l 1G /swapfile
    chmod 600 /swapfile
    mkswap /swapfile
    swapon /swapfile
    echo '/swapfile none swap sw 0 0' >> /etc/fstab

    Allow your SSH port before enabling UFW; if it is not 22, replace OpenSSH with your port.

    shell
    ufw allow OpenSSH
    ufw allow 'Nginx Full'
    ufw --force enable
    ufw status

    Step 2: Install Docker

    shell
    curl -fsSL https://get.docker.com | sh
    systemctl enable --now docker
    docker --version
    docker compose version

    Docker 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

    shell
    mkdir -p /opt/actual/data
    cd /opt/actual

    Create /opt/actual/docker-compose.yml:

    shell
    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:

    shell
    echo 'ACTUAL_VERSION=latest' > /opt/actual/.env

    Step 4: Start Actual

    shell
    cd /opt/actual
    docker compose pull
    docker compose up -d
    docker compose logs -f actual

    Press 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:

    shell
    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.

    shell
    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-run

    Step 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:

    shell
    #!/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 -delete
    shell
    chmod 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-backup

    Copy backups off the VPS regularly and occasionally export a budget from Settings > Export data. To restore a chosen archive:

    shell
    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 actual

    Updating Actual

    Back up first, read the release notes, change ACTUAL_VERSION in .env, and run:

    shell
    /usr/local/bin/actual-backup
    cd /opt/actual
    docker compose pull
    docker compose up -d
    docker image prune -f

    Reload each browser after updating and keep Ubuntu patched with apt update && apt -y upgrade.

    Troubleshooting

    SymptomCheck
    Blank page or SharedArrayBuffer errorUse HTTPS and check the certificate.
    502 Bad GatewayRun docker compose ps and docker compose logs actual.
    Sync fails with 413 / PayloadTooLargeRaise client_max_body_size and the three ACTUAL_UPLOAD_* values together.
    Password requested on each deviceExpected: each device signs in with the server password.
    Budget cannot open on a new deviceEnter the separate end-to-end encryption password; the server cannot recover it.
    Forgotten server passwordFollow the official reset instructions.
    Certbot challenge failsCheck DNS, ufw status and systemctl status nginx.