feat(deploy): Docker Compose stack for the instance
Caddy, the Serverpod server and PostgreSQL 16 in deploy/. Only Caddy publishes ports; the server and Postgres are reachable only inside the Compose network (Docker bypasses ufw, and the server listens on every interface). Secrets come from deploy/.env as environment variables; there is no production passwords.yaml, and .dockerignore keeps local secrets out of the build context.
The server image builds on Serverpod's template with an Ubuntu 24.04 runtime: complete glibc incl. libgcc_s for the SQLite libraries Serverpod 4 bundles, CA certificates, a non-root user, and exec so SIGTERM reaches the server. The scaffold's dev-only docker-compose.yaml is removed.
Verified locally: image builds, all native libraries resolve, no passwords.yaml in the image, migrations apply at start, Caddy serves 200, port 8080 unreachable from the host, Postgres in UTC.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-27 17:18:31 +02:00
|
|
|
# Copy to deploy/.env, restrict it (chmod 600), and fill in. deploy/.env is git-ignored
|
2026-10-13 23:11:40 +02:00
|
|
|
# and never goes into an image. How it reaches the server is decided when the server is
|
feat(deploy): Docker Compose stack for the instance
Caddy, the Serverpod server and PostgreSQL 16 in deploy/. Only Caddy publishes ports; the server and Postgres are reachable only inside the Compose network (Docker bypasses ufw, and the server listens on every interface). Secrets come from deploy/.env as environment variables; there is no production passwords.yaml, and .dockerignore keeps local secrets out of the build context.
The server image builds on Serverpod's template with an Ubuntu 24.04 runtime: complete glibc incl. libgcc_s for the SQLite libraries Serverpod 4 bundles, CA certificates, a non-root user, and exec so SIGTERM reaches the server. The scaffold's dev-only docker-compose.yaml is removed.
Verified locally: image builds, all native libraries resolve, no passwords.yaml in the image, migrations apply at start, Caddy serves 200, port 8080 unreachable from the host, Postgres in UTC.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-27 17:18:31 +02:00
|
|
|
# set up (e.g. rendered from Proton Pass on the deploying machine and copied over SSH,
|
|
|
|
|
# so the server itself never holds Proton credentials).
|
|
|
|
|
|
2026-10-13 23:11:40 +02:00
|
|
|
# `http://localhost` for the local rehearsal; `learn.halkyon.eu` on its server.
|
feat(deploy): Docker Compose stack for the instance
Caddy, the Serverpod server and PostgreSQL 16 in deploy/. Only Caddy publishes ports; the server and Postgres are reachable only inside the Compose network (Docker bypasses ufw, and the server listens on every interface). Secrets come from deploy/.env as environment variables; there is no production passwords.yaml, and .dockerignore keeps local secrets out of the build context.
The server image builds on Serverpod's template with an Ubuntu 24.04 runtime: complete glibc incl. libgcc_s for the SQLite libraries Serverpod 4 bundles, CA certificates, a non-root user, and exec so SIGTERM reaches the server. The scaffold's dev-only docker-compose.yaml is removed.
Verified locally: image builds, all native libraries resolve, no passwords.yaml in the image, migrations apply at start, Caddy serves 200, port 8080 unreachable from the host, Postgres in UTC.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-27 17:18:31 +02:00
|
|
|
DOMAIN=http://localhost
|
|
|
|
|
|
|
|
|
|
# Postgres password, shared by the postgres and server services.
|
|
|
|
|
# Generate: openssl rand -base64 32
|
|
|
|
|
POSTGRES_PASSWORD=
|
|
|
|
|
|
|
|
|
|
# Pepper for hashing session keys (server-side sessions). NEVER change it once
|
|
|
|
|
# sessions exist: everyone would be signed out, and anonymous accounts can't sign
|
|
|
|
|
# back in. Rotation only via fallbackSessionKeyHashPeppers.
|
|
|
|
|
# Generate: openssl rand -base64 32
|
|
|
|
|
SESSION_KEY_HASH_PEPPER=
|
feat(server): GitHub sign-in that keeps only the GitHub ID
As decided, a GitHub sign-in keeps nothing but the numeric GitHub ID, so the login is our own, on Serverpod's GitHub helpers (lib/src/auth/github.dart): Serverpod's stock login also stores the name and email in a profile and downloads the avatar, and its endpoint is never exposed. The server exchanges the authorization code with the client secret, reads the ID, and drops the access token.
GitHubUpgradeEndpoint links the GitHub ID to the signed-in anonymous person, grants identified (to their existing sessions too) and removes the anonymous identity, in one transaction: the person, their spaces and their studysets stay the same. GitHubSignInEndpoint gives a session for a person who has the GitHub account already; it's called without a session, since Serverpod issues no session for another person inside a signed-in call, and it never creates people. InstanceEndpoint.info() tells the app the instance's GitHub client ID. Refusals are GitHubSignInRefusedException with a reason: GitHub sign-in off here, the account taken by another person, another GitHub account linked already, an unknown account.
GitHub sign-in is on only where the passwords hold githubClientId and githubClientSecret (empty counts as missing, as Compose passes unset variables on as empty strings); production takes them from GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET in deploy/.env. grantIdentified can now join a caller's transaction. No migration: the GitHub accounts table comes with the auth module.
Tests, refusals first: upgrading when not signed in (checked by switching the guard off: the test fails), both ways without a GitHub app, an account taken by someone else with nothing changed, a second GitHub account, an unknown account with nobody created; then linking keeps the person and reaches their sessions and stores no email, linking twice changes nothing, signing in reaches the right person.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 13:35:51 +02:00
|
|
|
|
|
|
|
|
# GitHub sign-in (optional): the client ID and secret of this instance's own GitHub
|
|
|
|
|
# OAuth app (GitHub → Settings → Developer settings → OAuth Apps), registered with the
|
|
|
|
|
# callback URLs app.halkyon.learn://github-callback and http://127.0.0.1/github-callback.
|
|
|
|
|
# Empty: GitHub sign-in is off, and the app doesn't offer it.
|
|
|
|
|
GITHUB_CLIENT_ID=
|
|
|
|
|
GITHUB_CLIENT_SECRET=
|
2026-09-30 16:19:46 +02:00
|
|
|
|
|
|
|
|
# Where the source of what this instance runs is, shown in the app's About dialog: both
|
|
|
|
|
# licences (AGPL for the app, SSPL for the server) require offering it to the people who
|
|
|
|
|
# use the instance. Empty: the project's repository, which is right for unmodified code.
|
|
|
|
|
# Running modified code, set it to where your modified source is published.
|
|
|
|
|
SOURCE_URL=
|
Limit how much room people's files take
An anonymous person may keep 100 MB of images and sounds, an identified one
500 MB, and one person more by a file_allowance row the operator adds; all
of them together 15 GB, since every file is in the database and in each dump
the server keeps (decided 2026-10-02). Each comes from the environment
(FILE_LIMIT_*_MB), with these defaults, documented in deploy/.env.example.
The server checks when an upload starts, counting unfinished uploads so they
can't fill the disk unnoticed, and again when it finishes, counting what is
kept, so uploads started side by side can't overshoot; a refusal is a
FileLimitExceededException, and an upload refused as it finishes is deleted.
deletePerson deletes the allowance with the person. Migration: the new
table file_allowance, only added.
The app keeps a refused file waiting on the device and stops that round of
uploads; the Anki import warns when a deck's files may not fit in the room
left (asset.usage()).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 05:57:57 +02:00
|
|
|
|
|
|
|
|
# How much room people's images and sounds may take on this instance, in MiB. Empty: the
|
|
|
|
|
# defaults, 100 for an anonymous person (anonymous accounts are meant to become identified
|
|
|
|
|
# ones), 500 for one signed in with GitHub, and 15000 for everyone together (0: no limit
|
|
|
|
|
# for everyone together). Every file is in the database, so also in each of the 3 dumps
|
|
|
|
|
# this server keeps (and a fourth while a deploy writes its own): files take five times
|
|
|
|
|
# their size on the disk. One person may get more than the default; as the operator,
|
|
|
|
|
# in deploy/ (their id is their account's, "authUserId"):
|
|
|
|
|
# docker compose exec postgres psql -U halkyon_learn -d halkyon_learn -c "INSERT INTO
|
|
|
|
|
# file_allowance (\"authUserId\", \"maxBytes\", note) VALUES ('<id>', 2147483648, 'asked by mail')"
|
|
|
|
|
FILE_LIMIT_ANONYMOUS_MB=
|
|
|
|
|
FILE_LIMIT_IDENTIFIED_MB=
|
|
|
|
|
FILE_LIMIT_INSTANCE_MB=
|