# bokf — running in Docker Status: Draft 0.1 · 2026-09-17 · License: GPL-3.0-or-later Two roles: - **host** — runs Docker + the compose plugin only. No toolchain, no registry. - **dev machine** — has the source and builds the image; deploys with `scripts/deploy.sh` over SSH. There is no forge or registry in this flow. Releases are git tags, images are transferred directly with `docker save | ssh docker load`. State lives in two bind mounts next to `compose.yaml`: | Host path | Container | Contents | |---|---|---| | `var/db` | `/var/lib/bokfd` | SQLite database, `backup/`, `export/`, `certs/` | | `var/run` | `/run/bokfd` | Unix socket (mode 0660, owned by uid 10001) | The `bokfd` service publishes port 8788 for the TLS listener; the `certs` sidecar renews the certificate with lego via INWX DNS-01. Plain TCP stays off (loopback-only if enabled); the Unix socket is for clients on the host. The database is a single SQLite file. Back up with `backup.snapshot` (`VACUUM INTO`) and point restic at `var/db/backup` — never at the live file. ## First run On the dev machine, put the SSH target in `.env`: ```sh cp .env.example .env # set BOKF_HOST=user@host and BOKF_REMOTE_DIR=/srv/bokf scripts/deploy.sh v0.1.0 ``` The first run stops after shipping the image and compose.yaml, and prints the one-time init command. Run it on the host: ```sh cd /srv/bokf docker compose run --rm -e BOKFD_PASSWORD='' bokfd init docker compose up -d docker compose ps # wait for "healthy" ``` `init` creates the admin user and must run before the first `up`; it refuses to touch an already initialized database. Subsequent `scripts/deploy.sh` runs update the image, tag and compose file, restart the daemon and wait for the healthcheck. ## Deploying upgrades ```sh git tag v0.1.1 scripts/deploy.sh # tag defaults to git describe scripts/deploy.sh v0.1.1 # or pass one explicitly ``` The script: 1. `make` + `make test` on the dev machine, 2. builds `bokf:`: locally and ships it with `docker save | gzip | ssh docker load`, or — when the host runs a different CPU architecture — builds it natively on the host from a source tar, 3. copies `compose.yaml` and writes `BOKF_IMAGE`/`BOKF_TAG` into the host's `.env` (other keys are preserved), 4. `docker compose up -d --no-build`, then polls the container healthcheck, 5. on failure, puts the previous `BOKF_TAG` back and rolls back to the image that is still loaded on the host. Cross-architecture builds are automatic: `uname -m` is compared over SSH and a mismatch switches to a remote build. Override with `BOKF_BUILD=local` or `BOKF_BUILD=remote` (also settable in `.env`). A remote build pulls the Debian base image inside a container, so the host needs outbound network access but still no toolchain. Tags are `git describe` output unless passed. Tag releases (`v*`) so rollback and support have meaningful versions. The rollback image must still exist on the host; don't prune before the new version has proven itself. Manual rollback — set `BOKF_TAG` on the host and restart: ```sh cd /srv/bokf sed -i 's/^BOKF_TAG=.*/BOKF_TAG=v0.1.0/' .env docker compose up -d --no-build ``` ## Optional: a bare repository on the host For an off-machine copy of the source and an optional auto-deploy hook: ```sh ssh host 'git init --bare /srv/git/bokf.git' git remote add host ssh://host/srv/git/bokf.git git push host main ``` If the host also has the toolchain and a checkout whose origin is that bare repo, `deploy/post-receive.sample` can build, test and restart on every push to `main`. The normal `scripts/deploy.sh` flow does not need any of this. ## Clients Run clients inside the container — no host toolchain needed: ```sh docker compose exec bokfd bokftui # interactive TUI docker compose exec -e BOKFD_PASSWORD='' bokfd \ bokfctl --user admin fiscal_year.list ``` The clients honor `BOKFD_SOCKET`; a host-installed client can also point at `var/run/bokfd.sock`, but that file is owned by uid 10001, so the host user must be in that group (or use `sudo`). The TUI remembers the server and user (never the password) in `$XDG_CONFIG_HOME/bokf/tui.conf` (`~/.config/bokf/tui.conf`), written after a successful login. Precedence: `--socket`/`--user`, then `BOKFD_SOCKET`/ `BOKFD_USER`, then that file. For credentials from a Bitwarden/Vaultwarden item, use the user-space launcher — nothing runs as root: ```sh install -m 755 scripts/bokftui-bw ~/.local/bin/bokftui-bw BOKF_BW_ITEM=bokf bokftui-bw ``` `bokftui-bw` works with `rbw` (its agent keeps the vault unlocked for the session) or the official Bitwarden CLI `bw`, where the session key is cached in `~/.cache/bokf/bw-session` (mode 0600) so the master password is only asked when the session expires. On Node < 19 the launcher adds `--experimental-global-webcrypto` when the SDK's WASM crypto needs it. It reads the item's password, or the custom field named by `BOKF_BW_FIELD`, and execs the TUI. Prefer a scoped, revocable API token over the account password: create one on the host, put it in a custom field (e.g. `token`) of the item, then: ```sh docker compose exec -e BOKFD_PASSWORD='' bokfd \ bokfctl --user admin --org 1 token.create \ '{"label":"tui-laptop","scopes":["read","write","admin"]}' BOKF_BW_KIND=token BOKF_BW_FIELD=token BOKF_BW_ITEM=bokf bokftui-bw ``` With `BOKFD_TOKEN` set the TUI logs in without showing the login screen. Revoke the token with `token.revoke` when a machine goes away. ## TLS and external users Put the ACME DNS credentials in the host's `.env` once (they are never committed): ```sh LEGO_DOMAIN=bokf.makandra.eu LEGO_EMAIL=anders@makandra.eu INWX_USERNAME=... INWX_PASSWORD=... ``` Forward port 8788 on the router to the host. The `certs` sidecar obtains and renews the certificate into `var/db/certs/certificates/`; `bokfd` reloads it in place. On the very first `up`, `bokfd` may restart a few times until the certificate exists — check `docker compose logs certs`. Clients outside the LAN connect with the DNS name: ```sh export BOKFD_SOCKET=tls:bokf.makandra.eu:8788 bokftui ``` No CA override is needed with a public ACME certificate (the system trust store suffices); `BOKFD_TLS_CA=/path/ca.pem` exists for private CAs. To invite someone, create an account and give it only the role and org it needs — no VPN, no SSH: ```sh docker compose exec -e BOKFD_PASSWORD='' bokfd \ bokfctl --user admin user.create '{"username":"revisor", ...}' docker compose exec -e BOKFD_PASSWORD='' bokfd \ bokfctl --user admin org.member_add '{"org":1,"username":"revisor","role":"viewer"}' ``` Agents use `token.create` instead of a password; tokens are scoped and revocable. Only the TLS port is forwarded, and every command still requires authentication (`meta` and `health` excepted). ## Mock company ```sh docker compose exec -e BOKFD_PASSWORD='' bokfd \ bokfctl --user admin org.create \ '{"name":"Mock AB","org_nr":"556000-0000","fiscal_year_start_month":1}' ``` Develop against this org; agents get their own API token (`bokfctl token.create ...`, shown once). ## Backup and restore `backup.snapshot` makes a consistent copy of the live WAL database with `VACUUM INTO`; the result lands in `var/db/backup/` as `bokfd-.db` (the response carries its SHA-256). Interactive: ```sh docker compose exec -e BOKFD_PASSWORD='' bokfd \ bokfctl --user admin backup.snapshot ls var/db/backup ``` For unattended backups (restic, cron) use an admin-scoped token instead of the password, created once: ```sh docker compose exec -e BOKFD_PASSWORD='' bokfd \ bokfctl --user admin token.create '{"label":"backup","scopes":["admin"]}' # store the token (shown once) in a root-only file, e.g. /etc/bokf/bokfd-backup.env: # BOKFD_TOKEN=bokf_... ``` Then, before every restic run: take a snapshot through the daemon and let restic read only the snapshots — never the live database: ```sh set -a . /etc/bokf/bokfd-backup.env set +a docker compose --project-directory /mnt/data/bokf exec -T -e BOKFD_TOKEN \ bokfd bokfctl backup.snapshot >/dev/null find /mnt/data/bokf/var/db/backup -name 'bokfd-*.db' -mtime +14 -delete restic backup --exclude='/mnt/data/bokf/var/db/bokfd.db*' \ --exclude='/mnt/data/bokf/var/run' /mnt/data/bokf ``` The WAL and shared-memory files (`bokfd.db-wal`, `bokfd.db-shm`) must be excluded together with the main file; an inconsistent copy of a live SQLite database is worse than no backup. Restore: ```sh docker compose stop cp var/db/backup/.db var/db/bokfd.db rm -f var/db/bokfd.db-wal var/db/bokfd.db-shm docker compose start ``` To replace the mock with a real database from another host, restore its snapshot the same way (same or newer bokf version; older schemas migrate forward). SIE import is the alternative once the importer's CRLF/`#RAR` fixes land. ## Local development ```sh make -j"$(nproc)" && make test # no Docker required docker compose up --build # same image, local var/ data dir ``` `scripts/deploy.sh` always runs the tests before shipping. `var/`, `.env`, `*.db` and `*.se` are gitignored — real books never enter the repository.