The server's half of D25: every account has a way in, and the anonymous accounts left from before go with the daily pruning. - The module signs nobody in anonymously: `anonymousIdp` and `gitHubUpgrade` are gone, and so is linking GitHub at the register. - A password is set only by someone whose address is theirs (a `firstPassword` code) or who signs in with GitHub alone (a fresh sign-in and a `signUp` code); an anonymous account is refused `wrongCredentials` before any code is used. - SimpleLogin refuses to link onto a caller without `identified` (`wrongCaller`), the member's endpoint too, so nothing turns an anonymous account into one with a way in. - `pruneAnonymousPeople` deletes every anonymous account left, however new, never one identified. Learn and Todo schedule it as `prune-accounts` and cancel the old `prune-abandoned-anonymous-people` entry once. - Learn: someone without `identified` keeps no files; `FILE_LIMIT_ANONYMOUS_MB` is gone. `minimumAppBuild` is 23, as builds up to 22 start with an anonymous account. - Learn's sync tests make their person and session by SQL in the development database, as Serverpod stores sessions. - Docs, store listings and ADRs 0010, 0014, 0016, 0017 and 0019 amended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
214 lines
10 KiB
YAML
214 lines
10 KiB
YAML
# The Halkyon Learn instance: Caddy in front, the Serverpod server, PostgreSQL, and
|
|
# the dictionaries kept current beside them.
|
|
#
|
|
# cd deploy
|
|
# cp .env.example .env && chmod 600 .env # then fill in the secrets
|
|
# docker compose up -d --build
|
|
#
|
|
# Only Caddy publishes ports. Docker opens published ports directly in the kernel's
|
|
# packet filter, past ufw, and the Serverpod server listens on every interface, so the
|
|
# server and Postgres publish nothing: they're reachable only inside this project's
|
|
# networks, where Caddy finds the server by its service name.
|
|
#
|
|
# Two networks (decided 2026-10-14): `edge`, Caddy's, which the server shares with it,
|
|
# and the internal default one, where the server reaches Postgres. Caddy never reaches
|
|
# Postgres, nor does a sidecar behind Caddy.
|
|
#
|
|
# Sidecars (decided 2026-10-14) are each a compose file of their own beside this one,
|
|
# switched on in .env (COMPOSE_FILE=compose.yaml:compose.jellyfin.yaml): each adds its
|
|
# service and mounts its site into Caddy's sidecars/ folder (sidecars/README.md).
|
|
|
|
name: halkyon-learn
|
|
|
|
# Logs go to the host's journal, whose size and age limits apply (journald.conf).
|
|
x-logging: &logging
|
|
driver: journald
|
|
|
|
services:
|
|
caddy:
|
|
image: caddy:2
|
|
restart: unless-stopped
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
- "443:443/udp" # HTTP/3
|
|
environment:
|
|
# `learn.halkyon.eu` on its server (automatic Let's Encrypt certificate);
|
|
# `http://localhost` for the local rehearsal (plain HTTP, no certificate).
|
|
DOMAIN: ${DOMAIN:?set DOMAIN in deploy/.env}
|
|
# The account domain whose profile pages this instance serves, where it isn't
|
|
# DOMAIN; empty for none.
|
|
ACCOUNT_SITE: ${ACCOUNT_DOMAIN:-}
|
|
volumes:
|
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
- caddy_data:/data # certificates: must survive restarts (rate limits)
|
|
- caddy_config:/config
|
|
networks:
|
|
edge:
|
|
# Fixed, so containers behind Caddy can trust it as their proxy (the
|
|
# visitor's own address comes in X-Forwarded-For): the server's
|
|
# TRUSTED_PROXIES, Jellyfin's known proxies. Both, as Caddy may reach
|
|
# the server by either.
|
|
ipv4_address: 172.30.80.2
|
|
ipv6_address: fd4a:6c6b:7e80::2
|
|
depends_on:
|
|
- server
|
|
logging: *logging
|
|
|
|
server:
|
|
build:
|
|
context: ..
|
|
dockerfile: apps/learn/halkyon_learn_server/Dockerfile
|
|
image: halkyon-learn-server
|
|
restart: unless-stopped
|
|
# Applies pending migrations at every start, then serves. One instance only, so
|
|
# no two servers ever migrate at once. (On learn.halkyon.eu, deploy/redeploy.sh has
|
|
# applied them in a run of their own before, so nothing is left to do here.)
|
|
command: ["--apply-migrations"]
|
|
environment:
|
|
SERVERPOD_DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in deploy/.env}
|
|
SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper: ${SESSION_KEY_HASH_PEPPER:?set SESSION_KEY_HASH_PEPPER in deploy/.env}
|
|
# GitHub sign-in, only with this instance's GitHub OAuth app; empty: off.
|
|
SERVERPOD_PASSWORD_githubClientId: ${GITHUB_CLIENT_ID:-}
|
|
SERVERPOD_PASSWORD_githubClientSecret: ${GITHUB_CLIENT_SECRET:-}
|
|
# Email sign-in (ADR 0010), only with the pepper and an SMTP service; empty: off.
|
|
SERVERPOD_PASSWORD_emailSignInPepper: ${EMAIL_SIGN_IN_PEPPER:-}
|
|
SERVERPOD_PASSWORD_emailSmtpHost: ${EMAIL_SMTP_HOST:-}
|
|
SERVERPOD_PASSWORD_emailSmtpPort: ${EMAIL_SMTP_PORT:-}
|
|
SERVERPOD_PASSWORD_emailSmtpUser: ${EMAIL_SMTP_USER:-}
|
|
SERVERPOD_PASSWORD_emailSmtpPassword: ${EMAIL_SMTP_PASSWORD:-}
|
|
SERVERPOD_PASSWORD_emailFrom: ${EMAIL_FROM:-}
|
|
# Password sign-in with TOTP (ADR 0017), only with these keys and email
|
|
# sign-in; empty: off. Each at least 32 characters, or the server stops.
|
|
SERVERPOD_PASSWORD_totpKey: ${TOTP_KEY:-}
|
|
SERVERPOD_PASSWORD_recoveryCodeKey: ${RECOVERY_CODE_KEY:-}
|
|
SERVERPOD_PASSWORD_signInDecoyKey: ${SIGN_IN_DECOY_KEY:-}
|
|
# SimpleLogin (ADR 0020), only with the instance's SimpleLogin app; empty: off.
|
|
# Its callback; empty: this server's own page, on its web server's public address
|
|
# (https://learn.halkyon.eu/simplelogin-callback).
|
|
SERVERPOD_PASSWORD_simpleLoginClientId: ${SIMPLELOGIN_CLIENT_ID:-}
|
|
SERVERPOD_PASSWORD_simpleLoginClientSecret: ${SIMPLELOGIN_CLIENT_SECRET:-}
|
|
SIMPLELOGIN_CALLBACK_URL: ${SIMPLELOGIN_CALLBACK_URL:-}
|
|
# The ways in offered, of those set up (`simplelogin password`); empty: all.
|
|
SIGN_IN_WAYS: ${SIGN_IN_WAYS:-}
|
|
# Caddy, whose X-Forwarded-For names the visitor the per-network limits count
|
|
# (codes by mail, password and SimpleLogin sign-ins): without it, every
|
|
# visitor would share each limit with everyone. Its address on `edge`, below.
|
|
TRUSTED_PROXIES: 172.30.80.2 fd4a:6c6b:7e80::2
|
|
# The day GitHub stops signing anyone in (`2027-01-22`); empty: no end.
|
|
GITHUB_SIGN_IN_ENDS: ${GITHUB_SIGN_IN_ENDS:-}
|
|
# Where the source of what this instance runs is; empty: the project's.
|
|
SOURCE_URL: ${SOURCE_URL:-}
|
|
# The domain people's handles are on; empty: DOMAIN's host.
|
|
ACCOUNT_DOMAIN: ${ACCOUNT_DOMAIN:-}
|
|
# The instance's other services (ADR 0016), whose accounts this server's
|
|
# register keeps: the secret every call between them carries, and their web
|
|
# servers, told when an account goes; empty: this is the instance's only
|
|
# service.
|
|
SERVERPOD_PASSWORD_instanceSecret: ${INSTANCE_SECRET:-}
|
|
INSTANCE_MEMBERS: ${INSTANCE_MEMBERS:-}
|
|
# The Jellyfin server suggested to people; empty: jellyfin. on the
|
|
# instance's own domain, if one answers there; `none`: none.
|
|
JELLYFIN_URL: ${JELLYFIN_URL:-}
|
|
# Where people's data and the backups are, which the app shows: `EU`, a
|
|
# country (`DE`) or a subdivision (`US-CA`); empty: it says nothing.
|
|
DATA_LOCATION: ${DATA_LOCATION:-}
|
|
BACKUP_LOCATION: ${BACKUP_LOCATION:-}
|
|
# How much room people's files may take, in MiB; empty: the defaults.
|
|
FILE_LIMIT_IDENTIFIED_MB: ${FILE_LIMIT_IDENTIFIED_MB:-}
|
|
FILE_LIMIT_INSTANCE_MB: ${FILE_LIMIT_INSTANCE_MB:-}
|
|
# The git repositories of published studysets (ADR 0005, rule 3).
|
|
REPOSITORIES_DIR: /app/repositories
|
|
# The dictionary files it loads at start and rereads hourly, those that changed
|
|
# (see the server's README): those the dictionaries service keeps current, read
|
|
# only. To supply files yourself instead, remove that service and mount
|
|
# ./dictionaries here (`- ./dictionaries:/app/dictionaries:ro`).
|
|
volumes:
|
|
- dictionaries:/app/dictionaries:ro
|
|
# Every release of every published studyset: backed up beside the database
|
|
# (backup.sh), never deleted with `down -v`.
|
|
- repositories:/app/repositories
|
|
networks:
|
|
- default
|
|
- edge
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
logging: *logging
|
|
|
|
# Keeps the dictionary files current from EDRDG's files of the day, once they are a
|
|
# week old (EDRDG's licence asks it of servers showing its dictionaries at least
|
|
# monthly; the curator's README), and the languages of DICTIONARY_LANGUAGES: Chinese
|
|
# from MDBG's CC-CEDICT as often, the rest from Wiktionary's dump whenever kaikki.org
|
|
# makes a new one (about monthly, some 3 GB, converted as it downloads): in a
|
|
# container of its own, the curator never in the server's image or process. It only
|
|
# downloads and writes into the volume; the files are never backed up, as they can be
|
|
# had again.
|
|
dictionaries:
|
|
build:
|
|
context: ..
|
|
dockerfile: apps/learn/halkyon_learn_curator/Dockerfile
|
|
image: halkyon-learn-dictionaries
|
|
restart: unless-stopped
|
|
# Docker's stop signal reaches the curator through a small init.
|
|
init: true
|
|
# JMdict's conversion takes some 400 MB for a minute a week, Wiktionary's some
|
|
# 300 MB for ten minutes a month, KRDict's some 600 MB for half a minute.
|
|
mem_limit: 1g
|
|
# Unset: German, Greek, English, Korean and Chinese; empty: none.
|
|
command: ["--every", "7", "--languages", "${DICTIONARY_LANGUAGES-de,el,en,ko,zh}"]
|
|
volumes:
|
|
- dictionaries:/dictionaries
|
|
logging: *logging
|
|
|
|
postgres:
|
|
image: postgres:16
|
|
restart: unless-stopped
|
|
# The server's session is UTC anyway (the driver sets it); this makes the
|
|
# database's own default UTC too, for psql, COPY loads and future sidecars.
|
|
command: ["postgres", "-c", "timezone=UTC", "-c", "log_timezone=UTC"]
|
|
environment:
|
|
POSTGRES_USER: halkyon_learn
|
|
POSTGRES_DB: halkyon_learn
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in deploy/.env}
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U halkyon_learn -d halkyon_learn"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 12
|
|
logging: *logging
|
|
|
|
networks:
|
|
edge:
|
|
name: halkyon-learn-edge
|
|
# IPv6 too (the second reading's M5), so Docker forwards a visitor's IPv6
|
|
# connection to Caddy as it is, rather than through its userland proxy,
|
|
# which would show every IPv6 visitor as the network's gateway and so make
|
|
# them share every per-network limit. Needs Docker 27 or later with ip6tables
|
|
# (its default; not switched off in /etc/docker/daemon.json), and the network
|
|
# made anew: `docker compose down` (never -v) and `up -d` once, as Compose
|
|
# refuses a network whose settings changed. A unique local /64 (RFC 4193), the
|
|
# host's own; the CI runner's unit denies fd00::/8.
|
|
enable_ipv6: true
|
|
ipam:
|
|
config:
|
|
- subnet: 172.30.80.0/24
|
|
gateway: 172.30.80.1
|
|
# Containers without an address of their own (the server) get one from the
|
|
# upper half, so the fixed ones below it (Caddy .2, sidecars from .10) are
|
|
# never taken: the server started first once and took Caddy's (2026-10-14).
|
|
# (With a range alone, Docker would move the gateway to its first address.)
|
|
ip_range: 172.30.80.128/25
|
|
- subnet: fd4a:6c6b:7e80::/64
|
|
gateway: fd4a:6c6b:7e80::1
|
|
# As above: Caddy's ::2 and the sidecars' below it stay free.
|
|
ip_range: fd4a:6c6b:7e80::8000:0/97
|
|
|
|
volumes:
|
|
caddy_data:
|
|
caddy_config:
|
|
dictionaries:
|
|
postgres_data:
|
|
repositories:
|