aboutsummaryrefslogtreecommitdiff
path: root/docs/DEPLOY.md
blob: 74f395355520f8c220a523a83874f90f9f90146d (plain)
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# 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='<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`).

## 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='<owner-pw>' bokfd \
    bokfctl --user admin user.create '{"username":"revisor", ...}'
docker compose exec -e BOKFD_PASSWORD='<owner-pw>' 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='<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

`backup.snapshot` makes a consistent copy of the live WAL database with
`VACUUM INTO`; the result lands in `var/db/backup/` as
`bokfd-<timestamp>.db` (the response carries its SHA-256). Interactive:

```sh
docker compose exec -e BOKFD_PASSWORD='<pw>' 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='<pw>' 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/<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.