Actual Budget Setup: Host Your Own Privacy-First Finance Tracker in Docker

Step-by-step guide to deploying Actual Budget using Docker Compose. Take control of your personal finance, budget, and transactions securely.

Infrastructure Planning and Directory Structure

To deploy Actual Budget reliably, organize the application directories under /opt/actual-budget. This isolation simplifies backup tasks and environment management.

Run the following commands on your VPS to create the directories and set correct ownership:

sudo mkdir -p /opt/actual-budget/data
sudo chown -R 1000:1000 /opt/actual-budget/data

The container runs as an unprivileged user (UID/GID 1000) by default. Aligning host directory ownership prevents permission errors when the server writes configuration files and SQLite databases.

Docker Compose Configuration

Create a docker-compose.yml file inside /opt/actual-budget:

version: '3.8'

services:
  actual-server:
    image: actualbudget/actual-server:latest
    container_name: actual-server
    restart: unless-stopped
    ports:
      - "127.0.0.1:5006:5006"
    volumes:
      - ./data:/data
    environment:
      - ACTUAL_PORT=5006
      - ACTUAL_UPLOAD_DIR=/data
      - ACTUAL_TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16

Key Configuration Directives:

  • ports: Binding 127.0.0.1:5006:5006 restricts access to the container to localhost only. All external traffic must route through the reverse proxy.
  • volumes: Mounts the host's /opt/actual-budget/data directory to /data inside the container for persistent SQLite database storage.
  • ACTUAL_TRUSTED_PROXIES: Configures trusted upstream proxy IP ranges, ensuring headers like X-Forwarded-For are correctly interpreted.

Reverse Proxy Configuration

Actual Budget requires HTTPS for its Web Cryptography API client-side execution. Without SSL/TLS, end-to-end encryption and client synchronization will fail.

Option A: Nginx Configuration

Save this block as /etc/nginx/sites-available/budget.example.com (replace with your domain):

server {
    listen 80;
    server_name budget.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name budget.example.com;

    ssl_certificate /etc/letsencrypt/live/budget.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/budget.example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    client_max_body_size 20M;

    location / {
        proxy_pass http://127.0.0.1:5006;
        proxy_http_version 1.1;

        # Required for WebSockets (Sync engine)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        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;
    }
}

Option B: Caddyfile Configuration

If using Caddy, it handles certificate provision automatically. Add the following to your Caddyfile:

budget.example.com {
    reverse_proxy 127.0.0.1:5006 {
        header_up X-Forwarded-Proto {scheme}
    }
}

First Boot and E2E Sync Architecture

Once your proxy is configured, start the service:

cd /opt/actual-budget
docker compose up -d

Client-Server Sync Mechanism

Actual Budget utilizes a local-first design. All operations are executed on a local SQLite database in the browser.

  • Synchronization Protocol: The local client sends sync packages to the VPS server over HTTPS/WebSockets.
  • End-to-End Encryption (E2EE): Setting up a synchronization password is required on the first page load. The client uses this password to derive a local encryption key (via PBKDF2). This key encrypts all data before it leaves the client, ensuring the server only stores encrypted binary payloads.

Automated Backups

Since Actual Budget relies on SQLite databases, copying database files during active writes can cause corruption. The safest backup approach stops the container briefly before archiving the data.

Save the following backup script to /opt/actual-budget/backup.sh:

#!/bin/bash
BACKUP_DIR="/var/backups/actual-budget"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
mkdir -p "$BACKUP_DIR"

# Stop the container briefly to ensure database consistency
cd /opt/actual-budget
docker compose stop actual-server

# Create a compressed tarball of the data directory
tar -czf "$BACKUP_DIR/actual-budget-$TIMESTAMP.tar.gz" data

# Restart the container
docker compose start actual-server

# Delete backups older than 7 days
find "$BACKUP_DIR" -type f -name "*.tar.gz" -mtime +7 -delete

Make the script executable:

chmod +x /opt/actual-budget/backup.sh

Add a cron job to run the backup daily at 2:00 AM:

0 2 * * * /opt/actual-budget/backup.sh