halkyon/deploy/host/first-start.md

183 lines
8.2 KiB
Markdown
Raw Normal View History

# 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/halkyon-days/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).