Step 1 of both sidecars changed into deploy's home before its sudo commands, but Ubuntu closes a home to other users, so the operator's own user, the one with sudo, gets "Permission denied" there (found setting up LanguageTool on learn.halkyon.eu's server, 26 Oct). sudo reaches the folders by their full paths, which the shell makes of ~deploy without entering it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
153 lines
8 KiB
Markdown
153 lines
8 KiB
Markdown
# 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 "<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.
|