source: Klonkt/deploy/MULTI-INSTANCE.md@ 0f09302

main
Last change on this file since 0f09302 was 0f09302, checked in by Robin <roboburr@…>, 3 weeks ago

reset-admin wijst een instantie aan, en maakt nooit een database aan

In de gedeelde opstelling van deploy/klonkt@.service werkte het noodpad niet.
De code staat daar op /opt/klonkt en de configuratie per instantie in
/var/lib/klonkt/<slug>/.env, waar systemd hem leest via EnvironmentFile. Een
script dat je zelf start krijgt die omgeving niet: dotenv zoekt naast de code,
vindt niets, en draait zonder DATABASE_PATH.

Gemeten op een nagebouwde opstelling: 'npm run reset-admin' legde een lege
database in /opt/klonkt/storage/ -- de gedeelde codemap, die volgens de unit
juist read-only is -- en klapte daarna op 'no such table: users'. Een
stacktrace op de plek waar een aanwijzing hoort.

Drie dingen:

  • --instance <slug> leest dezelfde .env als systemd, met override, want wie de instantie aanwijst hoort te winnen van een DATABASE_PATH die nog in de shell hangt van een vorige.
  • De controle staat VOOR de import van src/config/database.js, want die maakt map en bestand aan zodra hij geladen wordt. Ontbreekt de database, dan stopt hij en noemt het pad; hij maakt er geen.
  • deploy/MULTI-INSTANCE.md noemt het commando nu, met de reden erbij. De hulppagina in de app zegt 'vanuit de projectmap', en dat klopt alleen bij een eigen checkout per instantie.

