Skip to content
The English translation is still being built: some pages may lag behind the German ones.
Self-hosting

Self-hosting

samudo is one binary, samu-server, with SQLite files in a data directory. It serves the API, live events and the web UI on one port. These steps use Docker Compose; the binary runs just as well under systemd.

Before you start

  • Keep it private. The boundary is the sign-in: without an active account everything except /health is closed, passwords are hashed with Argon2, sessions are tokens. That is all. There is no rate limiting and it has not been reviewed for exposure to the internet. Run it inside a VPN or your home network, behind a reverse proxy that limits who may connect.
  • HTTPS. The session cookie is marked Secure; browsers accept it over plain HTTP only on localhost. Put a reverse proxy with TLS in front (Caddy, nginx, Traefik).
  • Resources are small: a few thousand tasks are computed in memory; half a CPU and 256 MB are plenty.

Build the image

There is no published image yet. Build one from the repository:

git clone <repository> samu && cd samu
docker build -f deploy/Dockerfile -t samu-server .

The image is Debian slim with samu-server and samu, running as user 10010, with its data in /data.

Compose

# /srv/samu/docker-compose.yml
name: samu
services:
  server:
    image: samu-server
    restart: unless-stopped
    volumes:
      - data:/data
      - ./export:/export
    ports:
      - "127.0.0.1:8490:8490"     # the reverse proxy connects here
    environment:
      SAMU_DATA: /data
      SAMU_LISTEN: 0.0.0.0:8490
      SAMU_SECRET_KEY: ${SAMU_SECRET_KEY:-}
    healthcheck:
      test: ["CMD", "samu", "--server", "http://127.0.0.1:8490", "health"]
      interval: 30s
volumes:
  data:
cd /srv/samu
(umask 077; echo "SAMU_SECRET_KEY=$(openssl rand -base64 32)" > .env)
docker compose up -d
curl -s http://127.0.0.1:8490/health        # {"status":"ok",...}

A reverse proxy entry, for example with Caddy:

samu.example.org {
	reverse_proxy 127.0.0.1:8490
}

Live updates use server-sent events on /events; the proxy must not buffer them (Caddy and nginx with proxy_buffering off are fine).

Server options

OptionEnvironmentDefault
--dataSAMU_DATA/dataserver.db (accounts, sessions) and accounts/<name>.db
--listenSAMU_LISTEN0.0.0.0:8490address and port
--secret-keySAMU_SECRET_KEYnoneseals calendar sources; see below
--blob-max-bytesSAMU_BLOB_MAX_BYTES50 MBlargest attachment; never more than 50 MB
--blob-quota-bytesSAMU_BLOB_QUOTA_BYTESnoneall attachments of one account together
--calendar-hostsSAMU_CALENDAR_HOSTSanyhosts calendars are fetched from, separated by commas

Accounts

Accounts are managed only on the server, never over HTTP:

docker compose exec -it server samu-server account add private
docker compose exec -it server samu-server account list
docker compose exec -it server samu-server account passwd private   # ends its sessions
docker compose exec server samu-server account disable private      # ends its sessions

Each account is its own database. Use one for private life and one for work rather than mixing them.

Then sign in from a client: the web UI at your address, the command line, or pair the Android app.

The secret key

Calendar sources (subscription addresses, CalDAV passwords) are stored encrypted in the account’s database. The key is SAMU_SECRET_KEY, 32 bytes in Base64. Without it no calendar is fetched. Back it up apart from the databases: whoever has both has the sources; whoever loses the key sets each source again (samu calendar source).

A public instance

A server anyone can reach should limit what a stranger can do with it:

  • Sign-ins are always braked: after five failed sign-ins of one account name in 15 minutes, that name waits until the 15 minutes are over (HTTP 429); a good sign-in forgets the failures. At most two passwords are checked at once. Twenty wrong pairing codes in 15 minutes hold pairing the same way. A restart forgets all of it.
  • Attachments: set SAMU_BLOB_MAX_BYTES and SAMU_BLOB_QUOTA_BYTES so the disk cannot fill up.
  • Calendars: with SAMU_SECRET_KEY the server fetches whatever address a user gives it as a calendar, also inside your own network. SAMU_CALENDAR_HOSTS=cal.example.org limits that to the hosts you name; redirects to other hosts are not followed.

Backup

Everything is in the volume: server.db and accounts/<name>.db, plus the files of attachments. Copy them with the server stopped, or while it runs with SQLite’s backup:

sqlite3 /path/to/volume/accounts/private.db ".backup '/backup/private.db'"

Back up before every update that brings a database migration.

Export and restore

An export is a plain directory you can read with a text editor, jq or git diff, without samudo:

samu export ~/samu-export --org                                  # from a client, signed in
docker compose exec server samu-server export --account private /export/private   # on the server, also while it runs

It holds manifest.json, one JSON file per kind of object (tasks.json, projects.json, comments.json, …), logbook.json, the files of attachments under files/<sha256>, a README.md describing all of it, and with --org a samu.org outline. The same state gives the same bytes, so the directory works well in git: export on a timer and commit, and you have a readable history. deploy/export-git.sh with the systemd units deploy/samu-export.service and .timer is one way to do that (adjust the paths and accounts at the top).

Not in an export: deleted objects, sessions and passwords, hooks, and the sources of calendars.

To restore, stop the server, create an empty account and read the export into it:

docker compose stop server
docker compose run --rm server account add private      # only if it does not exist
docker compose run --rm server restore --account private /export/private
docker compose start server

Then set each calendar source again.

Updating

Build the new image, back up, then docker compose up -d. The server migrates the databases on start; a database migrated by a newer version cannot be opened by an older one, so do not go back without the backup. /health reports the running version.

Webhooks

The server takes signed deliveries at /hooks/<name>, for example from a smart ring. See Capture from the Index ring.