# 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 | 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 "" -cf - "" | ssh '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 `, 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:///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:///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.