Zes tests, waaronder de geslaagde weg: het getoonde wachtwoord moet echt op de
gebruiker passen. Controleproef gedraaid -- zonder de wacht valt de test om die
eist dat er niets wordt aangemaakt.

Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 8.5 KB
Line 
1# Running several Klonkts on one server
2
3One copy of the code, one directory of data per site. Adding a site is a
4directory and an `.env` file; updating is one command for all of them.
5
6```
7/opt/klonkt/ the code. Shared. Replaceable. No user data.
8/var/lib/klonkt/<slug>/ one instance: its .env, database, uploads.
9 .env
10 database.sqlite
11 media/
12 audio/
13```
14
15A *slug* is just a short name for an instance: `boiert`, `blog`, `label`. It is
16the directory name under `/var/lib/klonkt` and the systemd instance name
17(`klonkt@boiert`). It never appears in a URL.
18
19New installs get this layout on their own; the installer derives the slug from
20the domain (`boiert.eu` becomes `boiert`) unless you set `KLONKT_SLUG`. Servers
21installed before this existed keep their old layout untouched until you convert
22them, which is the section below.
23
24## Why the split
25
26When the database and uploads sit inside the checkout, the code directory holds
27live user data. That has three consequences you feel sooner or later:
28
29- **Updates get risky.** Anything that cleans up the checkout can reach your
30 uploads. Deleting and re-cloning the code becomes impossible.
31- **Backups get vague.** You either back up the code as well or you have to pick
32 the data out from between it.
33- **A second site needs a second copy of everything**, including `node_modules`,
34 and every copy has to be updated separately.
35
36After the split the checkout is disposable: throw it away, clone it again, and
37every instance still has its data. Backing up `/var/lib/klonkt/<slug>` backs up
38the whole instance, configuration included.
39
40## Who owns what
41
42Everything runs as the unprivileged `klonkt` system user, which is created by
43the installer and cannot log in.
44
45| Path | Owner | Mode | Written by |
46|---|---|---|---|
47| `/opt/klonkt` | `klonkt` | `0755` | `klonkt-update`, never the running app |
48| `/var/lib/klonkt/<slug>` | `klonkt` | `0750` | the app |
49| `/var/lib/klonkt/<slug>/.env` | `klonkt` | `0600` | you |
50
51Root is needed to install the systemd unit, create the directory and edit the
52web server config. The application itself never runs as root.
53
54The service unit narrows this further:
55
56```ini
57ProtectSystem=strict
58ReadWritePaths=/var/lib/klonkt/%i
59```
60
61The whole filesystem is read-only to the process except its own data directory.
62An instance cannot write into the code, and it cannot reach another instance's
63data, even if something inside the application goes wrong.
64
65## Migrating an existing install
66
67For a server that was installed the single site way, with everything under
68`/opt/klonkt`. Update the code first: the split needs a build where every media
69subdirectory follows `MEDIA_PATH`, and the script refuses to run on anything
70older.
71
72```bash
73klonkt-update
74sudo bash /opt/klonkt/scripts/klonkt-migrate-data.sh <slug> --dry-run
75sudo bash /opt/klonkt/scripts/klonkt-migrate-data.sh <slug>
76```
77
78Pick a slug that describes the site, for example `boiert` for boiert.eu. The
79script stops the service (so SQLite writes out its log), moves `storage/` and
80`.env` to `/var/lib/klonkt/<slug>/`, rewrites the three data paths, installs the
81systemd template, and switches from `klonkt.service` to `klonkt@<slug>`.
82
83Your web server config does not change. The instance keeps the same port,
84because the port comes from the same `.env`.
85
86**The old unit is moved aside, not deleted.** It ends up next to its old place
87as `klonkt.service.retired-<timestamp>`. Stopping and disabling is not enough:
88`systemctl restart klonkt` starts a disabled unit anyway, which is exactly what
89an updater generated before the split does. A resurrected `klonkt.service` no
90longer finds its `.env` (that moved with the data), falls back to the built-in
91defaults, and writes a fresh empty database into the checkout. Masking does not
92work here either, because the unit file sits in `/etc/systemd/system` and
93`systemctl mask` refuses while a real file is there.
94
95**Rolling back.** Move the data into `/opt/klonkt/storage`, restore the relative
96paths in `.env`, move the retired unit file back to
97`/etc/systemd/system/klonkt.service`, then:
98
99```bash
100sudo systemctl daemon-reload
101sudo systemctl enable --now klonkt
102```
103
104## Adding an instance
105
106```bash
107sudo bash /opt/klonkt/scripts/klonkt-add-instance.sh blog blog.example.com
108```
109
110That creates `/var/lib/klonkt/blog`, writes an `.env` with a fresh random
111`SESSION_SECRET` and a free port, starts `klonkt@blog`, and adds a Caddy block
112for the domain. Pass a port as a third argument to choose it yourself, or
113`--no-caddy` if you run your own proxy.
114
115Point the DNS at the server first, otherwise the certificate cannot be issued.
116
117Doing it by hand comes down to the same four things:
118
119```bash
120sudo mkdir -p /var/lib/klonkt/blog
121sudo tee /var/lib/klonkt/blog/.env >/dev/null <<'ENV'
122NODE_ENV=production
123PORT=3001
124HOST=127.0.0.1
125SESSION_SECRET=<openssl rand -hex 32>
126PUBLIC_BASE_URL=https://blog.example.com
127DATABASE_PATH=/var/lib/klonkt/blog/database.sqlite
128MEDIA_PATH=/var/lib/klonkt/blog/media
129AUDIO_PATH=/var/lib/klonkt/blog/audio
130ENV
131sudo chown -R klonkt:klonkt /var/lib/klonkt/blog
132sudo chmod 750 /var/lib/klonkt/blog && sudo chmod 600 /var/lib/klonkt/blog/.env
133sudo systemctl enable --now klonkt@blog
134```
135
136Every instance needs its own `PORT` and its own `PUBLIC_BASE_URL`. Give each one
137its own `SESSION_SECRET` as well: sharing it would make sessions from one site
138valid on another.
139
140The three data paths are the only ones you need. Media subdirectories such as
141`media/avatars` and `media/post-images` follow `MEDIA_PATH` on their own.
142
143## Updating them all at once
144
145```bash
146sudo klonkt-update
147```
148
149**Migrated before August 2026?** Then your `klonkt-update` still restarts the
150retired `klonkt.service`: the code updates but the running process never
151follows, and after the next update the site can 500 on pages whose template
152and route no longer match. Repair it once:
153
154```bash
155sudo systemctl restart klonkt@<slug> # load the current code now
156sudo bash /opt/klonkt/scripts/klonkt-refresh-updater.sh # fix the updater for good
157```
158
159The migration script does this by itself nowadays.
160
161That pulls the code once into `/opt/klonkt`, reinstalls dependencies only when
162`package-lock.json` changed, and restarts every instance it finds under
163`/var/lib/klonkt`. There is one copy of the code, so no instance can lag behind.
164
165Watch one instance while it comes back:
166
167```bash
168journalctl -u klonkt@blog -f
169```
170
171## Backups
172
173One directory per instance:
174
175```bash
176systemctl stop klonkt@blog
177tar czf blog-$(date +%F).tar.gz -C /var/lib/klonkt blog
178systemctl start klonkt@blog
179```
180
181Stopping first keeps the SQLite file consistent. To back up without downtime,
182use `sqlite3 database.sqlite ".backup snapshot.db"` and archive the snapshot
183together with `media/` and `audio/`.
184
185There is nothing to back up in `/opt/klonkt`: it is a checkout of a public
186repository and `klonkt-update` recreates it.
187
188## Removing an instance
189
190```bash
191sudo systemctl disable --now klonkt@blog
192sudo rm -rf /var/lib/klonkt/blog # this deletes the site's data
193```
194
195Then remove its block from the web server config and reload it.
196
197## Hosting for other people
198
199Everything here is about the machine. The moment an instance belongs to someone
200who is not you, there is a second set of questions — what you are holding, what
201you owe them, and how they leave. See [HOSTING.md](HOSTING.md).
202
203## Everyday commands
204
205| | |
206|---|---|
207| `systemctl status klonkt@<slug>` | is it running |
208| `journalctl -u klonkt@<slug> -f` | follow the log |
209| `systemctl restart klonkt@<slug>` | restart one instance |
210| `klonkt-update` | update the code, restart all instances |
211| `ls /var/lib/klonkt` | which instances exist |
212| `cd /opt/klonkt && npm run reset-admin -- --instance <slug>` | reset a forgotten admin password |
213
214### Why the password reset needs `--instance`
215
216Every other command here names its instance, and this one is no different — but
217the reason is easy to miss, because the app's own help page says only "run
218`npm run reset-admin` from the project folder". That advice is written for the
219one-instance layout, where the `.env` sits next to the code.
220
221Here it does not. The code is shared and read-only at `/opt/klonkt`; the
222configuration lives at `/var/lib/klonkt/<slug>/.env`, and **systemd** reads it
223through `EnvironmentFile=`. A script started by hand gets none of that: it looks
224for an `.env` beside the code, finds nothing, and is left without
225`DATABASE_PATH`. `--instance` reads the same file systemd does.
226
227Without it the script now stops and says which path it wanted, instead of
228creating an empty database inside the shared code directory. If your data lives
229somewhere other than `/var/lib/klonkt`, set `KLONKT_DATA_ROOT`.
Note: See TracBrowser for help on using the repository browser.