source: Klonkt/scripts/klonkt-add-instance.sh@ ccaa530

main
Last change on this file since ccaa530 was 2dd1dc4, checked in by Robin <roboburr@…>, 6 weeks ago

Scheid gebruikersdata van code voor self-hosters

Een instance bewaarde zijn database, uploads en .env binnen de checkout. Daardoor
bevatte de codemap levende gebruikersdata: opruimen bij een deploy kon uploads
raken, een back-up moest de data tussen de code vandaan vissen, en een tweede
site vroeg een tweede kopie van alles, inclusief node_modules, die apart
bijgewerkt moest worden.

Nu staat de code in /opt/klonkt en de data per instance in /var/lib/klonkt/<slug>.
De checkout is daarmee wegwerpbaar: weggooien en opnieuw klonen laat elke
instance intact. Een site toevoegen is een map plus een .env, zonder tweede
kopie van de code, en klonkt-update brengt ze in een keer allemaal naar de
nieuwe versie.

De systemd-template draait als de klonkt-gebruiker met ProtectSystem=strict en
ReadWritePaths op alleen de eigen datamap. Een instance kan dus niet in de code
schrijven en niet bij de data van een andere instance, ook niet als er in de app
iets misgaat.

Changed files:
scripts/install.sh

  • KLONKT_DATA_ROOT en KLONKT_SLUG toegevoegd, slug afgeleid van het domein
  • verse installatie zet data in /var/lib/klonkt/<slug> en start klonkt@<slug>
  • bestaande installaties met een .env in de checkout blijven ongemoeid, opnieuw draaien mag nooit een levende database verplaatsen
  • weigert bij een database in de checkout zonder .env, te dubbelzinnig
  • klonkt-update herstart voortaan elke instance, niet alleen klonkt.service

deploy/DEPLOY.md

  • sectie 9b: meerdere zelfstandige Klonkts naast elkaar, met verwijzing
  • onderscheid verduidelijkt met de bestaande multi-tenant sectie, die gaat over sites binnen een instance

New file:
deploy/klonkt@.service

  • systemd template-unit, een service per instance, gedeelde code

scripts/klonkt-migrate-data.sh

  • zet een bestaande installatie om, met --dry-run en een rollback-pad
  • weigert op code zonder src/config/paths.js, anders schrijft de app alsnog naast zijn eigen code

scripts/klonkt-add-instance.sh

  • nieuwe instance: datamap, .env met verse SESSION_SECRET en vrije poort, service en Caddy-blok

deploy/MULTI-INSTANCE.md

  • indeling, eigenaarschap en rechten, migratie, instances toevoegen, updaten, back-up en verwijderen

Nog te doen: dit is getest op de fleet en met een lokale rooktest, maar de
verse-installatiestap zelf is nog niet op een schone VPS gedraaid.

