source: Klonkt/scripts/klonkt-migrate-data.sh@ 2dd1dc4

main
Last change on this file since 2dd1dc4 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: 6.1 KB
RevLine 
[2dd1dc4]1#!/usr/bin/env bash
2#
3# Move an existing Klonkt install to the split layout:
4#
5# /opt/klonkt/ shared code, read-only at runtime
6# /var/lib/klonkt/<slug>/ this instance's data and .env
7#
8# Before, an instance kept its database, uploads and .env inside the checkout.
9# That made the code directory undeletable (it held live user data), made
10# backups awkward, and meant a second instance needed a second copy of the code.
11#
12# Run as root on the server. Safe to re-run: it stops at the first step that is
13# already done rather than moving anything twice.
14#
15# sudo bash scripts/klonkt-migrate-data.sh <slug>
16# sudo bash scripts/klonkt-migrate-data.sh <slug> --dry-run
17#
18# The slug names the instance and nothing else: it is the directory under
19# /var/lib/klonkt and the systemd instance name (klonkt@<slug>).
20
21set -euo pipefail
22
23KLONKT_DIR="${KLONKT_DIR:-/opt/klonkt}"
24KLONKT_USER="${KLONKT_USER:-klonkt}"
25DATA_ROOT="${KLONKT_DATA_ROOT:-/var/lib/klonkt}"
26OLD_UNIT="klonkt.service"
27
28SLUG=""
29DRY=0
30for arg in "$@"; do
31 case "$arg" in
32 --dry-run) DRY=1 ;;
33 -*) echo "unknown option: $arg" >&2; exit 2 ;;
34 *) SLUG="$arg" ;;
35 esac
36done
37
38say() { printf ' %s\n' "$*"; }
39step() { printf '\n== %s\n' "$*"; }
40die() { printf '\nERROR: %s\n' "$*" >&2; exit 1; }
41run() { if [ "$DRY" = 1 ]; then printf ' [dry-run] %s\n' "$*"; else eval "$@"; fi; }
42
43[ "$(id -u)" = 0 ] || die "run this as root (sudo)."
44[ -n "$SLUG" ] || die "usage: $0 <slug> [--dry-run] e.g. $0 boiert"
45[[ "$SLUG" =~ ^[a-z0-9][a-z0-9._-]*$ ]] || die "slug must be lowercase letters, digits, dot, dash or underscore."
46
47DATA_DIR="$DATA_ROOT/$SLUG"
48ENV_OLD="$KLONKT_DIR/.env"
49ENV_NEW="$DATA_DIR/.env"
50
51step "Preflight"
52[ -d "$KLONKT_DIR" ] || die "no install at $KLONKT_DIR"
53[ -f "$ENV_OLD" ] || [ -f "$ENV_NEW" ] || die "no .env at $ENV_OLD (already migrated elsewhere?)"
54id -u "$KLONKT_USER" >/dev/null 2>&1 || die "user $KLONKT_USER does not exist"
55
56# The split only works on code where every media subdirectory derives from
57# MEDIA_PATH. On older code the subdirectories fall back into the checkout, so
58# the app would quietly recreate storage/ next to the code and uploads would
59# land there.
60[ -f "$KLONKT_DIR/src/config/paths.js" ] || die \
61 "this build is too old for the split layout: src/config/paths.js is missing.
62 Update first (git pull in $KLONKT_DIR), then run this again."
63say "code at $KLONKT_DIR supports MEDIA_PATH-derived subdirectories"
64
65if [ -d "$DATA_DIR" ] && [ -n "$(ls -A "$DATA_DIR" 2>/dev/null)" ]; then
66 die "$DATA_DIR already exists and is not empty. Remove it or pick another slug."
67fi
68say "target $DATA_DIR is free"
69[ "$DRY" = 1 ] && say "DRY RUN: nothing will be changed"
70
71step "Stopping the service"
72if systemctl is-active --quiet "$OLD_UNIT"; then
73 run "systemctl stop $OLD_UNIT"
74 say "stopped $OLD_UNIT (SQLite checkpoints its write-ahead log on shutdown)"
75else
76 say "$OLD_UNIT was not running"
77fi
78
79step "Creating the data directory"
80run "mkdir -p '$DATA_DIR'"
81
82step "Moving data out of the checkout"
83if [ -d "$KLONKT_DIR/storage" ]; then
84 say "found $(find "$KLONKT_DIR/storage" -type f 2>/dev/null | wc -l) files in storage/ ($(du -sh "$KLONKT_DIR/storage" 2>/dev/null | cut -f1))"
85 # Everything, including database.sqlite plus its -wal and -shm siblings.
86 run "shopt -s dotglob nullglob; for f in '$KLONKT_DIR/storage/'*; do mv \"\$f\" '$DATA_DIR/'; done"
87 run "rmdir '$KLONKT_DIR/storage' 2>/dev/null || true"
88 say "moved to $DATA_DIR"
89else
90 say "no storage/ directory (already moved?)"
91fi
92
93if [ -f "$ENV_OLD" ]; then
94 run "mv '$ENV_OLD' '$ENV_NEW'"
95 say "moved .env to $ENV_NEW"
96fi
97
98step "Pointing the data paths at the new location"
99# Replace when present, append when absent, so this works regardless of which
100# variables the original install wrote.
101set_env() {
102 local key="$1" val="$2"
103 if [ "$DRY" = 1 ]; then printf ' [dry-run] %s=%s\n' "$key" "$val"; return; fi
104 if grep -q "^${key}=" "$ENV_NEW" 2>/dev/null; then
105 sed -i "s#^${key}=.*#${key}=${val}#" "$ENV_NEW"
106 else
107 printf '%s=%s\n' "$key" "$val" >> "$ENV_NEW"
108 fi
109 printf ' %s=%s\n' "$key" "$val"
110}
111set_env DATABASE_PATH "$DATA_DIR/database.sqlite"
112set_env MEDIA_PATH "$DATA_DIR/media"
113set_env AUDIO_PATH "$DATA_DIR/audio"
114
115step "Ownership and permissions"
116run "chown -R '$KLONKT_USER:$KLONKT_USER' '$DATA_DIR'"
117run "chmod 750 '$DATA_DIR'"
118run "chmod 600 '$ENV_NEW'"
119say "data owned by $KLONKT_USER, .env readable only by that user"
120
121step "Installing the systemd template"
122if [ -f "$KLONKT_DIR/deploy/klonkt@.service" ]; then
123 run "install -m 0644 '$KLONKT_DIR/deploy/klonkt@.service' /etc/systemd/system/klonkt@.service"
124 say "installed /etc/systemd/system/klonkt@.service"
125else
126 die "template not found at $KLONKT_DIR/deploy/klonkt@.service"
127fi
128run "systemctl daemon-reload"
129
130step "Switching to klonkt@$SLUG"
131if systemctl is-enabled --quiet "$OLD_UNIT" 2>/dev/null; then
132 run "systemctl disable --now $OLD_UNIT"
133 say "disabled $OLD_UNIT (file kept, so you can roll back)"
134fi
135run "systemctl enable --now 'klonkt@$SLUG'"
136
137step "Verifying"
138if [ "$DRY" = 1 ]; then
139 say "dry run: skipping verification"
140 exit 0
141fi
142sleep 3
143systemctl is-active --quiet "klonkt@$SLUG" || {
144 echo
145 journalctl -u "klonkt@$SLUG" -n 30 --no-pager || true
146 die "klonkt@$SLUG did not start. Roll back with: systemctl enable --now $OLD_UNIT"
147}
148say "klonkt@$SLUG is running"
149
150PORT="$(grep -m1 '^PORT=' "$ENV_NEW" | cut -d= -f2- | tr -d '\r')"
151if [ -n "$PORT" ]; then
152 if curl -fsS --max-time 8 -o /dev/null "http://127.0.0.1:${PORT}/"; then
153 say "responding on 127.0.0.1:${PORT}"
154 else
155 say "WARNING: no answer on 127.0.0.1:${PORT} yet; check: journalctl -u klonkt@$SLUG -f"
156 fi
157fi
158
159if [ -e "$KLONKT_DIR/storage" ]; then
160 say "WARNING: $KLONKT_DIR/storage came back. That means this build still writes"
161 say " next to its code. Report it; do not delete the directory."
162else
163 say "the checkout no longer holds user data"
164fi
165
166cat <<EOF
167
168Done. This instance now looks like:
169
170 code $KLONKT_DIR shared, replaceable, no user data
171 data $DATA_DIR database, uploads and .env
172 unit klonkt@$SLUG
173
174Back up $DATA_DIR and you have the whole instance.
175Add another instance with: klonkt-add-instance.sh <slug> <domain> <port>
176EOF
Note: See TracBrowser for help on using the repository browser.