183 lines
8.2 KiB
Markdown
183 lines
8.2 KiB
Markdown
|
|
# halkyon.li's first start
|
|||
|
|
|
|||
|
|
The instance on balthasar, once (ADR 0039), after the host's steps 1 to 6
|
|||
|
|
(`README.md`): Learn, Train, Todo and Meditate with Jellyfin, LanguageTool, BRouter,
|
|||
|
|
Train's map and place search, all from the start (the developer's choice of
|
|||
|
|
8 Nov 2026). Each step leaves what came before running. Commands run on balthasar
|
|||
|
|
as the developer (`ssh balthasar` from melchior) unless a step says otherwise; those
|
|||
|
|
for `deploy` after `sudo -iu deploy`. Later deploys are `redeploy.sh`'s (D13).
|
|||
|
|
|
|||
|
|
1. **The sidecars' users**, each ID the sidecar's alone, as its compose file runs it
|
|||
|
|
(`useradd` warns that the IDs lie above the system range, which is meant: they must
|
|||
|
|
never be a person's, `../sidecars/README.md`):
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
for each in jellyfin:61096 languagetool:61081 brouter:61177 maptiles:61178 photon:61179; do
|
|||
|
|
name=${each%:*} id=${each#*:}
|
|||
|
|
sudo groupadd --system --gid "$id" "$name"
|
|||
|
|
sudo useradd --system --uid "$id" --gid "$id" --no-create-home --shell /usr/sbin/nologin "$name"
|
|||
|
|
done
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. **`deploy`**, which owns the checkout and runs the instance, in Docker's group (so
|
|||
|
|
as good as root on this host: its key may run `redeploy.sh` alone, step 3). Its home
|
|||
|
|
is on the HDD, as `/home` is:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
sudo useradd --create-home --shell /bin/bash --groups docker deploy
|
|||
|
|
sudo -iu deploy
|
|||
|
|
ssh-keygen -t ed25519 -N '' -C 'deploy@balthasar: halkyon, read-only' -f ~/.ssh/id_ed25519
|
|||
|
|
cat ~/.ssh/id_ed25519.pub
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
In Forgejo (`code.halkyon.app`, the repository's *Settings › Deploy keys*), that key
|
|||
|
|
as a deploy key, **without** write access. Then, still as `deploy`, the clone; SSH
|
|||
|
|
first shows the forge's host key, which must be
|
|||
|
|
`SHA256:98mCW/wdTIK7Jl6vwDE4qQqLECa8wkgDWye7+nQONn4` (ED25519):
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
git clone ssh://git@code.halkyon.app/project-halkyon/halkyon.git ~/halkyon
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
3. **The developer's keys**: the GPG key `redeploy.sh` checks every commit's signature
|
|||
|
|
with (`deploy/allowed-signers`), and the SSH key it alone may be run with. On
|
|||
|
|
melchior:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
gpg --export --armor B172B9ED76A32EBCABA24A9F07F6FCF405488B64 >developer.asc
|
|||
|
|
scp developer.asc ~/.ssh/raisondetredev@ramiel.pub balthasar:/tmp/
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
On balthasar, as `deploy`:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
gpg --import /tmp/developer.asc
|
|||
|
|
git -C ~/halkyon log -1 --format='%G? %GP' # U B172B9ED…B64: good, vouched for by the list
|
|||
|
|
printf 'restrict,command="/home/deploy/halkyon/deploy/redeploy.sh" %s\n' \
|
|||
|
|
"$(cat /tmp/raisondetredev@ramiel.pub)" >>~/.ssh/authorized_keys
|
|||
|
|
chmod 600 ~/.ssh/authorized_keys
|
|||
|
|
ssh-keygen -lf ~/.ssh/authorized_keys # SHA256:eSJ3RtDT0oJxCUq33B50snmkt1jSYw19Z0U6X+W9+hA
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
(`rm /tmp/developer.asc /tmp/raisondetredev@ramiel.pub` as the developer afterwards.) On melchior, in
|
|||
|
|
`~/.ssh/config`, through the tunnel, so it works at home and away alike:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Host balthasar-deploy
|
|||
|
|
HostName 10.61.97.2
|
|||
|
|
User deploy
|
|||
|
|
IdentityFile ~/.ssh/raisondetredev@ramiel
|
|||
|
|
IdentitiesOnly yes
|
|||
|
|
ProxyJump ramiel
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
4. **`.env`**, as `deploy`: every secret made here, the developer's settings, never
|
|||
|
|
over an existing file (`halkyon-li-env.sh`):
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
cd ~/halkyon
|
|||
|
|
sh deploy/host/halkyon-li-env.sh deploy/.env
|
|||
|
|
cat deploy/.env # the whole file into Proton Pass: Infra, a note "halkyon.li .env"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
5. **SimpleLogin's app**, the only way in (D4): in SimpleLogin, *Settings › Developer ›
|
|||
|
|
New website*, named *Halkyon (halkyon.li)*, its URL `https://learn.halkyon.li`, its
|
|||
|
|
redirect URL (callback) `https://learn.halkyon.li/simplelogin-callback`. Its client ID
|
|||
|
|
and secret into `deploy/.env` (`SIMPLELOGIN_CLIENT_ID`, `SIMPLELOGIN_CLIENT_SECRET`,
|
|||
|
|
as `deploy` with `nano ~/halkyon/deploy/.env`) and into the note in Proton Pass.
|
|||
|
|
|
|||
|
|
6. **The sidecars' folders, and Jellyfin moved in.** As the developer:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
d=/home/deploy/halkyon/deploy
|
|||
|
|
sudo install -d -o languagetool -g languagetool $d/languagetool $d/languagetool/fasttext
|
|||
|
|
sudo install -d -o brouter -g brouter $d/brouter/segments
|
|||
|
|
sudo install -d -o maptiles -g maptiles $d/maps/tiles
|
|||
|
|
sudo install -d -o photon -g photon $d/photon/data
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Then `../sidecars/README.md`, *Jellyfin on halkyon.li*, steps 2 to 5: the library
|
|||
|
|
to `/data/media`, the configuration and cache into the deploy, `network.xml` for
|
|||
|
|
Caddy, ADR 0032's folder removed. `.env` names the library already.
|
|||
|
|
|
|||
|
|
7. **The databases.** PostgreSQL first, alone, which makes the instance's superuser
|
|||
|
|
`halkyon_learn` with `.env`'s password; then a role and a database for each of the
|
|||
|
|
other three, as `../sidecars/README.md` explains for Train's (*Train*, step 2: each
|
|||
|
|
role logs in and is nothing more, owns its database alone and connects to no
|
|||
|
|
other). As `deploy`, the passwords read from `.env` rather than typed:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
cd ~/halkyon/deploy
|
|||
|
|
docker compose up -d --wait postgres
|
|||
|
|
set -a; . ./.env; set +a
|
|||
|
|
docker compose exec -T postgres psql -U halkyon_learn -d postgres -v ON_ERROR_STOP=1 \
|
|||
|
|
-v train="$TRAIN_POSTGRES_PASSWORD" -v todo="$TODO_POSTGRES_PASSWORD" \
|
|||
|
|
-v meditate="$MEDITATE_POSTGRES_PASSWORD" <<'SQL'
|
|||
|
|
CREATE ROLE halkyon_train LOGIN PASSWORD :'train';
|
|||
|
|
CREATE DATABASE halkyon_train OWNER halkyon_train;
|
|||
|
|
CREATE ROLE halkyon_todo LOGIN PASSWORD :'todo';
|
|||
|
|
CREATE DATABASE halkyon_todo OWNER halkyon_todo;
|
|||
|
|
CREATE ROLE halkyon_meditate LOGIN PASSWORD :'meditate';
|
|||
|
|
CREATE DATABASE halkyon_meditate OWNER halkyon_meditate;
|
|||
|
|
REVOKE CONNECT ON DATABASE halkyon_learn FROM PUBLIC;
|
|||
|
|
REVOKE CONNECT ON DATABASE halkyon_train FROM PUBLIC;
|
|||
|
|
REVOKE CONNECT ON DATABASE halkyon_todo FROM PUBLIC;
|
|||
|
|
REVOKE CONNECT ON DATABASE halkyon_meditate FROM PUBLIC;
|
|||
|
|
SQL
|
|||
|
|
exit # leaves deploy's shell, with .env's secrets in it
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
8. **The first start**, as `deploy` in `~/halkyon/deploy`. It builds every image (the
|
|||
|
|
four servers, the dictionaries, BRouter, the map's and Photon's), half an hour or
|
|||
|
|
more, then starts everything; each server applies its migrations as it starts:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
docker compose up -d --build
|
|||
|
|
docker compose ps # every service up; the servers healthy within a minute or two
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
What builds on afterwards, by itself: the dictionaries (some 3 GB downloaded within
|
|||
|
|
the first hour), BRouter's segments for southern Bavaria (minutes), the map's cut
|
|||
|
|
(an hour or more) and Photon's Germany (7.5 GB, an hour or more). Until then Train
|
|||
|
|
has no routes, an empty map and no search. From melchior, anywhere:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
for name in learn todo train meditate jellyfin; do
|
|||
|
|
curl -s -o /dev/null -w "$name.halkyon.li %{http_code}\n" https://$name.halkyon.li/
|
|||
|
|
done
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Every name answers (200, or a redirect), over a certificate Caddy got at home, as
|
|||
|
|
ramiel passes TLS through unopened.
|
|||
|
|
|
|||
|
|
9. **The first operator** (ADR 0021, D20, D32): in Halkyon Learn, another instance,
|
|||
|
|
`https://learn.halkyon.li`, *Sign in with SimpleLogin*, a handle chosen; the account
|
|||
|
|
then waits. As `deploy`, in `~/halkyon/deploy`, the developer's handle in lower case
|
|||
|
|
in place of `their_handle`:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
docker compose exec -T postgres psql -U halkyon_learn -d halkyon_learn -v ON_ERROR_STOP=1 <<'SQL'
|
|||
|
|
BEGIN;
|
|||
|
|
UPDATE "serverpod_auth_core_user"
|
|||
|
|
SET "scopeNames" = ("scopeNames"::jsonb || '["operator"]')::json
|
|||
|
|
WHERE "id" = (SELECT "authUserId" FROM "public_profile" WHERE "handleKey" = 'their_handle')
|
|||
|
|
AND NOT ("scopeNames"::jsonb ? 'operator');
|
|||
|
|
UPDATE "serverpod_auth_core_session"
|
|||
|
|
SET "scopeNames" = ("scopeNames"::jsonb || '["operator"]')::json
|
|||
|
|
WHERE "authUserId" = (SELECT "authUserId" FROM "public_profile" WHERE "handleKey" = 'their_handle')
|
|||
|
|
AND NOT ("scopeNames"::jsonb ? 'operator');
|
|||
|
|
COMMIT;
|
|||
|
|
SQL
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`UPDATE 1` twice, or the first once and the second `UPDATE 0` while there's no
|
|||
|
|
session yet. Back in the app, *Try again* on the wait screen: an operator never
|
|||
|
|
waits, and Settings shows *This instance*. Everyone else now signs up and waits for
|
|||
|
|
the operator, who approves them there, the four words read out first.
|
|||
|
|
|
|||
|
|
10. **Backups** (D18): `../backups.md`, *Setting it up*, for halkyon.li: on the same
|
|||
|
|
Storage Box a sub-account of its own (`halkyon-li`), a key and a restic repository
|
|||
|
|
of their own, the repository's password into Proton Pass (Infra,
|
|||
|
|
"halkyon.li restic"), then the timer.
|
|||
|
|
|
|||
|
|
Then the CI runner, `README.md`, step 7, and the Instance console (ADR 0040).
|