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>
2026-10-14 00:16:53 +02:00
|
|
|
|
# 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` |
|
feat(deploy): Jellyfin at home, published through WireGuard (ADR 0032)
The instance serves jellyfin.halkyon.eu and its Caddy proxies through a
WireGuard tunnel to a Jellyfin on the operator's own machine, which dials
out, so nothing names the home network's address and its router opens no
port. compose.jellyfin-home.yaml takes the sidecar's place, with its
site's rules unchanged (jellyfin.caddy's upstream is now a setting), and
leaves JELLYFIN_URL unset, so the instance suggests it only while it
answers.
On the instance's server the tunnel reaches Jellyfin from Caddy alone and
nothing of the host (wg-jellyfin-rules.sh, set and removed with the
tunnel); the CI runner's unit denies it, and CI's Sandbox step fails if a
job reaches it. The home end keeps Jellyfin in WireGuard's network
namespace, its port on the loopback alone. The steps are in
deploy/sidecars/README.md, the rules held by test/deploy/jellyfin_home_test.dart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 22:49:47 +01:00
|
|
|
|
| Jellyfin at home, published through a WireGuard tunnel, in its place | `compose.jellyfin-home.yaml` | `jellyfin.caddy` |
|
2026-10-25 23:13:46 +01:00
|
|
|
|
| LanguageTool, which checks spelling and grammar | `compose.languagetool.yaml` | none |
|
2026-10-29 00:05:39 +01:00
|
|
|
|
| Halkyon Train's server, with BRouter for its routes | `compose.train.yaml` | `train.caddy` |
|
2026-10-29 16:04:05 +01:00
|
|
|
|
| Train's own map tiles, beside Train | `compose.maps.yaml` | `train-maps.caddy` (in Train's site) |
|
2026-10-29 16:20:36 +01:00
|
|
|
|
| Photon, which finds places for Train's route planner, beside Train | `compose.search.yaml` | none |
|
2026-10-25 23:13:46 +01:00
|
|
|
|
|
|
|
|
|
|
Both together: `COMPOSE_FILE=compose.yaml:compose.jellyfin.yaml:compose.languagetool.yaml`.
|
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>
2026-10-14 00:16:53 +02:00
|
|
|
|
|
|
|
|
|
|
## 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
|
2026-10-14 01:24:50 +02:00
|
|
|
|
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
|
2026-10-26 12:18:46 +01:00
|
|
|
|
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.
|
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>
2026-10-14 00:16:53 +02:00
|
|
|
|
|
|
|
|
|
|
```sh
|
2026-10-14 01:24:50 +02:00
|
|
|
|
sudo groupadd --system --gid 61096 jellyfin
|
|
|
|
|
|
sudo useradd --system --uid 61096 --gid 61096 --no-create-home --shell /usr/sbin/nologin jellyfin
|
2026-10-26 12:18:46 +01:00
|
|
|
|
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
|
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>
2026-10-14 00:16:53 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-10-14 01:27:35 +02:00
|
|
|
|
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):
|
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>
2026-10-14 00:16:53 +02:00
|
|
|
|
|
|
|
|
|
|
```sh
|
2026-10-14 01:27:35 +02:00
|
|
|
|
tar -C "<the folder's parent>" -cf - "<folder>" |
|
|
|
|
|
|
ssh <deploy's ssh alias> 'tar -C halkyon-learn/deploy/jellyfin/media/movies -xf -'
|
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>
2026-10-14 00:16:53 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
2026-10-20 21:49:29 +02:00
|
|
|
|
- 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.
|
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>
2026-10-14 00:16:53 +02:00
|
|
|
|
- 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
|
2026-10-14 01:52:42 +02:00
|
|
|
|
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.
|
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>
2026-10-14 00:16:53 +02:00
|
|
|
|
|
|
|
|
|
|
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.
|
2026-10-25 23:13:46 +01:00
|
|
|
|
|
feat(deploy): Jellyfin at home, published through WireGuard (ADR 0032)
The instance serves jellyfin.halkyon.eu and its Caddy proxies through a
WireGuard tunnel to a Jellyfin on the operator's own machine, which dials
out, so nothing names the home network's address and its router opens no
port. compose.jellyfin-home.yaml takes the sidecar's place, with its
site's rules unchanged (jellyfin.caddy's upstream is now a setting), and
leaves JELLYFIN_URL unset, so the instance suggests it only while it
answers.
On the instance's server the tunnel reaches Jellyfin from Caddy alone and
nothing of the host (wg-jellyfin-rules.sh, set and removed with the
tunnel); the CI runner's unit denies it, and CI's Sandbox step fails if a
job reaches it. The home end keeps Jellyfin in WireGuard's network
namespace, its port on the loopback alone. The steps are in
deploy/sidecars/README.md, the rules held by test/deploy/jellyfin_home_test.dart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 22:49:47 +01:00
|
|
|
|
## Jellyfin at home
|
|
|
|
|
|
|
|
|
|
|
|
The Jellyfin sidecar's site, served by the instance, with Jellyfin on the operator's own
|
|
|
|
|
|
machine rather than beside the instance (ADR 0032): `compose.jellyfin-home.yaml` on the
|
|
|
|
|
|
instance's server, `home-jellyfin/` on that machine, a WireGuard tunnel between them
|
|
|
|
|
|
that the machine dials out on. People reach the instance's server alone; nothing names
|
|
|
|
|
|
the home network's address, and its router opens no port. Caddy's rules are the
|
|
|
|
|
|
sidecar's (`jellyfin.caddy`), and the tunnel reaches Jellyfin from Caddy alone and
|
|
|
|
|
|
nothing of the instance's server (`home-jellyfin/server/wg-jellyfin-rules.sh`; nor can
|
|
|
|
|
|
the CI runner reach it, `IPAddressDeny`). The instance suggests it only while it
|
|
|
|
|
|
answers: `JELLYFIN_URL` stays unset, so the instance looks at `jellyfin.` on its own
|
|
|
|
|
|
domain, which `JELLYFIN_DOMAIN` must therefore be. With the machine off, asleep or
|
|
|
|
|
|
offline, Caddy answers 502, and the instance stops suggesting it at its first look
|
|
|
|
|
|
after that, within about an hour. It takes the place of the sidecar, never runs
|
|
|
|
|
|
beside it.
|
|
|
|
|
|
|
|
|
|
|
|
The tunnel's addresses: 10.61.96.1 the instance's end, 10.61.96.2 the home end, UDP
|
|
|
|
|
|
51820 on the instance's server. Steps, in order; ✱ marks a step as root, 🏠 one on the
|
|
|
|
|
|
home machine.
|
|
|
|
|
|
|
|
|
|
|
|
1. 🏠 **The home end's key** (the operator, in `deploy/home-jellyfin/` of their
|
|
|
|
|
|
checkout): WireGuard's own tool from the image, so nothing is installed; the
|
|
|
|
|
|
private key goes into the git-ignored config, which only its owner reads, the
|
|
|
|
|
|
public key is printed for step 3.
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
cd deploy/home-jellyfin
|
|
|
|
|
|
image=$(sed -n 's/^ *image: \(lscr.io\/linuxserver\/wireguard@.*\)/\1/p' compose.yaml)
|
|
|
|
|
|
(umask 077 && mkdir -p wireguard/wg_confs &&
|
|
|
|
|
|
docker run --rm --entrypoint wg "$image" genkey > wireguard/home.key)
|
|
|
|
|
|
docker run --rm -i --entrypoint wg "$image" pubkey < wireguard/home.key
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
2. ✱ **The instance's end** (the operator, on the instance's server, as a user with
|
|
|
|
|
|
sudo): WireGuard's tools, the key, and the files from the checkout. The key is in
|
|
|
|
|
|
the password manager too: a new server needs it again, or the home end a new
|
|
|
|
|
|
public key.
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
sudo apt-get install wireguard-tools
|
|
|
|
|
|
(umask 077 && wg genkey | sudo tee /etc/wireguard/wg-jellyfin.key | wg pubkey)
|
|
|
|
|
|
from=~deploy/halkyon-learn/deploy/home-jellyfin/server
|
|
|
|
|
|
sudo install -m 0600 "$from/wg-jellyfin.conf" /etc/wireguard/wg-jellyfin.conf
|
|
|
|
|
|
sudo install -m 0700 "$from/wg-jellyfin-rules.sh" /etc/wireguard/wg-jellyfin-rules.sh
|
|
|
|
|
|
sudo install -D -m 0644 "$from/wg-quick-after-docker.conf" \
|
|
|
|
|
|
/etc/systemd/system/wg-quick@wg-jellyfin.service.d/after-docker.conf
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
The printed public key is the instance's, for step 4.
|
|
|
|
|
|
|
|
|
|
|
|
3. ✱ **Introduce the home end, open the port, start it** (as in step 2):
|
|
|
|
|
|
`sudo nano /etc/wireguard/wg-jellyfin.conf`, the home end's public key from step 1
|
|
|
|
|
|
in place of `<the home end's public key>`; where the host has a firewall, UDP 51820
|
|
|
|
|
|
opened (`sudo ufw allow 51820/udp`); the runner's unit as the checkout has it, so it
|
|
|
|
|
|
can't reach the tunnel; then the tunnel.
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
sudo install -m 0644 ~deploy/halkyon-learn/deploy/runner/forgejo-runner.service \
|
|
|
|
|
|
/etc/systemd/system/forgejo-runner.service
|
|
|
|
|
|
sudo systemctl daemon-reload
|
|
|
|
|
|
sudo systemctl restart forgejo-runner
|
|
|
|
|
|
sudo systemctl enable --now wg-quick@wg-jellyfin
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
4. 🏠 **The home end's config, folders and media** (in `deploy/home-jellyfin/`):
|
|
|
|
|
|
`wg0.conf.example` as `wireguard/wg_confs/wg0.conf`, with the key from step 1 and
|
|
|
|
|
|
the instance's public key from step 2; Jellyfin's folders owned by its user ID
|
|
|
|
|
|
(61096, as on the instance's server: no person and no other container uses it); the
|
|
|
|
|
|
media folder named in `.env` and readable by all, a folder per film, show or album
|
|
|
|
|
|
as Jellyfin expects them.
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
(umask 077 && sed "s|<the home end's private key>|$(cat wireguard/home.key)|" \
|
|
|
|
|
|
wg0.conf.example > wireguard/wg_confs/wg0.conf)
|
|
|
|
|
|
nano wireguard/wg_confs/wg0.conf # the instance's public key
|
|
|
|
|
|
sudo install -d -o 61096 -g 61096 jellyfin/config jellyfin/cache
|
|
|
|
|
|
echo "JELLYFIN_MEDIA=$HOME/Videos" > .env
|
|
|
|
|
|
chmod -R o+rX "$HOME/Videos"
|
|
|
|
|
|
docker compose up -d
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`sudo wg show wg-jellyfin` on the instance's server now shows a handshake.
|
|
|
|
|
|
|
2026-10-31 14:24:22 +01:00
|
|
|
|
Whenever `wg0.conf` fails to come up (a placeholder or a wrong key, an endpoint that
|
2026-10-31 15:10:46 +01:00
|
|
|
|
doesn't resolve: why it names the server by its address, ADR 0032, D11, and a new
|
|
|
|
|
|
address of the server's goes there too), the image's client mode deletes the namespace's default route
|
2026-10-31 14:24:22 +01:00
|
|
|
|
(`ip route del default` in its `svc-wireguard/run`, ls126), and Jellyfin with it loses
|
|
|
|
|
|
the internet: TMDb's metadata and images fail with *Network is unreachable*, and a
|
|
|
|
|
|
library scanned then gets frames of its videos for posters. A tunnel that comes up
|
|
|
|
|
|
keeps the route, even before its first handshake. Fixing `wg0.conf` alone isn't enough:
|
|
|
|
|
|
`docker compose up -d --force-recreate`, both containers, since Jellyfin lives in
|
|
|
|
|
|
WireGuard's namespace and a restart of WireGuard alone leaves it in the old one. The
|
|
|
|
|
|
folder `wireguard/` is the container's user's (61096) from its first start on, so
|
|
|
|
|
|
`wg0.conf` is edited with sudo.
|
|
|
|
|
|
|
feat(deploy): Jellyfin at home, published through WireGuard (ADR 0032)
The instance serves jellyfin.halkyon.eu and its Caddy proxies through a
WireGuard tunnel to a Jellyfin on the operator's own machine, which dials
out, so nothing names the home network's address and its router opens no
port. compose.jellyfin-home.yaml takes the sidecar's place, with its
site's rules unchanged (jellyfin.caddy's upstream is now a setting), and
leaves JELLYFIN_URL unset, so the instance suggests it only while it
answers.
On the instance's server the tunnel reaches Jellyfin from Caddy alone and
nothing of the host (wg-jellyfin-rules.sh, set and removed with the
tunnel); the CI runner's unit denies it, and CI's Sandbox step fails if a
job reaches it. The home end keeps Jellyfin in WireGuard's network
namespace, its port on the loopback alone. The steps are in
deploy/sidecars/README.md, the rules held by test/deploy/jellyfin_home_test.dart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 22:49:47 +01:00
|
|
|
|
5. 🏠 **Set Jellyfin up** at http://localhost:8096 on the home machine, before the site is
|
|
|
|
|
|
switched on: the sidecar's step 6, but for its networking, which is the tunnel's:
|
|
|
|
|
|
- Dashboard › Networking › **LAN networks**: `172.29.96.1/32` alone, the gateway
|
|
|
|
|
|
through which this machine's loopback reaches it (`home-jellyfin/compose.yaml`).
|
|
|
|
|
|
Left empty, Jellyfin counts every private range as local, the tunnel's too, and an
|
|
|
|
|
|
admin without remote access could sign in through it.
|
|
|
|
|
|
- Dashboard › Networking › **Known proxies**: `10.61.96.1`, as which Docker on the
|
|
|
|
|
|
instance's server masquerades Caddy's requests. Jellyfin then sees each visitor's
|
|
|
|
|
|
own address.
|
|
|
|
|
|
- **Allow remote connections** on; the admin hidden from login screens, its remote
|
|
|
|
|
|
connections off; Quick Connect, the demo account and its lockout as in the
|
|
|
|
|
|
sidecar's step 6. Every account but those meant for everyone hidden from login
|
|
|
|
|
|
screens: the instance suggests this server to all its people.
|
|
|
|
|
|
|
|
|
|
|
|
6. **Switch the site on** (the operator, on the instance's server, as `deploy`): in
|
|
|
|
|
|
`deploy/.env`, `JELLYFIN_URL` left empty,
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
COMPOSE_FILE=compose.yaml:compose.jellyfin-home.yaml
|
|
|
|
|
|
JELLYFIN_DOMAIN=jellyfin.halkyon.eu
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
then `cd ~/halkyon-learn/deploy && docker compose up -d`. Caddy and the server are
|
|
|
|
|
|
recreated with it; the instance is away for seconds.
|
|
|
|
|
|
|
|
|
|
|
|
7. **Check from outside** (anyone, anywhere): the sidecar's step 7; and on the instance's
|
|
|
|
|
|
server, as `deploy`, that nothing reaches the home end but Caddy, and the home end
|
|
|
|
|
|
nothing of the server:
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
# Refused (on edge, but not at Caddy's address): times out.
|
|
|
|
|
|
docker run --rm --network halkyon-learn-edge caddy:2 \
|
|
|
|
|
|
wget -T 5 -q -O /dev/null http://10.61.96.2:8096/System/Info/Public && echo LEAK
|
|
|
|
|
|
# Caddy itself gets through.
|
|
|
|
|
|
docker compose exec -T caddy wget -T 5 -q -O - http://10.61.96.2:8096/System/Info/Public
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
and on the home machine, in `deploy/home-jellyfin/`:
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
2026-10-31 16:47:35 +01:00
|
|
|
|
# Refused (nothing new from the tunnel reaches the host): gives up after 5 s. The
|
|
|
|
|
|
# image's nc is OpenBSD's, whose own -w ends the try.
|
|
|
|
|
|
docker compose exec -T wireguard nc -w 5 -z 10.61.96.1 22 && echo LEAK
|
feat(deploy): Jellyfin at home, published through WireGuard (ADR 0032)
The instance serves jellyfin.halkyon.eu and its Caddy proxies through a
WireGuard tunnel to a Jellyfin on the operator's own machine, which dials
out, so nothing names the home network's address and its router opens no
port. compose.jellyfin-home.yaml takes the sidecar's place, with its
site's rules unchanged (jellyfin.caddy's upstream is now a setting), and
leaves JELLYFIN_URL unset, so the instance suggests it only while it
answers.
On the instance's server the tunnel reaches Jellyfin from Caddy alone and
nothing of the host (wg-jellyfin-rules.sh, set and removed with the
tunnel); the CI runner's unit denies it, and CI's Sandbox step fails if a
job reaches it. The home end keeps Jellyfin in WireGuard's network
namespace, its port on the loopback alone. The steps are in
deploy/sidecars/README.md, the rules held by test/deploy/jellyfin_home_test.dart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 22:49:47 +01:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Stopping the home end (`docker compose stop`) makes the site answer 502, and
|
|
|
|
|
|
`instance.info()` names no `jellyfinUrl` after the instance's next look, within about
|
|
|
|
|
|
an hour.
|
|
|
|
|
|
|
|
|
|
|
|
Switching it off: on the instance's server the compose file out of `COMPOSE_FILE`, `docker
|
|
|
|
|
|
compose up -d --remove-orphans`, and ✱ `sudo systemctl disable --now
|
|
|
|
|
|
wg-quick@wg-jellyfin`, the port closed again; at home `docker compose down`, with
|
|
|
|
|
|
`deploy/home-jellyfin/jellyfin/` kept until removed by hand.
|
|
|
|
|
|
|
2026-10-25 23:13:46 +01:00
|
|
|
|
## 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
|
2026-10-26 12:18:46 +01:00
|
|
|
|
not meet a container's IDs). The folder is named in full, as Jellyfin's.
|
2026-10-25 23:13:46 +01:00
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
sudo groupadd --system --gid 61081 languagetool
|
|
|
|
|
|
sudo useradd --system --uid 61081 --gid 61081 --no-create-home --shell /usr/sbin/nologin languagetool
|
2026-10-26 12:18:46 +01:00
|
|
|
|
sudo mkdir -p ~deploy/halkyon-learn/deploy/languagetool/fasttext
|
|
|
|
|
|
sudo chown -R languagetool:languagetool ~deploy/halkyon-learn/deploy/languagetool
|
2026-10-25 23:13:46 +01:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
2026-10-29 00:05:39 +01:00
|
|
|
|
|
|
|
|
|
|
## 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`
|
2026-10-30 12:55:42 +01:00
|
|
|
|
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.
|
2026-10-29 00:05:39 +01:00
|
|
|
|
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.
|
|
|
|
|
|
|
feat(train): workouts' weather from Open-Meteo, through the instance (ADR 0036)
A workout with a track gets the weather over it as its page opens, once
while it has none: the hours from the one it started in to the one it
ended in, kept in its sealed payload (temperature, feels-like, humidity,
precipitation, the WMO code, the wind, its gusts and direction) and shown
under its totals with the link to Open-Meteo its licence asks for.
Train's server asks Open-Meteo in the person's place (the `weather`
endpoint), so Open-Meteo never sees their address: the place rounded to
two decimals (also on the phone), a span of at most 48 hours, the
forecast API for recent spans and the archive for older ones, for people
signed in, at most 30 requests a person a minute, nothing kept or
logged. On by default; `WEATHER=off` switches it off, and
`OPEN_METEO_API_KEY` moves it to the paid hosts for an instance used
commercially.
Health Connect's fingerprint leaves the weather out, so keeping it
writes nothing again. No table and no migration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 13:59:41 +01:00
|
|
|
|
Workouts' weather (Halkyon Train's ADR 0036) comes from Open-Meteo through Train's
|
|
|
|
|
|
server, on by default, the free API from the server's address (non-commercial use alone,
|
|
|
|
|
|
10,000 requests a day). `WEATHER=off` in `deploy/.env` stops it; an instance with ads or
|
|
|
|
|
|
subscriptions needs one of Open-Meteo's paid plans, its key in `OPEN_METEO_API_KEY`.
|
|
|
|
|
|
|
2026-10-29 16:04:05 +01:00
|
|
|
|
|
|
|
|
|
|
## 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
|
2026-10-30 12:55:42 +01:00
|
|
|
|
maps stay empty. A failed cut is tried again an hour later.
|
2026-10-29 16:04:05 +01:00
|
|
|
|
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.
|
2026-10-29 16:20:36 +01:00
|
|
|
|
|
|
|
|
|
|
## 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
|
2026-10-30 12:55:42 +01:00
|
|
|
|
within a minute, and until then Train's planner offers no search.
|
2026-10-29 16:20:36 +01:00
|
|
|
|
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.
|