Deployment¶
kasas is a single static binary in a tiny container, designed to be boring to run and forget. This page covers the common deployment shapes.
Container image¶
Multi-arch (amd64/arm64) images are published to GHCR on every release, built
FROM scratch — ~12 MB pulled, ~24 MB on disk for linux/amd64 (the embedded WASM
dashboard adds ~5 MB).
The image's HEALTHCHECK runs kasas healthcheck
(the scratch base has no shell or curl).
Docker Compose¶
The bundled docker-compose.yml mounts ./data and reads the setup token from the
environment:
export KASAS_SIMPLEFIN_SETUP_TOKEN="<your base64 setup token>"
docker compose up -d # or: up -d --build to build locally
The access URL is persisted to ./data/secrets.json on first sync, so the token is
consumed once. Swap the Compose build: for image: ghcr.io/paulmeier/kasas:latest
to run the published image.
Volume permissions¶
The container runs as a non-root user, UID 65532. The mounted data directory
must be writable by that user:
Postgres¶
kasas runs on SQLite (default) or Postgres with the same binary, selected at runtime — see Data Model → multi-dialect storage. Point it at a server and it creates its schema on first start:
KASAS_DATABASE_DRIVER=postgres \
KASAS_DATABASE_DSN="postgres://user:pass@host:5432/kasas?sslmode=disable" \
kasas serve
The Compose file includes an optional Postgres service behind a profile:
# set KASAS_DATABASE_DRIVER=postgres + KASAS_DATABASE_DSN in docker-compose.yml, then:
docker compose --profile postgres up -d
Moving an existing SQLite ledger to Postgres¶
Pointing kasas at a fresh Postgres database creates an empty schema — it does not carry your SQLite data across on its own. To bring an existing ledger with you, run the built-in migration once:
It copies every table — accounts, transactions, rules, events, history, settings,
and the rest — into the (empty) Postgres database with ids preserved, reading the
SQLite file without modifying it. Then set database.driver=postgres and
database.dsn and restart. The same thing is available from the dashboard under
Settings → Migrate to Postgres. See the
migration guide for the full walkthrough and caveats.
Vault¶
Instead of the local 0600 secrets.json, store the SimpleFIN access URL and the
dashboard token in HashiCorp Vault (KV v2). Enable [vault] (see
Configuration → [vault]):
[vault]
enabled = true
address = "https://vault.example.com:8200" # or VAULT_ADDR
token = "…" # or VAULT_TOKEN
mount = "secret"
path = "kasas"
kasas reads and writes both secrets under one KV path, merging keys so setting one doesn't disturb the other.
Tailscale¶
kasas has a single shared-secret auth model and no per-user accounts, so the strongest posture is to keep it off the public internet and reach it over a private network. Tailscale is an easy fit: run kasas bound to its tailnet address (or behind a Tailscale sidecar/serve) and only your devices can reach it.
A tailnet address is still 'beyond loopback'
kasas treats any non-loopback bind — including a Tailscale CGNAT
(100.64.0.0/10) address — as exposed, so it refuses to start there without
a dashboard token unless you set
server.allow_unauthenticated = true (KASAS_SERVER_ALLOW_UNAUTHENTICATED=true).
Setting a token is the recommended path: it secures the dangerous admin
operations too, and the in-place updater (when
update.allow_apply is on) is then both tailnet-only and token-gated.
Unraid¶
Paul self-hosts kasas on Unraid. Point the data volume at the appdata share and match ownership to the container's UID:
Map the container's /data to /mnt/user/appdata/kasas, publish port 8080, and
set KASAS_SIMPLEFIN_SETUP_TOKEN (first run) and KASAS_DASHBOARD_TOKEN. For
Docker deployments, set KASAS_UPDATE_CHECK=false and update by pulling a new
image (below).
Updating¶
Pull the new image and recreate the container:
Set KASAS_UPDATE_CHECK=false to suppress the outbound release check, since
you update by pulling.
Every release publishes static binaries (linux/darwin × amd64/arm64) with SHA-256 checksums. The binary updates itself in place:
kasas self-update # download, verify, replace
kasas self-update -check # report only; install nothing
It verifies the download against the published .sha256 and refuses on a
mismatch. Restart afterwards to run the new version. See the
CLI reference.
When a newer release is available the dashboard shows an "Update & restart"
banner. Clicking it calls POST /api/v1/update, which performs the same
download → verify → replace and then re-execs the new binary in place (no
external supervisor); the page reloads onto the new version.
Securing the in-place updater
update.allow_apply is off by default. Turn it on to let the dashboard/API
replace the running binary with a checksum-verified GitHub release and restart
it. Even when on, the apply lives in the admin tier: it requires the
dashboard token and is refused (503) on an
unsecured instance. Keep kasas on a trusted network (e.g. Tailscale)
too, or leave allow_apply off and use the kasas self-update CLI to upgrade.
While serve runs, kasas also checks once a day for a newer release and logs a
notice — it never self-modifies from the check. Builds without a release version
(e.g. dev) skip the check entirely.