Docs Getting started

Hosting on a cloud server

For SlowBooks Pro v2.31.4 · Download this guide (one HTML file) · Source on GitHub

One small Linux VPS, Docker, a reverse proxy with a real certificate, and backups that leave the machine. This is the whole recipe. It takes about an hour, and nothing in it is specific to SlowBooks except the last section.

Read the two-sentence licence note first, then the list of what the desktop build is not for.

Before you start

Whose books. LICENSE section 2 lets you run SlowBooks on a server for yourself, your business, or your own clients' books. Section 3(b) does not let you offer it to other people as a hosted service โ€” giving people outside your organization access to the interface or the API as the thing they pay for. A cloud box for your own company is the first case. Hosting it for strangers is the second, and this guide is not for that.

Not the desktop build. SlowBooksPro.exe --serve-lan and the macOS app serve plain HTTP on a trusted office network. They have no TLS and the docs say not to port-forward them; a VPN or an access-controlled tunnel to the office host is the way to reach one from outside (Server Edition setup, "Reaching it from outside the office"). On a cloud server run the Docker image, which is built for this: it refuses to start in debug mode, requires a real session secret and encryption key, and emits Secure cookies and HSTS.

What is at stake. A company file holds payroll records with Social Security numbers, bank feeds, customer addresses and your tax filings. Treat the server the way you would treat the filing cabinet those used to live in: one door, one key, and a copy somewhere else.

1. The server

Any provider with a plain Linux virtual machine works: DigitalOcean, Hetzner, Linode, Vultr, a small EC2 or Lightsail instance, a Compute Engine e2-small. SlowBooks is not demanding.

Size1 vCPU, 2 GB RAM, 25 GB disk is comfortable for one company and a handful of users; the stress-test company (two years of books, 1,900 documents) runs in well under 1 GB
OSUbuntu 24.04 LTS, which this guide uses. Debian 12 works too, with Docker installed from Docker's own repository (its docker-compose package is the retired v1)
RegionNear your users; the books do not care
CostRoughly $5 to $12 a month at the time of writing

Create it with an SSH key, not a password, and note its public IP.

Point a DNS name at that IP before you go further, because the certificate step needs one. books.example.com is the name used below; use your own.

2. Lock the door

Log in once as root and do these before installing anything.

# updates, a firewall, and an SSH lockout for brute force
apt update && apt upgrade -y
apt install -y ufw fail2ban unattended-upgrades
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw allow 80/tcp     # only so the proxy can get its certificate
ufw allow 443/tcp
ufw enable
systemctl enable --now fail2ban

# a normal user for everything after this; disable root logins
adduser books
usermod -aG sudo books
rsync --archive --chown=books:books ~/.ssh /home/books
sed -i 's/^#\?PermitRootLogin .*/PermitRootLogin no/; s/^#\?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart ssh
# Ubuntu's cloud images also drop settings into /etc/ssh/sshd_config.d/, and
# the first value wins: confirm with `sshd -T | grep -E 'permitrootlogin|passwordauthentication'`

From here on log in as books. If your provider offers a firewall in its control panel, set the same three ports there too; two layers cost nothing.

Port 3001 is never opened. The app only ever talks to the proxy on the machine's own network.

3. Docker and the source

sudo apt install -y docker.io docker-compose-v2 git
sudo usermod -aG docker books   # log out and in again afterwards
git clone https://github.com/VonHoltenCodes/SlowBooks-Pro-2026.git ~/slowbooks
cd ~/slowbooks
git checkout v2.31.3             # the latest release tag, never main

Pin a release tag (2.26.0 or later: earlier tags' production compose has no FORWARDED_ALLOW_IPS). main moves between gates; a tag is a build that three platforms passed.

4. Secrets and settings

The production compose file refuses to start without these. Generate them fresh on the server; do not reuse anything from a laptop or another install.

cd ~/slowbooks
cp .env.example .env
chmod 600 .env

# generate the two secrets and a database password
echo "PAYROLL_ENCRYPTION_SECRET=$(openssl rand -base64 32)" >> .env
echo "SESSION_SECRET_KEY=$(openssl rand -hex 32)"           >> .env
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)"            >> .env

