feat(deploy): sidecars beside the instance, Jellyfin the first, behind an edge network

An instance may now run services beside itself, each a compose file of
its own switched on in deploy/.env (COMPOSE_FILE=compose.yaml:compose.
jellyfin.yaml), so the deploy runs them with the instance. The first is
Jellyfin, which people watch and listen to learn from: the instance then
suggests it (JELLYFIN_URL from JELLYFIN_DOMAIN), and Caddy serves its
site, refusing every change to a user from outside, so a passwordless
demo account stays everyone's; its admin signs in through an SSH tunnel.

The instance's compose gets a second network, `edge`, for Caddy (at a
fixed address, which sidecars trust as their proxy) and the server;
Postgres stays on the internal one. Caddy loses its needless way to
Postgres, and a sidecar never gains one. A sidecar publishes no port,
as the CI runner may reach 127.0.0.1, and runs as a user ID of its own.
Caddy imports the sidecars' sites from sidecars/, mounted by their
compose files (none without), and redeploy.sh recreates it when one
changes. sidecars/README.md has the steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-14 00:16:53 +02:00
commit 4b45cacfa4
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 192 additions and 4 deletions

88
deploy/sidecars/README.md Normal file
View file

@ -0,0 +1,88 @@
# 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` |
## 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 ID 61096 is Jellyfin's alone; the media folder is `deploy`'s, so it
can copy files in, readable by all.
```sh
sudo useradd --system --uid 61096 --user-group --no-create-home --shell /usr/sbin/nologin jellyfin
cd ~deploy/halkyon-learn/deploy
sudo mkdir -p jellyfin/config jellyfin/cache jellyfin/media
sudo chown -R jellyfin:jellyfin jellyfin/config jellyfin/cache
sudo chown -R deploy: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:
```sh
rsync -av --chmod=D755,F644 "<folder>" <deploy's ssh alias>:halkyon-learn/deploy/jellyfin/media/movies/
```
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` (Caddy's address 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.
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.

View file

@ -0,0 +1,17 @@
# The Jellyfin sidecar's site (compose.jellyfin.yaml).
{$JELLYFIN_DOMAIN} {
# Its demo account (sidecars/README.md) has no password and is everyone's: nobody
# may change a user from outside (give it a password, change its settings or its
# picture), only sign in. Halkyon Learn writes nothing else under /Users. The setup
# wizard is closed too. The admin works through an SSH tunnel, which doesn't pass
# here.
@userChanges {
method POST PUT DELETE
path /Users/* /UserImage /UserImage/*
not path /Users/AuthenticateByName /Users/AuthenticateWithQuickConnect
}
respond @userChanges 403
respond /Startup/* 403
reverse_proxy jellyfin:8096
}