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:
parent
a848b71bd4
commit
190c3872f4
8 changed files with 3726 additions and 97 deletions
|
|
@ -65,6 +65,12 @@ DOMAIN=http://localhost
|
|||
#MAP_SOURCE_NAME=
|
||||
#MAP_ATTRIBUTION=
|
||||
#MAP_ABOUT_URL=
|
||||
# With Train's own map (compose.maps.yaml, sidecars/README.md, Train's map; ADR 0030,
|
||||
# D50), after compose.train.yaml in COMPOSE_FILE: the secret its tiles' keys are made
|
||||
# with (openssl rand -hex 32), and the region cut, `west,south,east,north` in degrees
|
||||
# (empty: Europe's, -25,35,45,75). MAP_STYLE_URL is then not used.
|
||||
#MAP_KEY_SECRET=
|
||||
#MAP_REGION=
|
||||
|
||||
# Postgres password, shared by the postgres and server services.
|
||||
# Generate: openssl rand -base64 32
|
||||
|
|
|
|||
68
deploy/compose.maps.yaml
Normal file
68
deploy/compose.maps.yaml
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# The instance's own map for Halkyon Train (ADR 0030, D50), beside compose.train.yaml,
|
||||
# which it needs: Protomaps' basemap cut for Europe, served by pmtiles as ordinary
|
||||
# tiles, which Train's style (served by Train's server) draws. Switched on in .env:
|
||||
#
|
||||
# COMPOSE_FILE=compose.yaml:compose.train.yaml:compose.maps.yaml
|
||||
# MAP_KEY_SECRET=<openssl rand -hex 32>
|
||||
#
|
||||
# Only people signed in to Train get tiles: Caddy asks Train's server about the key
|
||||
# each tile comes with before passing it on (sidecars/train-maps.caddy). Neither
|
||||
# logs a tile, as what was asked shows the area viewed. The cut (some 45 GB, to be
|
||||
# measured) is in ./maps/tiles, renewed monthly, owned by a user ID of its own, which
|
||||
# no person, no other container and not the CI runner uses. Setting it up:
|
||||
# sidecars/README.md, Train's map.
|
||||
|
||||
services:
|
||||
map-tiles:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/maps/Dockerfile
|
||||
image: halkyon-maps
|
||||
restart: unless-stopped
|
||||
user: "61178:61178"
|
||||
read_only: true
|
||||
volumes:
|
||||
- ./maps/tiles:/tiles:ro
|
||||
# Beside Caddy alone: no site of its own, no published port, no way out.
|
||||
networks:
|
||||
- tiles
|
||||
mem_limit: 512m
|
||||
# No log: a tile's address is where someone looked.
|
||||
logging:
|
||||
driver: none
|
||||
|
||||
map-tiles-refresh:
|
||||
image: halkyon-maps
|
||||
restart: unless-stopped
|
||||
user: "61178:61178"
|
||||
read_only: true
|
||||
command: ["/app/tiles.sh"]
|
||||
environment:
|
||||
# `west,south,east,north`; Europe's.
|
||||
MAP_REGION: ${MAP_REGION:--25,35,45,75}
|
||||
volumes:
|
||||
- ./maps/tiles:/tiles
|
||||
# Out to Protomaps' builds alone; on a network of its own, beside nothing.
|
||||
networks:
|
||||
- downloads
|
||||
mem_limit: 512m
|
||||
logging:
|
||||
driver: journald
|
||||
|
||||
train-server:
|
||||
environment:
|
||||
# The tiles as Caddy serves them, and the secret their keys are made with.
|
||||
MAP_TILES_URL: https://${TRAIN_DOMAIN}/tiles/europe/{z}/{x}/{y}.mvt
|
||||
SERVERPOD_PASSWORD_mapKeySecret: ${MAP_KEY_SECRET:?set MAP_KEY_SECRET in deploy/.env}
|
||||
|
||||
caddy:
|
||||
volumes:
|
||||
- ./sidecars/train-maps.caddy:/etc/caddy/train/maps.caddy:ro
|
||||
networks:
|
||||
tiles: {}
|
||||
|
||||
networks:
|
||||
# Caddy and the tiles, without a way out.
|
||||
tiles:
|
||||
internal: true
|
||||
downloads:
|
||||
22
deploy/maps/Dockerfile
Normal file
22
deploy/maps/Dockerfile
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# The instance's own map tiles (Halkyon Train's ADR 0030, D50): go-pmtiles, checked
|
||||
# against GitHub's SHA-256 of its release, serving the region's cut of Protomaps'
|
||||
# basemap, and cutting it anew each month (tiles.sh). Built from the repository root:
|
||||
#
|
||||
# docker build -f deploy/maps/Dockerfile .
|
||||
FROM alpine:3.22
|
||||
|
||||
# go-pmtiles v1.31.2, released 22 Jul 2026 (BSD-3-Clause).
|
||||
ADD --checksum=sha256:3ed7dbf4ec2e6dfe5e25b6f70d1ffc932729f93c86db353bf514dd71010a312f \
|
||||
https://github.com/protomaps/go-pmtiles/releases/download/v1.31.2/go-pmtiles_1.31.2_Linux_x86_64.tar.gz /tmp/pmtiles.tar.gz
|
||||
RUN apk add --no-cache curl ca-certificates \
|
||||
&& mkdir -p /app \
|
||||
&& tar -xzf /tmp/pmtiles.tar.gz -C /app pmtiles \
|
||||
&& rm /tmp/pmtiles.tar.gz
|
||||
COPY --chmod=755 deploy/maps/tiles.sh /app/tiles.sh
|
||||
|
||||
ENV PATH="/app:$PATH"
|
||||
EXPOSE 8080
|
||||
|
||||
# The tiles in /tiles (a volume), `/<name>/{z}/{x}/{y}.mvt` for each `<name>.pmtiles`;
|
||||
# no TileJSON (Train's style names the tiles itself), no CORS (no browser asks).
|
||||
CMD ["pmtiles", "serve", "/tiles", "--port", "8080", "--cache-size", "256"]
|
||||
43
deploy/maps/tiles.sh
Normal file
43
deploy/maps/tiles.sh
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#!/bin/sh
|
||||
# Keeps the instance's own map tiles current (Halkyon Train's ADR 0030, D50): the
|
||||
# newest of Protomaps' daily builds of the planet, cut to MAP_REGION (`west,south,
|
||||
# east,north` in degrees; by default Europe's, the box BRouter's segments cover) at
|
||||
# zooms 0 to 15, into /tiles/europe.pmtiles; again every MAP_REFRESH_DAYS days (30)
|
||||
# where a newer build has come. The cut is written into /tiles/next/ and moved over
|
||||
# the old one, so the server never reads half a file; until it is done, the old one
|
||||
# serves. Both at once take twice a cut's room.
|
||||
set -eu
|
||||
|
||||
builds=https://build-metadata.protomaps.dev/builds.json
|
||||
region=${MAP_REGION:--25,35,45,75}
|
||||
days=${MAP_REFRESH_DAYS:-30}
|
||||
cd /tiles
|
||||
mkdir -p next
|
||||
|
||||
refresh() {
|
||||
build=$(curl -fsS "$builds" | grep -o '"key": *"[0-9]\{8\}\.pmtiles"' |
|
||||
grep -o '[0-9]\{8\}\.pmtiles' | sort | tail -n 1) || build=
|
||||
if [ -z "$build" ]; then
|
||||
echo "Protomaps' builds didn't come; the tiles stay as they are." >&2
|
||||
return 0
|
||||
fi
|
||||
if [ -e europe.build ] && [ "$(cat europe.build)" = "$build" ]; then
|
||||
echo "The tiles are $build's, the newest."
|
||||
return 0
|
||||
fi
|
||||
rm -f next/europe.pmtiles
|
||||
if pmtiles extract "https://build.protomaps.com/$build" next/europe.pmtiles \
|
||||
--bbox="$region" --maxzoom=15 --download-threads=4; then
|
||||
mv next/europe.pmtiles europe.pmtiles
|
||||
echo "$build" >europe.build
|
||||
echo "The tiles are $build's now, $(du -h europe.pmtiles | cut -f1)."
|
||||
else
|
||||
rm -f next/europe.pmtiles
|
||||
echo "The cut of $build failed; the tiles stay as they are." >&2
|
||||
fi
|
||||
}
|
||||
|
||||
while :; do
|
||||
refresh
|
||||
sleep $((days * 86400))
|
||||
done
|
||||
|
|
@ -71,6 +71,10 @@ main() {
|
|||
if [ -n "$train" ]; then
|
||||
docker compose build -q train-server brouter
|
||||
fi
|
||||
# Train's own map, where .env switches it on (compose.maps.yaml).
|
||||
if docker compose config --services | grep -qx map-tiles; then
|
||||
docker compose build -q map-tiles
|
||||
fi
|
||||
dump halkyon_learn
|
||||
if [ -n "$train" ]; then
|
||||
dump halkyon_train
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
14
deploy/sidecars/train-maps.caddy
Normal file
14
deploy/sidecars/train-maps.caddy
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue