Run the quick start we publish, and it did not work #71
+7
-7
@@ -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=
|
||||
|
||||
@@ -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:<password>@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
|
||||
|
||||
Executable
+145
@@ -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"
|
||||
Reference in New Issue
Block a user