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
/healthis 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 onlocalhost. 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
| Option | Environment | Default | |
|---|---|---|---|
--data | SAMU_DATA | /data | server.db (accounts, sessions) and accounts/<name>.db |
--listen | SAMU_LISTEN | 0.0.0.0:8490 | address and port |
--secret-key | SAMU_SECRET_KEY | none | seals calendar sources; see below |
--blob-max-bytes | SAMU_BLOB_MAX_BYTES | 50 MB | largest attachment; never more than 50 MB |
--blob-quota-bytes | SAMU_BLOB_QUOTA_BYTES | none | all attachments of one account together |
--calendar-hosts | SAMU_CALENDAR_HOSTS | any | hosts 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 sessionsEach 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_BYTESandSAMU_BLOB_QUOTA_BYTESso the disk cannot fill up. - Calendars: with
SAMU_SECRET_KEYthe server fetches whatever address a user gives it as a calendar, also inside your own network.SAMU_CALENDAR_HOSTS=cal.example.orglimits 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 runsIt 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 serverThen 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.