feat(train): the instance's own map tiles, drawn by Train's style (ADR 0030, D50)

Protomaps' basemap cut for Europe and served by pmtiles beside Train's server
(compose.maps.yaml), drawn by Train's own style, light and dark, which the
server serves. Only people signed in get tiles: map.source hands a key that
Caddy has the server check before each tile; nothing logs a tile.

Also fixes .dockerignore, which left apps/train/ out of the build context, so
Train's server's and BRouter's images couldn't have been built.

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-29 16:04:05 +01:00
commit 190c3872f4
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 3726 additions and 97 deletions

View file

@ -20,6 +20,7 @@ site changes.
| 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) |
Both together: `COMPOSE_FILE=compose.yaml:compose.jellyfin.yaml:compose.languagetool.yaml`.
@ -225,3 +226,49 @@ Steps, in order; ✱ marks a step as root.
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.
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.

View file

@ -0,0 +1,14 @@
# Halkyon Train's own map (compose.maps.yaml, ADR 0030, D50), inside Train's site
# (train.caddy imports it): Train's style, public, from its web server; and the tiles,
# each passed on only once Train's server takes the key it came with
# (`Authorization: Bearer`, /map/tile-key: 204 or 401). Caddy logs no request.
handle /map/style/* {
reverse_proxy train-server:8082
}
handle_path /tiles/* {
forward_auth train-server:8082 {
uri /map/tile-key
}
reverse_proxy map-tiles:8080
}

View file

@ -1,10 +1,18 @@
# Halkyon Train's site (compose.train.yaml, ADR 0030, D46): its API server alone. Its
# web server (8082) takes only the register's calls between the instance's services
# (`/halkyon-base/…`, ADR 0016), on the instance's own network, never through here.
# Halkyon Train's site (compose.train.yaml, ADR 0030, D46): its API server, and its own
# map where compose.maps.yaml switches it on (D50). Its web server (8082) takes the
# register's calls between the instance's services (`/halkyon-base/…`, ADR 0016) on the
# instance's own network, never through here; through here only the map's style and
# the tiles' check (train-maps.caddy).
{$TRAIN_DOMAIN} {
encode zstd gzip
# Train's own map where compose.maps.yaml mounts it (train-maps.caddy); none
# without.
import /etc/caddy/train/*.caddy
# Serverpod's streaming methods use WebSockets; reverse_proxy upgrades them as-is.
# The visitor's address comes last in X-Forwarded-For, as for Learn's server.
reverse_proxy train-server:8080
handle {
reverse_proxy train-server:8080
}
}