1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
# 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/` |
| `var/run` | `/run/bokfd` | Unix socket (mode 0660, owned by uid 10001) |
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='<admin-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:<tag>`: 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='<pw>' 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`).
## Mock company
```sh
docker compose exec -e BOKFD_PASSWORD='<pw>' 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
```sh
docker compose exec -e BOKFD_PASSWORD='<pw>' bokfd \
bokfctl --user admin backup.snapshot
ls var/db/backup # <db>-<timestamp>.db + .sha256
```
Restore:
```sh
docker compose stop
cp var/db/backup/<snapshot>.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.
|