Then open .env and set the rest by hand:

KeySet it to
POSTGRES_USER, POSTGRES_DBbookkeeper is fine for both
APP_DEBUGfalse
FORCE_HTTPStrue (the production default; writing it down is fine)
FORWARDED_ALLOW_IPSthe proxy's address or CIDR. For Caddy on the host as in step 6, 127.0.0.1. For a proxy in the compose network, that network's subnet: after step 5, docker network inspect slowbooks_default --format '{{(index .IPAM.Config 0).Subnet}}' prints it (the name is the folder's name plus _default). uvicorn then takes the client address and scheme from the proxy's X-Forwarded-* headers, and the login audit trail and rate limiter see the real client. Wrong or unset, every page redirects to itself forever. Never * or 0.0.0.0/0
APP_WORKERS1, unless you also run a Redis and set RATE_LIMIT_STORAGE_URI (HTTPS with a proxy); with the default of 4 workers and no shared store, the sign-in rate limit is four times looser than it reads
CORS_ALLOW_ORIGINShttps://books.example.com, your real name, no wildcard
SESSION_IDLE_TIMEOUT_SECONDS14400 (four hours) is the default; shorter is fine
EMPLOYER_EIN, EMPLOYER_STATE, SUTA_RATEyours, if you run payroll

PAYROLL_ENCRYPTION_SECRET encrypts the payroll and API-key columns at rest. Copy it somewhere safe now. A backup restored on a new server without this exact value cannot read those columns, and there is no recovery.

The production compose also expects a certificate pair for PostgreSQL at ./certs/postgres/. The database is only reachable from the app container on Docker's private network, so a self-signed pair is appropriate here:

mkdir -p certs/postgres
openssl req -x509 -nodes -newkey rsa:2048 -days 3650 \
  -subj "/CN=postgres" \
  -keyout certs/postgres/server.key -out certs/postgres/server.crt
sudo chown 70:70 certs/postgres/server.key   # the postgres user inside the alpine image
# openssl wrote the key readable by its owner only, which is what postgres wants

5. Start it

The production compose file publishes nothing to the host: the app is only reachable inside Docker's network. The proxy in the next step runs on the host, so give it a door that only the host can see โ€” port 3001 bound to loopback, never to the public interface:

cat > docker-compose.proxy.yml <<'YML'
services:
  slowbooks:
    ports:
      - "127.0.0.1:3001:3001"
YML
docker compose -f docker-compose.prod.yml -f docker-compose.proxy.yml up -d --build
docker compose -f docker-compose.prod.yml -f docker-compose.proxy.yml logs -f slowbooks

The log ends with the startup checks passing and uvicorn listening on 3001. If a check fails it says which setting is wrong and exits; fix .env and run up -d again. curl -sI http://127.0.0.1:3001/health answers 307 from the host (the redirect to HTTPS, which is the app working as set); from anywhere else the port does not exist, and the firewall would refuse it anyway.

6. The proxy and the certificate

Caddy is the least to get wrong. It obtains and renews the certificate itself and sets the forwarding headers the app needs.

Install Caddy from its own repository (Ubuntu's package is behind; the steps are at https://caddyserver.com/docs/install), then replace the Caddyfile it installed:

sudo tee /etc/caddy/Caddyfile >/dev/null <<'EOF'
books.example.com {
    reverse_proxy localhost:3001
}
EOF
sudo systemctl reload caddy

Wait a few seconds, then open https://books.example.com. You should see the first-run setup screen, and the browser's padlock should be clean. Do step 7 now, before anything else: until the first password is set, anyone who reaches the address could set it. nginx and Traefik are covered in HTTPS with a proxy if you already run one of those.

If you would rather run Caddy as a container, skip the loopback publish above, put Caddy on the compose network, and proxy to slowbooks:3001.

7. First run, and the users

Open the site, set the administrator's password, and enter the company. Then, under Settings โ†’ Users, add a named account for each person and stop sharing the first sign-in for daily work. Roles are administrator, bookkeeper, accountant and read-only; Server Edition setup describes what each can reach.

On a multi-user install the sign-in screen lists the user names. That is by design on a trusted network; on a public address it is the one thing you may want to weigh, since it tells a stranger who works there. If it matters to you, keep a single user and share the books by role elsewhere.

8. Backups that leave the machine

A backup on the same disk as the database is not a backup. Two layers:

Nightly database dump, copied off the box. Dump from the database container itself โ€” it has pg_dump and reaches the database over its own socket, so no password or host is needed. Save this as /home/books/backup.sh (the #!/bin/bash line must be the first line of the file: cron runs a script with sh otherwise, which stops at pipefail):

#!/bin/bash
set -euo pipefail
cd /home/books/slowbooks
mkdir -p /home/books/backups
docker compose -f docker-compose.prod.yml exec -T postgres \
  pg_dump --clean --if-exists -U bookkeeper bookkeeper | gzip > "/home/books/backups/slowbooks_$(date +%Y%m%d_%H%M%S).sql.gz"
# ship the newest one off the machine โ€” pick one:
#   rclone copy /home/books/backups remote:slowbooks-backups        (any cloud bucket)
#   scp /home/books/backups/*.gz you@home-nas:/backups/slowbooks/   (a machine you own)
find /home/books/backups -name '*.gz' -mtime +30 -delete
chmod +x ~/backup.sh
(crontab -l 2>/dev/null; echo "15 2 * * * /home/books/backup.sh >> /home/books/backup.log 2>&1") | crontab -

Restoring, on a stack that has run once (the app's first start migrates and seeds the database, so the dump has to replace what is there; the --clean --if-exists above makes it do that, and ON_ERROR_STOP makes a failure say so instead of exiting 0 with the old data in place):

cd /home/books/slowbooks
docker compose -f docker-compose.prod.yml stop slowbooks
gunzip -c /home/books/backups/the-file.sql.gz | \
  docker compose -f docker-compose.prod.yml exec -T postgres psql -v ON_ERROR_STOP=1 -U bookkeeper bookkeeper
docker compose -f docker-compose.prod.yml start slowbooks

A dump made without --clean restores only into an empty database: drop and recreate it first (dropdb/createdb -U bookkeeper bookkeeper in the postgres container). Uploaded receipts, attachments and the logo are in the database from 2.18, so the dump carries them; the slowbooks_uploads volume holds what earlier versions wrote, which 2.18's first start copies in. Keep it mounted.

Provider snapshots of the whole disk, weekly, from the control panel. They are cheap and they restore the proxy and the secrets along with the data.

Test a restore once, on a scratch server, before you need it. The steps are in Operations runbook. Remember the encryption secret.

9. Keep it current

cd ~/slowbooks
git fetch --tags
git checkout v2.32.0          # the new tag
docker compose -f docker-compose.prod.yml -f docker-compose.proxy.yml up -d --build

The app migrates the database on start. Take a backup first; a release never needs one, and the one time it does you will be glad. The Docker image is built and started in CI on every change, and every release is gated on Linux as well as Windows and macOS.

sudo apt update && sudo apt upgrade -y     # monthly, for the host itself

What this gives you

In transitHTTPS from Caddy with an automatically renewed certificate; HSTS from the app
At restPayroll and API-key columns encrypted with your key; PostgreSQL over TLS inside Docker
Doors22, 80, 443 open; SSH by key only; fail2ban; the app and the database unreachable from outside
SessionsSecure, SameSite=Strict cookie; four-hour idle timeout on a 30-day hard expiry; rate-limited sign-in, every attempt recorded
RecoveryA nightly dump off the box, a weekly disk snapshot, and a restore you have tried once

What it does not give you

  • Two-factor sign-in. SlowBooks does not have it. The password and the key-only SSH are the perimeter. If you want a second factor, put the site behind a VPN (Tailscale is the easiest) or an identity-aware proxy such as Cloudflare Access, and open 443 only to that.
  • A managed service. Nobody is watching this server but you. The provider's uptime alerts and the backup log are your monitoring.
  • Compliance paperwork. HIPAA assessment is honest about the gaps if you need to answer to an auditor.

If you would rather not expose the site to the internet at all, the VPN option above is a good answer for a small company: the server has no public web port, and every user's laptop is on the private network. Everything else in this guide still applies.