build(train): Train's server and BRouter deployable beside the instance (ADR 0030, D46)
deploy/compose.train.yaml adds Train's server at train.halkyon.eu, a member of Learn's register, with its database halkyon_train in the instance's PostgreSQL, and BRouter beside it alone (v1.7.10, checked by SHA-256, no log), its segments of the operator's region kept current monthly. Nothing changes until deploy/.env names the compose file: redeploy.sh and backup.sh take Train in only then, and the operator's steps are in sidecars/README.md. restic now forgets snapshots grouped by host alone, so adding Train's dump leaves no group behind for good. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
781576726a
commit
a8f7c26e69
9 changed files with 483 additions and 36 deletions
|
|
@ -37,13 +37,35 @@ DOMAIN=http://localhost
|
|||
|
||||
# Sidecars switched on beside the instance (sidecars/README.md), each by its compose file
|
||||
# after the instance's own: compose.jellyfin.yaml, a Jellyfin people watch and listen to
|
||||
# learn from. Unset: the instance alone.
|
||||
# learn from; compose.languagetool.yaml, spelling and grammar checked; compose.train.yaml,
|
||||
# Halkyon Train's server with its BRouter. Unset: the instance alone.
|
||||
#COMPOSE_FILE=compose.yaml:compose.jellyfin.yaml
|
||||
|
||||
# With the Jellyfin sidecar: the domain it answers at, which the instance then suggests
|
||||
# to its people (the app's Jellyfin sign-in fills it in).
|
||||
#JELLYFIN_DOMAIN=jellyfin.example.org
|
||||
|
||||
# With Halkyon Train beside the instance (compose.train.yaml, sidecars/README.md,
|
||||
# Train): the domain its apps reach it at, its database's password, its own session
|
||||
# pepper (never change it once people sign in), INSTANCE_SECRET above set, and the
|
||||
# region of BRouter's segments, `west,south,east,north` in whole degrees, multiples of
|
||||
# 5 (empty: Europe's, -25,35,40,70, about 3.3 GB). Generate each secret:
|
||||
# openssl rand -hex 32. Its server is told of accounts gone without INSTANCE_MEMBERS;
|
||||
# setting it, name Train's too (`http://train-server:8082/`).
|
||||
#TRAIN_DOMAIN=train.example.org
|
||||
#TRAIN_POSTGRES_PASSWORD=
|
||||
#TRAIN_SESSION_KEY_HASH_PEPPER=
|
||||
#BROUTER_REGION=
|
||||
# The Loop's firmware through the instance (Train's ADR 0030, D40): `off` to stop
|
||||
# relaying it; empty: on.
|
||||
#LOOP_FIRMWARE_RELAY=
|
||||
# Train's map (ADR 0030, D22): a MapLibre style over HTTPS, with its name, credit and
|
||||
# page; empty: OpenFreeMap's.
|
||||
#MAP_STYLE_URL=
|
||||
#MAP_SOURCE_NAME=
|
||||
#MAP_ATTRIBUTION=
|
||||
#MAP_ABOUT_URL=
|
||||
|
||||
# Postgres password, shared by the postgres and server services.
|
||||
# Generate: openssl rand -base64 32
|
||||
POSTGRES_PASSWORD=
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
# how it was set up and how to restore.
|
||||
#
|
||||
# Everything people keep is in the database, their files too (Serverpod's database
|
||||
# cloud storage), but for the releases of published studysets, which are git
|
||||
# cloud storage), and in Train's where .env switches Train on (compose.train.yaml,
|
||||
# Halkyon Train's ADR 0030, D46), but for the releases of published studysets, which are git
|
||||
# repositories in the server's volume: a dump and a bundle of each repository cover
|
||||
# the instance; the secrets in deploy/.env are kept in Proton Pass instead. restic stores the dump encrypted in a Hetzner Storage Box
|
||||
# and keeps 24 hourly, 7 daily, 4 weekly and 3 monthly snapshots (decided 2026-10-03).
|
||||
|
|
@ -35,6 +36,11 @@ cd "$(dirname "$0")"
|
|||
# the last hour and stores it only once; restic compresses what it stores.
|
||||
mkdir -p -m 700 "$HOME/backups"
|
||||
dump="$HOME/backups/latest.dump"
|
||||
train_dump="$HOME/backups/latest-train.dump"
|
||||
train=
|
||||
if docker compose config --services | grep -qx train-server; then
|
||||
train=yes
|
||||
fi
|
||||
(
|
||||
umask 077
|
||||
# Never during a deploy (redeploy.sh holds this lock): its migrations would wait
|
||||
|
|
@ -42,8 +48,14 @@ dump="$HOME/backups/latest.dump"
|
|||
exec 9>"$HOME/.redeploy.lock"
|
||||
flock 9
|
||||
docker compose exec -T postgres pg_dump -U halkyon_learn -Fc -Z 0 halkyon_learn >"$dump.partial"
|
||||
if [ -n "$train" ]; then
|
||||
docker compose exec -T postgres pg_dump -U halkyon_learn -Fc -Z 0 halkyon_train >"$train_dump.partial"
|
||||
fi
|
||||
)
|
||||
mv "$dump.partial" "$dump"
|
||||
if [ -n "$train" ]; then
|
||||
mv "$train_dump.partial" "$train_dump"
|
||||
fi
|
||||
|
||||
# Each repository as a git bundle, after the dump: a release the dump lacks is never
|
||||
# numbered again (the server numbers past the highest tag), while one the bundles
|
||||
|
|
@ -67,8 +79,15 @@ releases="$HOME/backups/repositories.tar"
|
|||
)
|
||||
mv "$releases.partial" "$releases"
|
||||
|
||||
restic backup --quiet "$dump" "$releases"
|
||||
restic forget --quiet --keep-hourly 24 --keep-daily 7 --keep-weekly 4 --keep-monthly 3
|
||||
if [ -n "$train" ]; then
|
||||
restic backup --quiet "$dump" "$train_dump" "$releases"
|
||||
else
|
||||
restic backup --quiet "$dump" "$releases"
|
||||
fi
|
||||
# By host alone: restic groups by the paths backed up too, so a path added (Train's
|
||||
# dump) would leave the snapshots before it a group of their own, never forgotten, and
|
||||
# a deleted account in them for good.
|
||||
restic forget --quiet --group-by host --keep-hourly 24 --keep-daily 7 --keep-weekly 4 --keep-monthly 3
|
||||
|
||||
# Once a day, in the run at 03:00: free the space of forgotten snapshots, and read the
|
||||
# repository's structure; on Sundays also a tenth of the stored data, chosen at random,
|
||||
|
|
|
|||
|
|
@ -196,6 +196,21 @@ the way back from a restore that went wrong.
|
|||
docker compose up -d
|
||||
```
|
||||
|
||||
Train's database, where `.env` switches Train on (`compose.train.yaml`, Halkyon Train's
|
||||
ADR 0030, D46), is in the same snapshots as `latest-train.dump`, and is restored the
|
||||
same way, its server stopped meanwhile (`docker compose stop train-server`), from the
|
||||
same snapshot as Learn's, so that the register's accounts and Train's people agree:
|
||||
|
||||
```sh
|
||||
restic dump <snapshot> /home/deploy/backups/latest-train.dump >~/backups/restore-train.dump
|
||||
docker compose exec -T postgres \
|
||||
pg_restore -U halkyon_learn -d postgres --clean --create <~/backups/restore-train.dump
|
||||
```
|
||||
|
||||
`pg_restore --create` makes the database anew owned as it was, by `halkyon_train`,
|
||||
whose role is the cluster's and so survives the restore (on a new server, make it first:
|
||||
`sidecars/README.md`, *Train*, step 2).
|
||||
|
||||
To check a backup without touching the instance, restore it into a database of its own
|
||||
next to the real one, look, and drop it:
|
||||
|
||||
|
|
|
|||
32
deploy/brouter/Dockerfile
Normal file
32
deploy/brouter/Dockerfile
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# BRouter for Train's routes (ADR 0030, D11, D44, D46): its release, checked against
|
||||
# GitHub's SHA-256 of it, on Eclipse Temurin's Java 17 as BRouter's own image, with
|
||||
# its profiles and Train's running profile. Built from the repository root:
|
||||
#
|
||||
# docker build -f deploy/brouter/Dockerfile .
|
||||
#
|
||||
# The same image refreshes the segments (segments.sh), so it carries curl.
|
||||
FROM eclipse-temurin:17-jre-noble
|
||||
|
||||
# BRouter v1.7.10, released 17 Jul 2026.
|
||||
ADD --checksum=sha256:023fec3ba997758e8cd7ab9e1bae52e962af3f00b57683e3de86b84ffad01532 \
|
||||
https://github.com/abrensch/brouter/releases/download/v1.7.10/brouter-1.7.10.zip /tmp/brouter.zip
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends unzip curl ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& unzip -q /tmp/brouter.zip -d /tmp \
|
||||
&& mkdir -p /app \
|
||||
&& mv /tmp/brouter-1.7.10/brouter-1.7.10-all.jar /app/brouter.jar \
|
||||
&& mv /tmp/brouter-1.7.10/profiles2 /profiles2 \
|
||||
&& rm -rf /tmp/brouter.zip /tmp/brouter-1.7.10
|
||||
COPY apps/train/brouter/halkyon-run.brf /profiles2/halkyon-run.brf
|
||||
COPY --chmod=755 deploy/brouter/segments.sh /app/segments.sh
|
||||
|
||||
WORKDIR /app
|
||||
EXPOSE 17777
|
||||
|
||||
# Segments in /segments4 (a volume), profiles in the image, no custom profiles; four
|
||||
# routes at a time, each stopped after a minute. -Xmx as BRouter's script raised: the
|
||||
# memory a route takes grows with its length, and Europe's long routes need more than
|
||||
# its 128 MB.
|
||||
CMD ["java", "-Xmx384M", "-Xms128M", "-Xmn16M", "-DmaxRunningTime=60", "-DuseRFCMimeType=false", "-DusePOSTRequests=false", \
|
||||
"-cp", "/app/brouter.jar", "btools.server.RouteServer", "/segments4", "/profiles2", "/tmp", "17777", "4", "0.0.0.0"]
|
||||
58
deploy/brouter/segments.sh
Executable file
58
deploy/brouter/segments.sh
Executable file
|
|
@ -0,0 +1,58 @@
|
|||
#!/bin/sh
|
||||
# Keeps BRouter's segments of the operator's region current (ADR 0030, D11, D44):
|
||||
# every 5°×5° segment of BRouter's project whose south-west corner lies in
|
||||
# BROUTER_REGION (`west,south,east,north` in whole degrees, multiples of 5; Europe's
|
||||
# `-25,35,40,70` by default), downloaded where the project has a newer one than the
|
||||
# volume holds, then again every BROUTER_REFRESH_DAYS days (30). Each is written beside
|
||||
# its old self and moved over it, so BRouter never reads half a file. Segments of
|
||||
# the sea, which the project doesn't have, are skipped.
|
||||
set -eu
|
||||
|
||||
base=https://brouter.de/brouter/segments4
|
||||
region=${BROUTER_REGION:--25,35,40,70}
|
||||
days=${BROUTER_REFRESH_DAYS:-30}
|
||||
cd /segments4
|
||||
|
||||
IFS=, read -r west south east north <<END
|
||||
$region
|
||||
END
|
||||
|
||||
name() {
|
||||
lon=$1 lat=$2
|
||||
if [ "$lon" -lt 0 ]; then ew="W$((-lon))"; else ew="E$lon"; fi
|
||||
if [ "$lat" -lt 0 ]; then ns="S$((-lat))"; else ns="N$lat"; fi
|
||||
echo "${ew}_${ns}.rd5"
|
||||
}
|
||||
|
||||
refresh() {
|
||||
listing=$(curl -fsS "$base/") || { echo "The segments' listing didn't come." >&2; return 0; }
|
||||
fetched=0 kept=0
|
||||
lon=$west
|
||||
while [ "$lon" -le "$east" ]; do
|
||||
lat=$south
|
||||
while [ "$lat" -le "$north" ]; do
|
||||
file=$(name "$lon" "$lat")
|
||||
if echo "$listing" | grep -q "href=\"$file\""; then
|
||||
# Only where the project's is newer than ours (If-Modified-Since): a
|
||||
# current one comes back empty.
|
||||
if [ -e "$file" ]; then set -- -z "$file"; else set --; fi
|
||||
if curl -fsS -R --retry 3 "$@" -o "$file.part" "$base/$file" &&
|
||||
[ -s "$file.part" ]; then
|
||||
mv "$file.part" "$file"
|
||||
fetched=$((fetched + 1))
|
||||
else
|
||||
rm -f "$file.part"
|
||||
kept=$((kept + 1))
|
||||
fi
|
||||
fi
|
||||
lat=$((lat + 5))
|
||||
done
|
||||
lon=$((lon + 5))
|
||||
done
|
||||
echo "Segments: $fetched fetched, $kept current, $(du -sh . | cut -f1) in all."
|
||||
}
|
||||
|
||||
while :; do
|
||||
refresh
|
||||
sleep $((days * 86400))
|
||||
done
|
||||
114
deploy/compose.train.yaml
Normal file
114
deploy/compose.train.yaml
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# Halkyon Train's server beside the instance (ADR 0030, D46), with the BRouter that
|
||||
# plans its routes (D11, D44). Switched on in .env, with Train's domain and secrets:
|
||||
#
|
||||
# COMPOSE_FILE=compose.yaml:compose.train.yaml
|
||||
# TRAIN_DOMAIN=train.halkyon.eu
|
||||
#
|
||||
# Train is a member of the instance's register, which Learn's server holds (ADR 0016):
|
||||
# one account a person, signing in through the register, the shared INSTANCE_SECRET
|
||||
# on every call between them, which reach each other on the instance's own networks
|
||||
# alone. Its database is `halkyon_train` in the instance's PostgreSQL, under a role of
|
||||
# its own that can't connect to Learn's. BRouter has no site and no sign-in: only
|
||||
# Train's server reaches it, on a network of their own, and asks it nothing that is
|
||||
# logged. Its segments (Europe's, about 3.3 GB) are in ./brouter/segments, which the
|
||||
# refresher keeps current monthly, owned by a user ID of their own, which no person,
|
||||
# no other container and not the CI runner uses. Setting it up: sidecars/README.md.
|
||||
|
||||
services:
|
||||
train-server:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: apps/train/halkyon_train_server/Dockerfile
|
||||
image: halkyon-train-server
|
||||
restart: unless-stopped
|
||||
# As Learn's: migrations at every start (redeploy.sh has applied them before).
|
||||
command: ["--apply-migrations"]
|
||||
environment:
|
||||
SERVERPOD_DATABASE_PASSWORD: ${TRAIN_POSTGRES_PASSWORD:?set TRAIN_POSTGRES_PASSWORD in deploy/.env}
|
||||
SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper: ${TRAIN_SESSION_KEY_HASH_PEPPER:?set TRAIN_SESSION_KEY_HASH_PEPPER in deploy/.env}
|
||||
# The register (ADR 0016): Learn's web server, on the instance's own network,
|
||||
# and the secret every call between them carries.
|
||||
ACCOUNT_REGISTER_URL: http://server:8082/
|
||||
SERVERPOD_PASSWORD_instanceSecret: ${INSTANCE_SECRET:?set INSTANCE_SECRET in deploy/.env}
|
||||
# Caddy, as for Learn's server (compose.yaml).
|
||||
TRUSTED_PROXIES: 172.30.80.2 fd4a:6c6b:7e80::2
|
||||
SOURCE_URL: ${SOURCE_URL:-}
|
||||
DATA_LOCATION: ${DATA_LOCATION:-}
|
||||
BACKUP_LOCATION: ${BACKUP_LOCATION:-}
|
||||
# Routes (D44), from the BRouter below.
|
||||
BROUTER_URL: http://brouter:17777/
|
||||
# The Loop's firmware through the instance (D40): on unless `off`.
|
||||
LOOP_FIRMWARE_RELAY: ${LOOP_FIRMWARE_RELAY:-}
|
||||
# The map source (D22); empty: OpenFreeMap.
|
||||
MAP_STYLE_URL: ${MAP_STYLE_URL:-}
|
||||
MAP_SOURCE_NAME: ${MAP_SOURCE_NAME:-}
|
||||
MAP_ATTRIBUTION: ${MAP_ATTRIBUTION:-}
|
||||
MAP_ABOUT_URL: ${MAP_ABOUT_URL:-}
|
||||
networks:
|
||||
- default
|
||||
- edge
|
||||
- routing
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
logging:
|
||||
driver: journald
|
||||
|
||||
brouter:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/brouter/Dockerfile
|
||||
image: halkyon-brouter
|
||||
restart: unless-stopped
|
||||
user: "61177:61177"
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:size=16m,uid=61177,gid=61177
|
||||
volumes:
|
||||
- ./brouter/segments:/segments4:ro
|
||||
# Beside Train's server alone: no site, no published port, never beside Caddy or
|
||||
# Postgres.
|
||||
networks:
|
||||
- routing
|
||||
mem_limit: 640m
|
||||
# No log at all: BRouter writes each route's waypoints to its output, and where
|
||||
# people run must never be kept. To look into a failure, switch to journald for
|
||||
# a while.
|
||||
logging:
|
||||
driver: none
|
||||
|
||||
brouter-segments:
|
||||
image: halkyon-brouter
|
||||
restart: unless-stopped
|
||||
user: "61177:61177"
|
||||
read_only: true
|
||||
command: ["/app/segments.sh"]
|
||||
environment:
|
||||
# `west,south,east,north` of the segments' south-west corners; Europe's.
|
||||
BROUTER_REGION: ${BROUTER_REGION:--25,35,40,70}
|
||||
volumes:
|
||||
- ./brouter/segments:/segments4
|
||||
# Out to brouter.de alone; on a network of its own, beside nothing.
|
||||
networks:
|
||||
- downloads
|
||||
mem_limit: 64m
|
||||
logging:
|
||||
driver: journald
|
||||
|
||||
caddy:
|
||||
environment:
|
||||
TRAIN_DOMAIN: ${TRAIN_DOMAIN:?set TRAIN_DOMAIN in deploy/.env}
|
||||
volumes:
|
||||
- ./sidecars/train.caddy:/etc/caddy/sidecars/train.caddy:ro
|
||||
|
||||
server:
|
||||
environment:
|
||||
# Train's web server, told when an account goes (ADR 0016); set
|
||||
# INSTANCE_MEMBERS in .env to name more members, Train's among them.
|
||||
INSTANCE_MEMBERS: ${INSTANCE_MEMBERS:-http://train-server:8082/}
|
||||
|
||||
networks:
|
||||
# Train's server and BRouter, without a way out.
|
||||
routing:
|
||||
internal: true
|
||||
downloads:
|
||||
|
|
@ -10,7 +10,8 @@
|
|||
# - as a fast-forward of the deployed commit,
|
||||
# - if every new commit is signed by a key in deploy/allowed-signers, as the deployed
|
||||
# checkout lists them (so no commit can add its own key),
|
||||
# dumps the database first (the last 3 dumps stay in ~/backups), applies the migrations
|
||||
# dumps the database first (the last 3 dumps stay in ~/backups; Train's too where .env
|
||||
# switches Train on, compose.train.yaml), applies the migrations
|
||||
# before switching to the new server, starts it once more after a migration (see below),
|
||||
# and fails unless that server answers afterwards. A failure leaves the checkout at the deployed commit, so
|
||||
# running the CI job again retries. By hand, with your own key, which passes the commit as CI does:
|
||||
|
|
@ -59,15 +60,30 @@ main() {
|
|||
git merge -q --ff-only "$commit"
|
||||
trap back_out EXIT
|
||||
cd deploy
|
||||
# Train's server, where .env switches it on (compose.train.yaml): built, dumped,
|
||||
# migrated and waited for as Learn's.
|
||||
train=
|
||||
if docker compose config --services | grep -qx train-server; then
|
||||
train=yes
|
||||
fi
|
||||
# Build while the running server keeps serving; then dump, migrate, switch.
|
||||
docker compose build -q server dictionaries
|
||||
dump
|
||||
if [ -n "$train" ]; then
|
||||
docker compose build -q train-server brouter
|
||||
fi
|
||||
dump halkyon_learn
|
||||
if [ -n "$train" ]; then
|
||||
dump halkyon_train
|
||||
fi
|
||||
# Migrations as a run of their own, before the switch: Serverpod's maintenance role
|
||||
# applies them and exits with 0 only if that worked. Each migration is one
|
||||
# transaction, so a failing one stops the deploy here and the running server keeps
|
||||
# serving the schema it knows. (Until the switch, the old server serves the new
|
||||
# schema; migrations that only add, as they should, keep that harmless.)
|
||||
docker compose run --rm --no-deps -e role=maintenance server --apply-migrations
|
||||
if [ -n "$train" ]; then
|
||||
docker compose run --rm --no-deps -e role=maintenance train-server --apply-migrations
|
||||
fi
|
||||
switched=yes
|
||||
docker compose up -d --remove-orphans
|
||||
# Caddy reads its Caddyfile when it starts, and `up` doesn't restart it for a
|
||||
|
|
@ -79,7 +95,10 @@ main() {
|
|||
if ! git diff --quiet "$deployed" "$commit" -- Caddyfile 'sidecars/*.caddy'; then
|
||||
docker compose up -d --force-recreate --no-deps caddy
|
||||
fi
|
||||
wait_until_answering
|
||||
wait_until_answering server
|
||||
if [ -n "$train" ]; then
|
||||
wait_until_answering train-server
|
||||
fi
|
||||
# After a migration the new server is started once more. The sync engine
|
||||
# (serverpod_offline_sync v0.0.10) reconciles its registry of synced tables as a
|
||||
# server first uses the database; when a migration changed a synced table, that
|
||||
|
|
@ -90,42 +109,50 @@ main() {
|
|||
# finds the registry as the first start left it.
|
||||
if ! git diff --quiet "$deployed" "$commit" -- ../apps/learn/halkyon_learn_server/migrations; then
|
||||
docker compose restart server
|
||||
wait_until_answering
|
||||
wait_until_answering server
|
||||
fi
|
||||
# Train's server too: it syncs through the same engine.
|
||||
if [ -n "$train" ] &&
|
||||
! git diff --quiet "$deployed" "$commit" -- ../apps/train/halkyon_train_server/migrations; then
|
||||
docker compose restart train-server
|
||||
wait_until_answering train-server
|
||||
fi
|
||||
trap - EXIT
|
||||
docker image prune -f >/dev/null
|
||||
echo "Deployed $commit."
|
||||
}
|
||||
|
||||
# Dumps the database, as it is before the new server applies its migrations.
|
||||
# Dumps the database $1 (halkyon_learn, halkyon_train), as it is before the new server
|
||||
# applies its migrations; as the instance's superuser, halkyon_learn.
|
||||
dump() {
|
||||
mkdir -p "$HOME/backups"
|
||||
file="$HOME/backups/halkyon_learn-$(git rev-parse --short "$deployed")-$(date -u +%Y%m%dT%H%M%SZ).dump"
|
||||
file="$HOME/backups/$1-$(git rev-parse --short "$deployed")-$(date -u +%Y%m%dT%H%M%SZ).dump"
|
||||
(
|
||||
umask 077
|
||||
docker compose exec -T postgres pg_dump -U halkyon_learn -Fc halkyon_learn >"$file"
|
||||
docker compose exec -T postgres pg_dump -U halkyon_learn -Fc "$1" >"$file"
|
||||
)
|
||||
echo "Database dumped to $file ($(du -h "$file" | cut -f1))."
|
||||
# Only the newest 3 (decided 2026-10-02): they are the deploys' way back, not backups;
|
||||
# those go off-site. Every file people keep is in each of them, so fewer leave room.
|
||||
# (ls is safe here: the names are ours, letters, digits and dashes.)
|
||||
# shellcheck disable=SC2012
|
||||
ls -1t "$HOME"/backups/halkyon_learn-*.dump | tail -n +4 | xargs -r rm --
|
||||
ls -1t "$HOME"/backups/"$1"-*.dump | tail -n +4 | xargs -r rm --
|
||||
}
|
||||
|
||||
# Waits up to a minute for the new server to answer inside the stack's network.
|
||||
# Waits up to a minute for the new server $1 (server, train-server) to answer inside the
|
||||
# stack's network.
|
||||
wait_until_answering() {
|
||||
tries=0
|
||||
while [ $tries -lt 30 ]; do
|
||||
if docker compose exec -T caddy wget -q -O /dev/null http://server:8080/ 2>/dev/null; then
|
||||
echo 'The server answers.'
|
||||
if docker compose exec -T caddy wget -q -O /dev/null "http://$1:8080/" 2>/dev/null; then
|
||||
echo "The $1 answers."
|
||||
return 0
|
||||
fi
|
||||
tries=$((tries + 1))
|
||||
sleep 2
|
||||
done
|
||||
docker compose logs --tail 40 server >&2
|
||||
fail "the server doesn't answer a minute after the switch"
|
||||
docker compose logs --tail 40 "$1" >&2
|
||||
fail "the $1 doesn't answer a minute after the switch"
|
||||
}
|
||||
|
||||
# A failed deploy puts the checkout back at the deployed commit, so running the CI job
|
||||
|
|
|
|||
|
|
@ -19,6 +19,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` |
|
||||
|
||||
Both together: `COMPOSE_FILE=compose.yaml:compose.jellyfin.yaml:compose.languagetool.yaml`.
|
||||
|
||||
|
|
@ -151,3 +152,76 @@ Steps, in order; ✱ marks a step as root.
|
|||
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.
|
||||
|
||||
## 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`
|
||||
says when); until then BRouter finds no route.
|
||||
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.
|
||||
|
||||
|
|
|
|||
10
deploy/sidecars/train.caddy
Normal file
10
deploy/sidecars/train.caddy
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# 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.
|
||||
{$TRAIN_DOMAIN} {
|
||||
encode zstd gzip
|
||||
|
||||
# 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
|
||||
}
|
||||
Loading…
Reference in a new issue