-robo
Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100755
File size: 4.8 KB
Line 
1#!/usr/bin/env bash
2#
3# Add a Klonkt instance. An instance is a data directory and an .env file; the
4# code in /opt/klonkt is shared with every other instance and is not copied.
5#
6# sudo bash scripts/klonkt-add-instance.sh <slug> <domain> [port]
7# sudo bash scripts/klonkt-add-instance.sh blog blog.example.com --no-caddy
8#
9# Leave the port out and a free one is chosen. Run klonkt-update once and every
10# instance on the machine moves to the new code together.
11
12set -euo pipefail
13
14KLONKT_DIR="${KLONKT_DIR:-/opt/klonkt}"
15KLONKT_USER="${KLONKT_USER:-klonkt}"
16DATA_ROOT="${KLONKT_DATA_ROOT:-/var/lib/klonkt}"
17NO_CADDY="${KLONKT_NO_CADDY:-}"
18LANG_DEFAULT="${KLONKT_DEFAULT_LANG:-}"
19
20SLUG=""; DOMAIN=""; PORT=""
21for arg in "$@"; do
22 case "$arg" in
23 --no-caddy) NO_CADDY=1 ;;
24 -*) echo "unknown option: $arg" >&2; exit 2 ;;
25 *) if [ -z "$SLUG" ]; then SLUG="$arg"
26 elif [ -z "$DOMAIN" ]; then DOMAIN="$arg"
27 elif [ -z "$PORT" ]; then PORT="$arg"
28 fi ;;
29 esac
30done
31
32say() { printf ' %s\n' "$*"; }
33step() { printf '\n== %s\n' "$*"; }
34die() { printf '\nERROR: %s\n' "$*" >&2; exit 1; }
35
36[ "$(id -u)" = 0 ] || die "run this as root (sudo)."
37[ -n "$SLUG" ] && [ -n "$DOMAIN" ] || die "usage: $0 <slug> <domain> [port] [--no-caddy]"
38[[ "$SLUG" =~ ^[a-z0-9][a-z0-9._-]*$ ]] || die "slug must be lowercase letters, digits, dot, dash or underscore."
39
40DATA_DIR="$DATA_ROOT/$SLUG"
41ENV_FILE="$DATA_DIR/.env"
42
43step "Preflight"
44[ -d "$KLONKT_DIR" ] || die "no shared code at $KLONKT_DIR. Install Klonkt first."
45[ -f "$KLONKT_DIR/src/config/paths.js" ] || die \
46 "this build is too old to share one checkout between instances.
47 Update first: klonkt-update"
48id -u "$KLONKT_USER" >/dev/null 2>&1 || die "user $KLONKT_USER does not exist"
49[ -e "$DATA_DIR" ] && die "$DATA_DIR already exists. Pick another slug."
50[ -f /etc/systemd/system/klonkt@.service ] || {
51 [ -f "$KLONKT_DIR/deploy/klonkt@.service" ] || die "missing $KLONKT_DIR/deploy/klonkt@.service"
52 install -m 0644 "$KLONKT_DIR/deploy/klonkt@.service" /etc/systemd/system/klonkt@.service
53 systemctl daemon-reload
54 say "installed the systemd template (first instance on this machine)"
55}
56
57# Pick a port nobody is listening on and no other instance has claimed.
58if [ -z "$PORT" ]; then
59 for p in $(seq 3000 3099); do
60 grep -rqs "^PORT=${p}$" "$DATA_ROOT"/*/.env && continue
61 ss -ltnH "sport = :$p" 2>/dev/null | grep -q . && continue
62 PORT="$p"; break
63 done
64 [ -n "$PORT" ] || die "no free port found in 3000-3099; pass one explicitly."
65fi
66say "slug $SLUG, domain $DOMAIN, port $PORT"
67
68step "Creating $DATA_DIR"
69mkdir -p "$DATA_DIR"
70
71step "Writing .env"
72SECRET="$(openssl rand -hex 32)"
73{
74 echo "NODE_ENV=production"
75 echo "PORT=${PORT}"
76 # Loopback only: the reverse proxy reaches it, the internet cannot bypass HTTPS.
77 echo "HOST=127.0.0.1"
78 echo "SESSION_SECRET=${SECRET}"
79 echo "PUBLIC_BASE_URL=https://${DOMAIN}"
80 echo "DATABASE_PATH=${DATA_DIR}/database.sqlite"
81 echo "MEDIA_PATH=${DATA_DIR}/media"
82 echo "AUDIO_PATH=${DATA_DIR}/audio"
83 [ -n "$LANG_DEFAULT" ] && echo "KLONKT_DEFAULT_LANG=${LANG_DEFAULT}"
84} > "$ENV_FILE"
85say "random SESSION_SECRET, data paths under $DATA_DIR"
86
87step "Ownership and permissions"
88chown -R "$KLONKT_USER:$KLONKT_USER" "$DATA_DIR"
89chmod 750 "$DATA_DIR"
90chmod 600 "$ENV_FILE"
91say "owned by $KLONKT_USER; .env readable only by that user"
92
93step "Starting klonkt@$SLUG"
94systemctl enable --now "klonkt@$SLUG"
95sleep 3
96systemctl is-active --quiet "klonkt@$SLUG" || {
97 echo; journalctl -u "klonkt@$SLUG" -n 30 --no-pager || true
98 die "klonkt@$SLUG did not start."
99}
100curl -fsS --max-time 8 -o /dev/null "http://127.0.0.1:${PORT}/" \
101 && say "responding on 127.0.0.1:${PORT}" \
102 || say "WARNING: no answer yet on 127.0.0.1:${PORT}; check journalctl -u klonkt@$SLUG -f"
103
104if [ -z "$NO_CADDY" ] && command -v caddy >/dev/null 2>&1; then
105 step "Caddy"
106 CADDY=/etc/caddy/Caddyfile
107 if grep -q "^${DOMAIN} {" "$CADDY" 2>/dev/null; then
108 say "a block for ${DOMAIN} already exists, left untouched"
109 else
110 cp "$CADDY" "${CADDY}.bak.$(date +%s)" 2>/dev/null || true
111 printf '\n%s {\n reverse_proxy 127.0.0.1:%s\n encode gzip zstd\n}\n' "$DOMAIN" "$PORT" >> "$CADDY"
112 caddy validate --config "$CADDY" --adapter caddyfile >/dev/null 2>&1 \
113 || die "Caddy config invalid after adding ${DOMAIN} — check $CADDY (a .bak was made)"
114 systemctl reload caddy 2>/dev/null || systemctl restart caddy
115 say "serving ${DOMAIN}"
116 fi
117fi
118
119cat <<EOF
120
121Instance ready.
122
123 data $DATA_DIR
124 unit klonkt@$SLUG
125 port 127.0.0.1:$PORT
126 code $KLONKT_DIR (shared with every other instance)
127
128Next: open https://${DOMAIN}/auth/register and create the admin account.
129
130 status systemctl status klonkt@$SLUG
131 logs journalctl -u klonkt@$SLUG -f
132 update klonkt-update # updates the code once, restarts all instances
133 backup $DATA_DIR # this directory is the whole instance
134EOF
Note: See TracBrowser for help on using the repository browser.