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
2 changed files with 3726 additions and 97 deletions

22
deploy/maps/Dockerfile Normal file
View 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
View 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