halkyon/deploy/compose.yaml
Tizian 「ティツィアーン」 af1dbee509
build: Melos, a Dart check and every path for the single repository
- Melos 8.9 as the root's dev dependency, with scripts across the
  workspace (analyze, test:dart, test:flutter, generate, check) and its
  filters (--diff, --include-dependents); cli_util overridden to 0.5.2
  for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only
  its logger, unchanged in 0.5.
- tool/check.dart in place of tool/check.sh: packages, formatting of our
  own files, analysis, the tools' tests, then every package's tests
  through Melos (--changed-since=<commit> for only what changed and what
  depends on it). tool/pre-push is the hook: the signature check, then
  the checks. CI generates every server's code and runs the same check.
- The images build from the root with a workspace narrowed to what they
  need (tool/workspace_only.sh); compose, the deploy's migration test,
  the snap, the release script, the editor's launch files and Learn's
  asset tools (run from apps/learn) follow the new layout.
- The project's source is named as project-halkyon/halkyon (source link,
  snap, F-Droid listing, release uploads); the AGPL packages carry the
  licence's own text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-22 18:11:04 +02:00

174 lines
7.6 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).
ipv4_address: 172.30.80.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:-}
# 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 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_ANONYMOUS_MB: ${FILE_LIMIT_ANONYMOUS_MB:-}
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
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
volumes:
caddy_data:
caddy_config:
dictionaries:
postgres_data:
repositories: