Run the quick start we publish, and it did not work #71

Merged
paulhitt merged 3 commits from 4-selfhost-smoke into main 2026-08-31 19:30:34 +00:00
3 changed files with 166 additions and 9 deletions
+7 -7
View File
@@ -1,12 +1,6 @@
# Copy to .env and fill in. Generate the three secrets with:
#
# openssl rand -hex 32 # # Host port the relay is published on, on 127.0.0.1 only. Change this rather
# than editing docker-compose.yml — 8080 is frequently already taken on a
# machine that self-hosts other things, and editing the compose file makes the
# next `git pull` conflict.
FN_HOST_PORT=8080
POSTGRES_PASSWORD
# openssl rand -hex 32 # POSTGRES_PASSWORD
# openssl rand -base64 32 # FN_AT_REST_SECRET
#
# POSTGRES_PASSWORD is hex, not base64, and that is not a style choice: it is
@@ -22,6 +16,12 @@ POSTGRES_PASSWORD
# Keep issuer.pem. Losing it invalidates every token you have issued; leaking it
# lets anyone mint tokens this relay will accept.
# Host port the relay is published on, on 127.0.0.1 only. Change this rather
# than editing docker-compose.yml — 8080 is frequently already taken on a
# machine that self-hosts other things, and editing the compose file makes the
# next `git pull` conflict.
FN_HOST_PORT=8080
POSTGRES_PASSWORD=
FN_AT_REST_SECRET=
FN_ISSUER_PRIVATE_KEY=
+14 -2
View File
@@ -3,7 +3,7 @@
<!-- GENERATED by Scripts/build-doc-pages.sh from flamenet-e2e/SELF-HOSTING.md.
Edits made HERE are deleted the next time that script runs.
Change the markdown source and re-run it.
source-sha256: a4ed8fd6048d30cd -->
source-sha256: 0f36788aea97abd7 -->
<article class="band doc doc-generated">
<div class="wrap wrap-narrow">
<h1 id="self-hosting">Self-hosting</h1>
@@ -52,12 +52,24 @@ told.</p>
cd flamenet-relay
cp .env.example .env</code></pre>
<p>Generate the three secrets <code>.env</code> asks for:</p>
<pre class="bash"><code>openssl rand -base64 32 #("#") POSTGRES_PASSWORD
<pre class="bash"><code>openssl rand -hex 32 #("#") POSTGRES_PASSWORD
openssl rand -base64 32 #("#") FN_AT_REST_SECRET
openssl genpkey -algorithm ed25519 -out issuer.pem
openssl pkey -in issuer.pem -outform DER | tail -c 32 | base64 #("#") FN_ISSUER_PRIVATE_KEY
openssl pkey -in issuer.pem -pubout -outform DER | tail -c 32 | base64 #("#") FN_ISSUER_PUBLIC_KEY</code></pre>
<p>🔴 <strong><code>-hex</code> for the database password, not
<code>-base64</code>, and this one is not cosmetic.</strong> It is
interpolated into
<code>postgres://flamenet:&lt;password&gt;@db:5432/flamenet</code>,
where base64's <code>/</code>, <code>+</code> and <code>=</code> are all
meaningful. A password containing <code>/</code> makes that URL invalid
and the relay dies at start-up with a bare <code>Code=-1000</code> and
the connection string echoed back at you — which reads like the database
is unreachable rather than like the password needs escaping.
<code>openssl rand -base64 32</code> produces one of those roughly
<strong>three times in four</strong>, so following the old instructions
usually failed.</p>
<p>Then create the proxy network the compose file attaches to, and
start:</p>
<pre class="bash"><code>docker network create web
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env bash
# Run the published self-hosting quick start, exactly as SELF-HOSTING.md gives it.
#
# 🔴 **The quick start broke, silently, and stayed broken for a day.** Three
# separate faults at once — and the one that survived longest was a missing
# `docker network create web`, which fails only at `up` time, after the images
# have been pulled. Nothing in this repository ran those instructions, so
# nothing said so. The people it failed for were self-hosters, at the exact
# moment they were deciding whether this project works.
#
# ⚠️ **It has to actually BOOT.** `docker compose config` renders that compose
# file happily with no `web` network in existence: the external-network check
# happens at `up`. A job that only validated the YAML would have passed
# throughout the outage it exists to prevent.
#
# Scripts/selfhost-smoke.sh # published image, the documented path
# Scripts/selfhost-smoke.sh --build # build from source, the ARM path
#
# 🔴 **Not wired into CI, and not for want of trying.** This project's runner is
# a docker executor with `privileged = false` and no socket mounted, so a job
# here gets an alpine container with no bash, no docker CLI and no way to reach
# a daemon — `env: can't execute bash` is as far as it got. Running it in CI
# needs a runner change (privileged, a socket mount, or a shell executor), which
# is a decision about a machine that also serves production.
#
# ⚠️ Until then this is a RELEASE-TIME check somebody has to run, on any machine
# with docker:
#
# ssh <box> "cd /srv/prod/messenger/src && ./Scripts/selfhost-smoke.sh"
#
# A check that only runs when remembered is weaker than one that cannot be
# forgotten. It is still worth having: on its first two runs it found the
# missing `docker network create web` AND a documented password that broke the
# relay three times in four.
#
# Leaves nothing behind: a scratch directory, its own compose project name, and
# a trap that tears down volumes as well as containers.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
mode="${1:-}"
project="selfhost-smoke-$$"
# ⚠️ Its own network name, not `web`. A shared name would collide with a real
# proxy network on a machine that has one — and this script deletes it at the
# end, which on the production box would be a very bad afternoon.
network="${project}-web"
work="$(mktemp -d)"
cleanup() {
docker compose -p "$project" --project-directory "$work" \
-f "$work/docker-compose.yml" -f "$work/docker-compose.override.yml" \
down -v --remove-orphans >/dev/null 2>&1 || true
docker network rm "$network" >/dev/null 2>&1 || true
rm -rf "$work"
}
trap cleanup EXIT
echo "SELFHOST: laying out a fresh checkout the way a self-hoster would"
cp "$here/docker-compose.yml" "$work/docker-compose.yml"
cp "$here/docker-compose.build.yml" "$work/docker-compose.build.yml" 2>/dev/null || true
cp "$here/.env.example" "$work/.env"
# The build path needs the sources the Dockerfile copies.
if [ "$mode" = "--build" ]; then
cp -R "$here/Sources" "$here/Tests" "$here/Resources" "$here/Public" \
"$here/Package.swift" "$here/Package.resolved" "$here/Dockerfile" "$work/" 2>/dev/null || true
fi
echo "SELFHOST: generating the secrets .env asks for"
# The same four commands the documentation prints. Generated rather than
# hardcoded, so a change to what the compose file demands shows up here.
pem="$work/issuer.pem"
openssl genpkey -algorithm ed25519 -out "$pem" 2>/dev/null
set_env() {
# ⚠️ In-place edit that tolerates an empty value on the right of `=`, which is
# what .env.example ships. Anchored to the line start so a variable whose name
# is a suffix of another one cannot be hit.
python3 - "$work/.env" "$1" "$2" <<'PY'
import sys, re
path, key, val = sys.argv[1], sys.argv[2], sys.argv[3]
text = open(path, encoding='utf-8').read()
new, n = re.subn(rf'^{re.escape(key)}=.*$', f'{key}={val}', text, count=1, flags=re.M)
if n != 1:
sys.exit(f'{key} is not in .env.example — the documented steps have drifted')
open(path, 'w', encoding='utf-8').write(new)
PY
}
# 🔴 **hex, not base64, and this is the documented command for a reason.**
# `openssl rand -base64 32` emits `/` and `+` about 73% of the time, and the
# password goes into `postgres://user:PASSWORD@db/...` unencoded — a `/` ends
# the authority and the relay dies with `NSURLErrorDomain Code=-1000`, which
# names the URL and says nothing about passwords. This script generates what
# SELF-HOSTING.md tells people to generate; if that drifts back to base64, this
# fails again and says why.
set_env POSTGRES_PASSWORD "$(openssl rand -hex 32)"
set_env FN_AT_REST_SECRET "$(openssl rand -base64 32 | tr -d '\n')"
set_env FN_ISSUER_PRIVATE_KEY "$(openssl pkey -in "$pem" -outform DER 2>/dev/null | tail -c 32 | base64 | tr -d '\n')"
set_env FN_ISSUER_PUBLIC_KEY "$(openssl pkey -in "$pem" -pubout -outform DER 2>/dev/null | tail -c 32 | base64 | tr -d '\n')"
# A port nobody else is on, so this cannot collide with a relay already running
# on the machine — which on the deployment box is the production one.
port=18973
set_env FN_HOST_PORT "$port"
echo "SELFHOST: docker network create (the step that was missing)"
docker network create "$network" >/dev/null
# The compose file names the external network `web`; point it at ours for the
# duration without editing the file a self-hoster would actually use.
cat > "$work/docker-compose.override.yml" <<YAML
networks:
web:
name: ${network}
external: true
YAML
echo "SELFHOST: docker compose up -d"
# ⚠️ Absolute paths. `-f` resolves against the CWD, NOT against
# `--project-directory` — a relative name here is looked for wherever the script
# happens to be run from, which is how this first failed: "open
# /srv/prod/messenger/src/docker-compose.override.yml: no such file".
files=(-f "$work/docker-compose.yml" -f "$work/docker-compose.override.yml")
[ "$mode" = "--build" ] && files=(-f "$work/docker-compose.yml" -f "$work/docker-compose.build.yml" -f "$work/docker-compose.override.yml")
build_flag=()
[ "$mode" = "--build" ] && build_flag=(--build)
docker compose -p "$project" --project-directory "$work" "${files[@]}" up -d "${build_flag[@]}"
echo "SELFHOST: waiting for the relay to answer"
up=""
for _ in $(seq 1 60); do
if [ "$(curl -sf -m 2 "http://127.0.0.1:${port}/health" || true)" = "ok" ]; then up=1; break; fi
sleep 2
done
if [ -z "$up" ]; then
echo "SELFHOST FAILED: no /health after 120s — the documented quick start does not work"
docker compose -p "$project" --project-directory "$work" "${files[@]}" logs --tail 40 relay || true
exit 1
fi
# 🔴 The landing page, not just /health. "The relay serves its own landing page,
# so a working install explains itself rather than answering with a 404" is a
# promise the compose file's own header makes, and a missing Resources copy in
# the image would leave /health green and the page a 404.
code=$(curl -s -m 5 -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/")
[ "$code" = "200" ] || { echo "SELFHOST FAILED: landing page answered $code"; exit 1; }
echo "SELFHOST OK: the documented quick start boots and serves its landing page"