halkyon/deploy/sidecars/README.md

316 lines
17 KiB
Markdown
Raw Normal View History

# Sidecars
Services an instance may run beside itself (decided 2026-10-14), each switched on by
naming its compose file after the instance's own in `deploy/.env`:
```sh
COMPOSE_FILE=compose.yaml:compose.jellyfin.yaml
```
A sidecar's compose file adds its service and, if it is reached from outside, mounts
its site from this folder into Caddy's `/etc/caddy/sidecars/`, which the Caddyfile
imports. It joins the `edge` network beside Caddy and the server's public API, never the
internal one where Postgres is, and publishes no port of its own. Its process runs as a
user ID of its own, which no person, no other container and not the CI runner uses.
`redeploy.sh` deploys whatever `.env` switches on, and recreates Caddy when a sidecar's
site changes.
| Sidecar | Compose file | Site |
|---|---|---|
| Jellyfin, which people watch and listen to learn from | `compose.jellyfin.yaml` | `jellyfin.caddy` |
| LanguageTool, which checks spelling and grammar | `compose.languagetool.yaml` | none |
| Halkyon Train's server, with BRouter for its routes | `compose.train.yaml` | `train.caddy` |
| Train's own map tiles, beside Train | `compose.maps.yaml` | `train-maps.caddy` (in Train's site) |
| Photon, which finds places for Train's route planner, beside Train | `compose.search.yaml` | none |
Both together: `COMPOSE_FILE=compose.yaml:compose.jellyfin.yaml:compose.languagetool.yaml`.
## Jellyfin
A Jellyfin server beside the instance, which the instance then suggests to its people
(`JELLYFIN_URL`, set by the compose file): the app's Jellyfin sign-in fills it in and
lists the people it shows on its login screen. Videos stream as stored, nothing is
transcoded. Caddy refuses every change to a user from outside, so an account without a
password (a demo's) stays everyone's; the admin signs in through an SSH tunnel only.
Steps, in order; ✱ marks a step as root.
1. ✱ **Its user and folders** (the operator, on the instance's server, as a user with
sudo): the user and group ID 61096 are Jellyfin's alone, as the compose file runs it
(`useradd` warns that the ID lies above the system range, which is meant: it must
not meet a container's IDs); the media folder is `deploy`'s, so it can copy files
in, readable by all. The folders are named in full: `deploy`'s home is closed to
other users (Ubuntu's default), so a user with sudo can't `cd` into it, but `sudo`
reaches it by its path, which the shell makes of `~deploy` without entering it.
```sh
sudo groupadd --system --gid 61096 jellyfin
sudo useradd --system --uid 61096 --gid 61096 --no-create-home --shell /usr/sbin/nologin jellyfin
sudo mkdir -p ~deploy/halkyon-learn/deploy/jellyfin/config \
~deploy/halkyon-learn/deploy/jellyfin/cache ~deploy/halkyon-learn/deploy/jellyfin/media
sudo chown -R jellyfin:jellyfin ~deploy/halkyon-learn/deploy/jellyfin/config \
~deploy/halkyon-learn/deploy/jellyfin/cache
sudo chown -R deploy:deploy ~deploy/halkyon-learn/deploy/jellyfin/media
```
2. **DNS** (the operator, at the domain's DNS): the Jellyfin domain, such as
`jellyfin.example.org`, pointing at the instance's server (`A`, and `AAAA` if it has
an IPv6 address).
3. **Switch it on** (the operator, on the instance's server, as `deploy`): in
`deploy/.env`,
```sh
COMPOSE_FILE=compose.yaml:compose.jellyfin.yaml
JELLYFIN_DOMAIN=jellyfin.example.org
```
4. **The media** (the operator, on their own machine): a folder per film, show or
album, as Jellyfin expects them, into `deploy/jellyfin/media/`, readable by all;
through `tar` and SSH, which every server has (rsync would have to be installed on
it too):
```sh
tar -C "<the folder's parent>" -cf - "<folder>" |
ssh <deploy's ssh alias> 'tar -C halkyon-learn/deploy/jellyfin/media/movies -xf -'
```
5. **Start it** (the operator, on the instance's server, as `deploy`):
`cd ~/halkyon-learn/deploy && docker compose up -d`. Caddy and the server are
recreated with it (the site, and the instance's suggestion); the instance is away
for seconds.
6. **Set Jellyfin up through a tunnel** (the operator, on their own machine):
`ssh -N -L 8096:172.30.80.10:8096 <deploy's ssh alias>`, then http://localhost:8096.
- The wizard: an admin of a name nobody guesses, its password in a password
manager; a library of the type Movies (or Shows, Music) on `/media/…`; remote
connections allowed.
- Dashboard › Networking › **Known proxies**: `172.30.80.2` and `fd4a:6c6b:7e80::2`
(Caddy's addresses on `edge`). Jellyfin then sees each visitor's own address, and the tunnel as local.
- Dashboard › General: **Quick Connect** off, unless people sign in with it.
- Dashboard › Users › the admin: **Hide this user from login screens**; **Allow
remote connections to this server** off.
- For a demo, Dashboard › Users › **+**: an account without a password, shown on
login screens, seeing the demo's library alone; off: playback and conversion that
need transcoding, content downloading, remote control of other users, deleting
media. On its Profile tab, **Failed login tries before user is locked out** `-1`:
with Jellyfin's default of 3, anyone could switch the demo off with three wrong
passwords, and without a password the lockout guards nothing. People sign in to it
by tapping it and leaving the password empty: Jellyfin doesn't say which accounts
have none.
7. **Check from outside** (anyone, anywhere): `https://<domain>/System/Info/Public`
answers, `/Users/Public` lists the demo account, a `POST` to `/Users/Password` gets
403, and the instance's `instance.info()` names the domain as `jellyfinUrl`.
Switching it off: the compose file out of `COMPOSE_FILE`, then `docker compose up -d
--remove-orphans`; `deploy/jellyfin/` stays until removed by hand.
## LanguageTool
LanguageTool's open-source server beside the instance (ADR 0024), which checks spelling
and grammar for Halkyon Learn: Check in the assistant's field, and the underlines while
writing (the app's Settings › Writing). Only the instance's server reaches it
(`LANGUAGETOOL_URL`, set by the compose file): LanguageTool has no sign-in of its own,
so it gets no site, and the server checks who asks and how often (signed in, 60 checks a
minute each). Nothing written is kept or logged, LanguageTool's own log included: Docker
keeps none for it. It checks without n-gram data (LanguageTool's statistical checks of
words like their and there, tens of GB per language), in about 1.5 GB of memory.
Steps, in order; ✱ marks a step as root.
1. ✱ **Its user and folder** (the operator, on the instance's server, as a user with
sudo): the user and group ID 61081 are LanguageTool's alone, as the compose file runs
it (`useradd` warns that the ID lies above the system range, which is meant: it must
not meet a container's IDs). The folder is named in full, as Jellyfin's.
```sh
sudo groupadd --system --gid 61081 languagetool
sudo useradd --system --uid 61081 --gid 61081 --no-create-home --shell /usr/sbin/nologin languagetool
sudo mkdir -p ~deploy/halkyon-learn/deploy/languagetool/fasttext
sudo chown -R languagetool:languagetool ~deploy/halkyon-learn/deploy/languagetool
```
2. **Switch it on** (the operator, on the instance's server, as `deploy`): in
`deploy/.env`, `COMPOSE_FILE=compose.yaml:compose.languagetool.yaml`.
3. **Start it** (the operator, on the instance's server, as `deploy`):
`cd ~/halkyon-learn/deploy && docker compose up -d`. The server is recreated with it
(its `LANGUAGETOOL_URL`); the instance is away for seconds. At its first start
LanguageTool fetches fastText's model of languages (`lid.176.bin`, 126 MB, from
Facebook's `dl.fbaipublicfiles.com`) into `deploy/languagetool/fasttext/`, which it
uses to tell which language a note is in; checks start once it is there.
4. **Check it**: on the instance's server, as `deploy`, `docker compose ps languagetool`
says `healthy` after a minute; then, from anywhere, the instance names the languages
it checks:
```sh
curl -s -X POST -H 'Content-Type: application/json' -d '{}' https://<domain>/instance/info
```
answers with `grammarLanguages`, `de-DE` and `en-US` among them.
Switching it off: the compose file out of `COMPOSE_FILE`, then `docker compose up -d
--remove-orphans`; the app's Writing settings then say the instance checks nothing, and
`deploy/languagetool/` stays until removed by hand.
## Train
Halkyon Train's server beside the instance (Halkyon Train's ADR 0030, D46), at its own
domain, a member of the register Learn's server holds (ADR 0016), its database
`halkyon_train` in the instance's PostgreSQL, and BRouter planning its routes (D11, D44)
from the segments of the operator's region (Europe's by default, about 3.3 GB on
28 Oct 2026), refreshed monthly. `redeploy.sh` builds, dumps, migrates and waits for it
as for Learn's server once `.env` names its compose file, and `backup.sh` backs its
database up beside Learn's. The map stays OpenFreeMap's until the instance serves its
own tiles (D22, D23).
Steps, in order; ✱ marks a step as root.
1. **Room on the disk** (the operator, on the instance's server): `df -h ~deploy`.
BRouter's segments take about 3.3 GB for Europe, and a refresh writes each one beside
the old before replacing it; Train's dumps go to `~/backups` with Learn's.
2. **Its database** (the operator, on the instance's server, as `deploy`, in
`~/halkyon-learn/deploy`): a role of Train's own, with the password that goes into
`.env` as `TRAIN_POSTGRES_PASSWORD` (`openssl rand -hex 32`), and its database:
```sh
docker compose exec -T postgres psql -U halkyon_learn -d postgres -v ON_ERROR_STOP=1 \
-v password="<TRAIN_POSTGRES_PASSWORD>" <<'SQL'
CREATE ROLE halkyon_train LOGIN PASSWORD :'password';
CREATE DATABASE halkyon_train OWNER halkyon_train;
REVOKE CONNECT ON DATABASE halkyon_train FROM PUBLIC;
REVOKE CONNECT ON DATABASE halkyon_learn FROM PUBLIC;
SQL
```
`CREATE ROLE` makes the role Train's server signs in as, which may log in and is
nothing more (no superuser, no creating roles or databases); `CREATE DATABASE … OWNER`
gives it its own database, where its migrations make the tables. The two `REVOKE`s
take away the right every role has by default to connect to a database: only its
owner (and the instance's superuser, `halkyon_learn`, which Learn's server signs in
as) connects to `halkyon_train`, and `halkyon_train` no longer connects to Learn's.
Nothing of Learn's changes. (Learn's server, signing in as the superuser, could still
read Train's; giving it a role of its own is a step of its own.)
3. ✱ **BRouter's user and folder** (the operator, as a user with sudo): the user and
group ID 61177 are BRouter's alone, as the compose file runs it; the folder its
segments go to is theirs.
```sh
sudo groupadd --system --gid 61177 brouter
sudo useradd --system --uid 61177 --gid 61177 --no-create-home --shell /usr/sbin/nologin brouter
sudo mkdir -p ~deploy/halkyon-learn/deploy/brouter/segments
sudo chown -R brouter:brouter ~deploy/halkyon-learn/deploy/brouter/segments
```
4. **DNS** (the operator, at the domain's DNS): Train's domain, `train.halkyon.eu`,
pointing at the instance's server (`A`, and `AAAA` if it has an IPv6 address).
5. **Switch it on** (the operator, as `deploy`): in `deploy/.env`, `compose.train.yaml`
added to `COMPOSE_FILE`, and Train's settings (`.env.example`, *With Halkyon Train*):
```sh
COMPOSE_FILE=compose.yaml:compose.languagetool.yaml:compose.train.yaml
TRAIN_DOMAIN=train.halkyon.eu
TRAIN_POSTGRES_PASSWORD=<as in step 2>
TRAIN_SESSION_KEY_HASH_PEPPER=<openssl rand -hex 32>
```
`INSTANCE_SECRET` must be set (Learn's register takes no member without it). Where
`INSTANCE_MEMBERS` is set, add `http://train-server:8082/` to it; unset, the compose
file names Train.
6. **Start it** (the operator, as `deploy`): `docker compose up -d --build`. Caddy and
Learn's server are recreated with it (Train's site, and the register's member); the
instance is away for seconds. Train's server applies its migrations as it starts.
The segments come in the first hour or so (`docker compose logs brouter-segments`
says when); until then BRouter finds no route. Where brouter.de doesn't answer or
the folder isn't BRouter's, the log says so and it tries again an hour later.
7. **Check it**: `https://train.halkyon.eu/` answers; in Train, signed in with a Learn
account, a route plans between two points of the region.
## Train's map
The instance's own map for Halkyon Train (Halkyon Train's ADR 0030, D50), beside Train
(`compose.train.yaml` first): Protomaps' basemap cut for Europe from Protomaps' daily
build (the planet's was 139 GB on 29 Oct 2026; Europe's cut is estimated at some 45 GB,
to be measured), served by `pmtiles serve` and drawn by Train's own style, which Train's
server serves. Only people signed in to Train get tiles: `map.source` hands the app a
key, which Caddy has Train's server check before each tile. No tile is logged. The cut
is renewed monthly where Protomaps has a newer build, written beside the old one and
moved over it. Off, Train's maps are OpenFreeMap's (or `MAP_STYLE_URL`'s) again.
Steps, in order; ✱ marks a step as root.
1. **Room on the disk** (the operator, on the instance's server): `df -h ~deploy`.
A cut takes some 45 GB, and twice that while a new one is cut beside it.
2. ✱ **The tiles' user and folder** (the operator, as a user with sudo): the user and
group ID 61178 are the tiles' alone, as the compose file runs them.
```sh
sudo groupadd --system --gid 61178 maptiles
sudo useradd --system --uid 61178 --gid 61178 --no-create-home --shell /usr/sbin/nologin maptiles
sudo mkdir -p ~deploy/halkyon-learn/deploy/maps/tiles
sudo chown -R maptiles:maptiles ~deploy/halkyon-learn/deploy/maps/tiles
```
3. **Switch it on** (the operator, as `deploy`): in `deploy/.env`, `compose.maps.yaml`
added to `COMPOSE_FILE` after Train's, and the secret the keys are made with:
```sh
COMPOSE_FILE=compose.yaml:compose.languagetool.yaml:compose.train.yaml:compose.maps.yaml
MAP_KEY_SECRET=<openssl rand -hex 32>
```
`MAP_STYLE_URL` is then not used. Changing `MAP_KEY_SECRET` refuses every key handed
out before, until the apps ask for the source again (as each opens).
4. **Start it** (the operator, as `deploy`): `docker compose up -d --build`. Caddy and
Train's server are recreated with it. The first cut takes hours
(`docker compose logs -f map-tiles-refresh` says when it is done); until then Train's
maps stay empty. A failed cut is tried again an hour later.
5. **Check it**: in Train, signed in, a workout's map is drawn and names the instance as
its source; `curl -I https://train.halkyon.eu/tiles/europe/0/0/0.mvt` is refused
with 401. Not yet tried on the server (29 Oct 2026): whether `pmtiles serve` reads a
new cut moved over the old one without a restart (else `docker compose restart
map-tiles` after each cut), and Caddy's `import` of an empty folder where the map is
off.
## Train's search
Photon beside Train (Halkyon Train's ADR 0030, D50; `compose.train.yaml` first), which
finds the places Train's route planner searches for, on GraphHopper's weekly database
of Europe (a 32.6 GB download on 29 Oct 2026, more unpacked, to be measured; Germany's
alone 7.5 GB, `PHOTON_REGION=europe/germany`). Only Train's server reaches it, for
people signed in; nothing searched is kept or logged. The database is renewed monthly:
downloaded and unpacked beside the one in use, checked against its MD5, then put in
its place, Photon away for a minute or two.
Steps, in order; ✱ marks a step as root.
1. **Room and memory** (the operator, on the instance's server): `df -h ~deploy` and
`free -h`. The database takes tens of GB, twice that while a new one comes; Photon
wants `PHOTON_HEAP` (6g by default) and room beside it (`PHOTON_MEMORY`, 8g).
2. ✱ **Photon's user and folder** (the operator, as a user with sudo): the user and
group ID 61179 are Photon's alone, as the compose file runs it.
```sh
sudo groupadd --system --gid 61179 photon
sudo useradd --system --uid 61179 --gid 61179 --no-create-home --shell /usr/sbin/nologin photon
sudo mkdir -p ~deploy/halkyon-learn/deploy/photon/data
sudo chown -R photon:photon ~deploy/halkyon-learn/deploy/photon/data
```
3. **Switch it on** (the operator, as `deploy`): in `deploy/.env`, `compose.search.yaml`
added to `COMPOSE_FILE` after Train's:
```sh
COMPOSE_FILE=compose.yaml:compose.languagetool.yaml:compose.train.yaml:compose.maps.yaml:compose.search.yaml
```
4. **Start it** (the operator, as `deploy`): `docker compose up -d --build`. Train's
server is recreated with it. The first database takes an hour or more
(`docker compose logs -f photon-data` says when it is ready); Photon starts on it
within a minute, and until then Train's planner offers no search.
5. **Check it**: in Train, signed in, the planner's search finds a town of the region.
Not yet tried on the server (29 Oct 2026): the database's size unpacked, Photon's
memory on Europe's, and that 1.3.0 reads GraphHopper's `1.0` build.