Skip to content
FeaturesStart free trial
Self-hosting opens with the public launch. Until then the install script, the container image and the files this guide refers to are not public, so the steps below will not work yet. Sumhour Cloud is open now, with a 30-day free trial.

Self-hosting Sumhour

Sumhour runs anywhere Docker runs. Self-hosting is free and has every feature: there are no plans, trials or limits on your own server. You need a machine with 1 GB of memory, Docker with Compose v2, and a domain name if people outside your network will use it.

Terminal window
curl -fsSL https://raw.githubusercontent.com/sumhour/sumhour/main/deploy/selfhost/install.sh | sh -s -- --url https://time.example.com

The installer makes a sumhour folder with compose.yml, backup.sh, restore.sh and a .env holding fresh secrets. It then starts three containers (the app, the background worker and Postgres 17) and waits until Sumhour answers. Options:

Option Default What it does
--url http://localhost:3000 The address people open. Sign-in links and passkeys depend on it.
--port 3000 The port on this machine.
--dir ./sumhour Where the files go.

Running the installer again keeps your .env and your data. It pulls the newest images and restarts, which makes it an upgrade too.

  1. Download deploy/selfhost/compose.yml and deploy/selfhost/env.example into an empty folder.
  2. Copy env.example to .env and fill in APP_URL, AUTH_SECRET and POSTGRES_PASSWORD. For the two secrets, openssl rand -hex 32 gives a good value.
  3. Run docker compose up -d. Migrations run on every start, so the database is ready the first time.

Open your address and sign up with your email. Sumhour signs people in with emailed links or passkeys; there are no passwords. Until email is set up, each link is written to the app log:

Terminal window
docker compose logs app | grep -o 'http[^ ]*magic-link[^ ]*' | tail -n 1

Once you are in, add a passkey under your name, then Sign-in and security. After that you can sign in without email at all.

Set one of these in .env and restart with docker compose up -d:

  • SMTP_URL=smtps://user:[email protected]:465 works with any mail provider.
  • RESEND_API_KEY=re_... uses Resend.

Set EMAIL_FROM to an address your provider allows you to send from, such as Sumhour <[email protected]>.

Put a reverse proxy in front of port 3000 and set APP_URL to the https address. With Caddy, which gets certificates by itself, the whole configuration is:

time.example.com {
reverse_proxy localhost:3000
}

Sumhour trusts forwarded headers only from proxies on the same machine or a private network, so a proxy on the same host or in the same Docker network works without settings.

./backup.sh saves the database and the uploaded files (logos and receipts) into backups/. It checks that the database dump can be read and keeps the newest 14 of each (KEEP=30 ./backup.sh keeps more). Run it every night with cron, and copy the backups folder to another machine:

15 3 * * * cd /opt/sumhour && ./backup.sh >> backups/backup.log 2>&1

To restore, run it with the two files. This replaces the current data, so it asks you to type restore first:

Terminal window
./restore.sh backups/sumhour-db-20261001-031500.dump backups/sumhour-files-20261001-031500.tar.gz

Keep your .env with the backups. Without the same AUTH_SECRET, restored workspaces lose their connected integrations and everyone has to sign in again.

Terminal window
./backup.sh
docker compose pull
docker compose up -d

SUMHOUR_VERSION=latest in .env follows each release. To upgrade only when you decide, set it to a version such as 0.1.0 and change it when you are ready. Release notes are in CHANGELOG.md.

The standard setup runs the background jobs (reminders, recurring invoices, imports, webhooks) in their own worker container. Platforms that run a single container can set WORKER=inline on the app instead, and the jobs run inside it. Never run both.

The app runs as one process, which uses one CPU core. On a server with spare cores, set WEB_CONCURRENCY in .env to the number of cores you can give it, then run docker compose up -d. The processes share the port. Each one takes about 60 MB of memory when idle and up to 300 MB when busy. With WORKER=inline, the background jobs still run once, in the first process. Leave it at 1 on a small server.

Add deploy/unraid/sumhour.xml as a template in Community Applications. It runs the app and the jobs in one container and needs a Postgres 17 database; install one from Community Applications, create a sumhour database, and enter its address as the Database URL.

Add https://raw.githubusercontent.com/sumhour/sumhour/main/deploy/portainer/templates.json under Settings, App Templates. The Sumhour template deploys deploy/selfhost/compose.yml as a stack and asks for the address and the two secrets.

On Render, render.yaml in the repository creates the app (with the jobs inside), a Postgres database and a small disk. Render fills in APP_URL with its own address; set yours when you add a domain. On DigitalOcean App Platform, .do/deploy.template.yaml does the same with a development database.

Setting Turns on
ANTHROPIC_API_KEY The in-app assistant.
QBO_CLIENT_ID, QBO_CLIENT_SECRET Sending invoices to QuickBooks Online (your own Intuit app).
XERO_CLIENT_ID, XERO_CLIENT_SECRET Sending invoices to Xero (your own Xero app).
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET Time suggestions from Google Calendar, and “Continue with Google” on the sign-in pages (add the redirect URI <APP_URL>/api/auth/callback/google).
MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET “Continue with Microsoft” on the sign-in pages: an Entra app registration for organizational and personal accounts, redirect URI <APP_URL>/api/auth/callback/microsoft, optional ID-token claims email and xms_edov.

Every other variable is described in .env.example at the root of the repository.

  • docker compose ps shows whether all three containers are up.
  • docker compose logs app --tail 100 shows the app’s last messages, including why it would not start (a missing or too short AUTH_SECRET is the usual one).
  • curl http://localhost:3000/healthz answers {"ok":true,...} with the running version.