server-up/install.sh
Ramon fc2e80fbda
Some checks failed
Deploy server-up (dev) / deploy (push) Failing after 2m8s
v0.10.00-beta - Server Up draait niet meer als root
- bij de installatie wordt gevraagd onder welk account Server Up draait: een
  nieuw systeemaccount 'serverup', het account waarmee je werkt, een bestaand
  account, of root zoals voorheen (--user NAAM slaat de vraag over)
- nieuw docker-entrypoint.sh: start als root, zet /data, stacks en backups
  klaar, en zakt met setpriv af naar SU_UID:SU_GID met de capabilities die
  nodig blijven (chown, dac_override, fowner - alle drie al in Docker's
  standaardset)
- appdata blijft daarbij bewust ongemoeid; die mappen zijn van de apps zelf
- de gid van de docker-socket wordt uit de socket zelf gelezen, zodat het
  account niet in de docker-groep hoeft (dat zou root op de host geven)
- installatieformulier vult PUID/PGID met de ids waaronder Server Up draait in
  plaats van de 1000 die 44 sjablonen blind noemen
- een onleesbaar bestand laat de rest van de backup niet meer sneuvelen; wat
  ontbreekt komt in het log en in de metadata (skipped)
- terugzetten herstelt het eigenaarschap van appdata, dat tarfile met
  filter="data" laat vallen
- CI controleert in een echte container dat setpriv bestaat, dat er wordt
  afgezakt, dat de socket-gid meekomt, dat /data schrijfbaar is en dat SU_UID=0
  root laat blijven
- SU_UID=0 in .env is de ontsnappingsklep naar het oude gedrag

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
2026-08-02 22:02:36 +02:00

896 lines
36 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
# Server Up — installeren, bijwerken of verwijderen op een Linux-server.
#
# curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/raw/branch/main/install.sh | sh
#
# Liever eerst lezen wat je uitvoert (aan te raden):
#
# curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/raw/branch/main/install.sh -o install.sh
# less install.sh
# sh install.sh
#
# POSIX sh, dus het werkt onder dash, ash en bash.
set -eu
SCRIPT_VERSIE="1.1"
GIT_HOST="git.ramonbesselink.nl"
GIT_PAD="bes-r/server-up"
BRANCH="main"
DOEL="/opt/server-up"
POORT="5000"
BIND="127.0.0.1"
TOKEN=""
JA=0
DROOG=0
ACTIE="install"
BEHEERDER=""
BEHEERDER_WW=""
BEHEERDER_WW_BESTAND=""
# Expliciet meegegeven waarden overschrijven wél een bestaande .env; de rest
# blijft staan, zodat een --update je instellingen niet terugzet.
BIND_EXPLICIET=0
POORT_EXPLICIET=0
# Onder welk account Server Up draait. Leeg = nog niet gekozen; 0 = root.
GEBRUIKER=""
GEBRUIKER_EXPLICIET=0
SU_UID=""
SU_GID=""
SU_NAAM=""
BASE_DIR="${BASE_DIR:-}"
# ── Uitvoer ──────────────────────────────────────────────────────────────────
# Kleuren alleen als we naar een terminal schrijven; in een pipe of logbestand
# leveren escape-codes onleesbare rommel op.
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
C_OK=$(printf '\033[32m'); C_WAARSCHUW=$(printf '\033[33m')
C_FOUT=$(printf '\033[31m'); C_KOP=$(printf '\033[1m'); C_UIT=$(printf '\033[0m')
else
C_OK=''; C_WAARSCHUW=''; C_FOUT=''; C_KOP=''; C_UIT=''
fi
log() { printf '%s\n' " $*"; }
stap() { printf '\n%s%s%s\n' "$C_KOP" "$*" "$C_UIT"; }
goed() { printf ' %s✔%s %s\n' "$C_OK" "$C_UIT" "$*"; }
waarschuw() { printf ' %s!%s %s\n' "$C_WAARSCHUW" "$C_UIT" "$*"; }
fout() { printf '\n %s✖ %s%s\n\n' "$C_FOUT" "$*" "$C_UIT" >&2; exit 1; }
# Bij --dry-run alleen waarschuwen: je wilt de hele voorvertoning zien, ook op
# een machine waar Docker (nog) niet staat.
fout_tenzij_droog() {
if [ "$DROOG" -eq 1 ]; then waarschuw "$*"; else fout "$*"; fi
}
heeft() { command -v "$1" >/dev/null 2>&1; }
# Voert uit, of toont alleen wat er zou gebeuren bij --dry-run.
doe() {
if [ "$DROOG" -eq 1 ]; then
printf ' [droog] %s\n' "$*"
else
"$@"
fi
}
vraag() {
# vraag "Vraag?" -> 0 bij ja. Met --yes of zonder terminal: altijd ja.
[ "$JA" -eq 1 ] && return 0
[ -t 0 ] || return 0
printf ' %s [J/n] ' "$1"
read -r antwoord </dev/tty || return 0
case "$antwoord" in [nN]*) return 1 ;; *) return 0 ;; esac
}
gebruik() {
cat <<UITLEG
Server Up installatiescript ${SCRIPT_VERSIE}
Gebruik: install.sh [opties]
--dir PAD Installatiemap (standaard: ${DOEL})
--port POORT Poort voor de webinterface (standaard: ${POORT})
--bind ADRES Waarop de poort luistert (standaard: ${BIND})
Gebruik 0.0.0.0 om er van buiten de server bij te kunnen,
maar zet er dan wel een reverse proxy met TLS voor.
--branch NAAM Branch om te installeren (standaard: ${BRANCH})
--token TOKEN Toegangstoken, als je git-repo niet openbaar is
--user NAAM Account waaronder Server Up draait ('serverup' wordt zo
nodig aangemaakt; 'root' is de oude situatie). Zonder deze
optie wordt ernaar gevraagd.
--admin NAAM Maak meteen een beheerdersaccount met deze naam
--admin-password-file PAD
Lees het wachtwoord uit een bestand. Kan ook via de
omgevingsvariabele SU_ADMIN_PASSWORD. Geef je geen van
beide, dan wordt ernaar gevraagd; kan dat niet (geen
terminal), dan wordt er één aangemaakt en getoond.
--create-admin Alleen het beheerdersaccount aanmaken, bij een installatie
die al draait. Werkt zolang er nog geen account bestaat.
--update Bijwerken naar de nieuwste versie
--uninstall Stoppen en verwijderen (je gegevens blijven staan)
--yes Niets vragen, overal ja op antwoorden
--dry-run Alleen tonen wat er zou gebeuren
--help Deze uitleg
Het wachtwoord staat bewust niet als optie op de opdrachtregel: die is voor
iedereen op de server te zien met 'ps' en blijft in je shell-geschiedenis staan.
Voorbeelden:
sh install.sh
sh install.sh --bind 0.0.0.0 --port 8080
sh install.sh --admin ramon
SU_ADMIN_PASSWORD=... sh install.sh --admin ramon --yes
sh install.sh --update
UITLEG
}
# ── Argumenten ───────────────────────────────────────────────────────────────
while [ $# -gt 0 ]; do
case "$1" in
--dir) DOEL="${2:?--dir vereist een pad}"; shift 2 ;;
--port) POORT="${2:?--port vereist een nummer}"; POORT_EXPLICIET=1; shift 2 ;;
--bind) BIND="${2:?--bind vereist een adres}"; BIND_EXPLICIET=1; shift 2 ;;
--branch) BRANCH="${2:?--branch vereist een naam}"; shift 2 ;;
--token) TOKEN="${2:?--token vereist een waarde}"; shift 2 ;;
--user) GEBRUIKER="${2:?--user vereist een accountnaam}"
GEBRUIKER_EXPLICIET=1; shift 2 ;;
--admin) BEHEERDER="${2:?--admin vereist een gebruikersnaam}"; shift 2 ;;
--admin-password-file)
BEHEERDER_WW_BESTAND="${2:?--admin-password-file vereist een pad}"; shift 2 ;;
--admin-password)
fout "Gebruik --admin-password-file of SU_ADMIN_PASSWORD; een wachtwoord op de opdrachtregel is voor iedereen zichtbaar met 'ps'." ;;
--update) ACTIE="update"; shift ;;
--uninstall) ACTIE="uninstall"; shift ;;
--create-admin) ACTIE="create-admin"; shift ;;
--yes|-y) JA=1; shift ;;
--dry-run) DROOG=1; shift ;;
--help|-h) gebruik; exit 0 ;;
*) fout "Onbekende optie: $1 (gebruik --help)" ;;
esac
done
case "$POORT" in ''|*[!0-9]*) fout "Ongeldige poort: $POORT" ;; esac
[ "$POORT" -ge 1 ] && [ "$POORT" -le 65535 ] || fout "Poort buiten bereik: $POORT"
# Docker publiceert op een adres, niet op een hostnaam. Alleen cijfers, punten
# en dubbele punten (IPv6) toestaan, zodat een typefout hier opvalt en niet pas
# als 'docker compose up' er een onbegrijpelijke fout over geeft.
geldig_bind() {
case "$1" in
''|*[!0-9.:a-fA-F]*) return 1 ;;
*) return 0 ;;
esac
}
geldig_bind "$BIND" || fout "Ongeldig bind-adres: $BIND (verwacht een IP-adres, bijvoorbeeld 127.0.0.1 of 0.0.0.0)"
# ── Rechten ──────────────────────────────────────────────────────────────────
# Alleen sudo gebruiken waar het echt nodig is: kan de gebruiker zelf al in de
# doelmap schrijven en docker aanroepen, dan blijft alles onder zijn eigen account.
SUDO=""
bepaal_rechten() {
if [ "$(id -u)" -eq 0 ]; then
return 0
fi
bovenliggend="$(dirname "$DOEL")"
if [ -w "$bovenliggend" ] || { [ -d "$DOEL" ] && [ -w "$DOEL" ]; }; then
if docker info >/dev/null 2>&1; then
goed "Draait onder je eigen account; sudo is niet nodig"
return 0
fi
fi
if heeft sudo; then
SUDO="sudo"
log "sudo wordt gebruikt voor ${DOEL} en docker"
else
fout "Geen root en geen sudo. Draai als root, of kies met --dir een map waar je zelf in mag schrijven."
fi
}
# ── Vereisten ────────────────────────────────────────────────────────────────
controleer_systeem() {
stap "Systeem controleren"
bepaal_rechten
[ "$(uname -s)" = "Linux" ] || waarschuw "Dit script is voor Linux; op $(uname -s) is het onbeproefd."
for hulp in curl tar; do
heeft "$hulp" || fout_tenzij_droog "'$hulp' ontbreekt. Installeer het en probeer opnieuw."
done
goed "$(uname -s) $(uname -m)"
}
controleer_docker() {
# Bij een proefdraai niets aanroepen wat via sudo loopt: dan staat er
# ineens een wachtwoordvraag op het scherm terwijl we net beloofd hebben
# dat er niets gebeurt — en zonder terminal blijft hij daarop hangen.
if [ "$DROOG" -eq 1 ]; then
if heeft docker; then
goed "Docker aanwezig (de daemon wordt bij een proefdraai niet aangesproken)"
else
waarschuw "Docker is niet geïnstalleerd; het script zou aanbieden dat te doen."
fi
return 0
fi
if heeft docker; then
if $SUDO docker info >/dev/null 2>&1; then
goed "Docker draait ($($SUDO docker version --format '{{.Server.Version}}' 2>/dev/null || echo '?'))"
else
fout_tenzij_droog "Docker is geïnstalleerd maar de daemon reageert niet. Start hem met: systemctl start docker"
fi
else
waarschuw "Docker is niet geïnstalleerd."
if vraag "Nu installeren via het officiële script van docker.com?"; then
stap "Docker installeren"
doe sh -c "curl -fsSL https://get.docker.com | $SUDO sh"
doe $SUDO systemctl enable --now docker || true
heeft docker || [ "$DROOG" -eq 1 ] || fout "Installeren van Docker is niet gelukt."
goed "Docker geïnstalleerd"
else
fout_tenzij_droog "Docker is vereist. Zie https://docs.docker.com/engine/install/"
fi
fi
if $SUDO docker compose version >/dev/null 2>&1; then
goed "Compose-plugin aanwezig"
else
fout_tenzij_droog "De docker-compose-plugin ontbreekt. Installeer 'docker-compose-plugin' via je pakketbeheerder."
fi
}
# ── Broncode ophalen ─────────────────────────────────────────────────────────
bron_url() {
if [ -n "$TOKEN" ]; then
printf 'https://%s@%s/%s.git' "$TOKEN" "$GIT_HOST" "$GIT_PAD"
else
printf 'https://%s/%s.git' "$GIT_HOST" "$GIT_PAD"
fi
}
haal_broncode() {
stap "Broncode ophalen"
if [ -d "$DOEL/.git" ]; then
log "Bestaande installatie bijwerken in $DOEL"
doe $SUDO git -C "$DOEL" fetch --depth 1 origin "$BRANCH"
doe $SUDO git -C "$DOEL" reset --hard "origin/$BRANCH"
goed "Bijgewerkt naar de nieuwste $BRANCH"
return 0
fi
doe $SUDO mkdir -p "$DOEL"
if heeft git; then
# Zonder .git kunnen we later niet netjes bijwerken, dus git heeft de
# voorkeur boven een los archief.
if doe $SUDO git clone --depth 1 --branch "$BRANCH" "$(bron_url)" "$DOEL"; then
goed "Gekloond naar $DOEL"
return 0
fi
waarschuw "Klonen mislukte; ik probeer het archief."
fi
archief="https://${GIT_HOST}/${GIT_PAD}/archive/${BRANCH}.tar.gz"
log "Downloaden: $archief"
if [ "$DROOG" -eq 0 ]; then
tijdelijk="$(mktemp -d)"
curl -fsSL ${TOKEN:+-H "Authorization: token $TOKEN"} "$archief" \
| tar xz -C "$tijdelijk" --strip-components=1 \
|| fout "Ophalen mislukt. Is de repo openbaar? Zo niet, gebruik --token."
$SUDO cp -r "$tijdelijk/." "$DOEL/"
rm -rf "$tijdelijk"
fi
goed "Uitgepakt naar $DOEL"
}
# ── Instellingen ─────────────────────────────────────────────────────────────
server_ip() { hostname -I 2>/dev/null | awk '{print $1}'; }
kies_bind() {
# Alleen vragen als er nog iets te kiezen valt: met --bind, met --yes of
# zonder terminal is de keuze al gemaakt.
[ "$BIND_EXPLICIET" -eq 1 ] && return 0
[ "$JA" -eq 1 ] && return 0
[ "$DROOG" -eq 1 ] && return 0
[ -e /dev/tty ] || return 0
# Installeer je opnieuw over een bestaande map, dan is je huidige keuze het
# uitgangspunt — anders zet enter je server ongemerkt terug op loopback.
huidig=""
if [ -f "$DOEL/.env" ]; then
lees_env_terug "$DOEL/.env"
huidig="$BIND"
fi
ip="$(server_ip)"
stap "Bereikbaarheid"
printf ' Waarop moet de webinterface luisteren?\n\n'
printf ' 1) Alleen deze server (127.0.0.1) — veiligst\n'
printf ' Erbij via een SSH-tunnel of een reverse proxy op deze machine.\n'
printf ' 2) Het hele netwerk (0.0.0.0)\n'
printf ' Direct bereikbaar op http://%s:%s — zet er wel TLS voor.\n' "${ip:-<server-ip>}" "$POORT"
printf ' 3) Een specifiek adres\n\n'
if [ -n "$huidig" ]; then
printf ' Nu ingesteld: %s\n' "$huidig"
printf ' Keuze [enter = zo laten]: '
else
printf ' Keuze [1]: '
fi
read -r keuze </dev/tty || keuze=""
case "$keuze" in
1) BIND="127.0.0.1" ;;
2) BIND="0.0.0.0" ;;
3) printf ' Adres: '
read -r adres </dev/tty || adres=""
geldig_bind "$adres" || fout "Ongeldig bind-adres: $adres"
BIND="$adres" ;;
'') BIND="${huidig:-127.0.0.1}" ;;
*) fout "Onbekende keuze: $keuze" ;;
esac
BIND_EXPLICIET=1
goed "Luistert op $BIND"
}
zet_env_regel() {
# zet_env_regel BESTAND SLEUTEL WAARDE — vervangt de regel of voegt hem toe.
# Waarden zijn hier altijd al gevalideerd (cijfers, punten, dubbele punten).
$SUDO sh -c "sed -i '/^$2=/d' '$1' && printf '%s=%s\n' '$2' '$3' >> '$1'"
}
# ── Onder welk account draait Server Up? ─────────────────────────────────────
# Server Up start als root — dat moet, want het docker-volume is van root — en
# zakt daarna af naar dit account. Zie docs/beveiliging.md voor wat dat wel en
# niet oplevert.
uid_van() { getent passwd "$1" 2>/dev/null | cut -d: -f3; }
gid_van() { getent passwd "$1" 2>/dev/null | cut -d: -f4; }
ingelogde_gebruiker() {
# Wie de installatie aanstuurt. Onder sudo is dat niet 'root' maar degene
# die het aanriep; zonder sudo valt logname terug op de terminal.
naam="${SUDO_USER:-}"
[ -n "$naam" ] || naam="$(logname 2>/dev/null || echo '')"
[ "$naam" = "root" ] && naam=""
printf '%s' "$naam"
}
bruikbare_gebruikers() {
# Gewone accounts: uid 100060000 en een echte shell. Systeemaccounts
# (daemon, www-data, nobody) horen hier niet tussen — die hebben eigen
# rechten en zouden verwarrend zijn om uit te kiezen.
awk -F: '$3 >= 1000 && $3 < 60000 &&
$7 !~ /(nologin|false|sync)$/ { print $1 " (uid " $3 ")" }' \
/etc/passwd 2>/dev/null
}
maak_serverup_account() {
# Systeemaccount: geen shell, geen wachtwoord, geen thuismap. Het bestaat
# alleen om eigenaar te zijn van bestanden.
if [ -n "$(uid_van serverup)" ]; then
log "Account 'serverup' bestaat al"
return 0
fi
if [ "$DROOG" -eq 1 ]; then
printf ' [droog] account serverup aanmaken (systeemaccount, geen shell)\n'
return 0
fi
if heeft useradd; then
$SUDO useradd --system --no-create-home --shell /usr/sbin/nologin serverup \
|| fout "Kon het account 'serverup' niet aanmaken."
elif heeft adduser; then
$SUDO adduser --system --no-create-home --shell /usr/sbin/nologin serverup \
|| fout "Kon het account 'serverup' niet aanmaken."
else
fout "Geen useradd of adduser gevonden. Maak zelf een account en geef het op met --user NAAM."
fi
goed "Account 'serverup' aangemaakt"
}
zet_gebruiker() {
# zet_gebruiker NAAM — vertaal een naam naar SU_UID/SU_GID.
SU_NAAM="$1"
if [ "$1" = "root" ]; then
SU_UID=0; SU_GID=0
waarschuw "Server Up draait als root; dat is de oude situatie."
return 0
fi
_uid="$(uid_van "$1")"
if [ -z "$_uid" ]; then
if [ "$1" = "serverup" ]; then
maak_serverup_account
_uid="$(uid_van serverup)"
fi
if [ -z "$_uid" ] && [ "$DROOG" -eq 1 ] && [ "$1" = "serverup" ]; then
# Alleen 'serverup' zouden we zélf hebben aangemaakt; dat het nu
# nog ontbreekt is dus geen fout. Een andere naam die niet bestaat
# is een typefout, en die hoor je juist bij een proefdraai te horen.
SU_UID=""; SU_GID=""
log "Zou draaien als 'serverup' (uid wordt bij het aanmaken toegekend)"
return 0
fi
[ -n "$_uid" ] || fout "Account '$1' bestaat niet. Maak het aan of kies een ander."
fi
SU_UID="$_uid"
SU_GID="$(gid_van "$1")"
SU_GID="${SU_GID:-$SU_UID}"
goed "Server Up draait als $1 ($SU_UID:$SU_GID)"
# Bewust géén lidmaatschap van de docker-groep: wie daarin zit kan een
# container starten die / mount, en is dus root op deze machine. De
# container leest de gid van de socket zelf uit en heeft dit niet nodig.
log "Niet toegevoegd aan de docker-groep — dat zou root-toegang geven en is niet nodig."
}
kies_gebruiker() {
[ "$GEBRUIKER_EXPLICIET" -eq 1 ] && { zet_gebruiker "$GEBRUIKER"; return 0; }
# Wat er al staat wint: bij --update wordt kies_bind niet aangeroepen, dus
# zonder dit zou een bestaande installatie elke keer opnieuw vragen.
lees_env_terug "$DOEL/.env"
[ -n "$SU_UID" ] && return 0
if [ "$JA" -eq 1 ] || [ ! -e /dev/tty ]; then
zet_gebruiker serverup
return 0
fi
_ingelogd="$(ingelogde_gebruiker)"
stap "Account"
printf ' Onder welk account moet Server Up draaien?\n'
printf ' Het draait nu nog als root; dat hoeft niet voor alles.\n\n'
printf " 1) Een nieuw account 'serverup' aanmaken — aanbevolen\n"
printf ' Systeemaccount zonder shell en zonder wachtwoord.\n'
if [ -n "$_ingelogd" ]; then
printf ' 2) %s — het account waarmee je nu werkt\n' "$_ingelogd"
else
printf ' 2) (geen ingelogd account gevonden)\n'
fi
printf ' 3) Een bestaand account kiezen\n'
printf ' 4) Als root draaien — zoals voorheen\n\n'
printf ' Keuze [1]: '
read -r keuze </dev/tty || keuze=""
case "$keuze" in
''|1) zet_gebruiker serverup ;;
2) [ -n "$_ingelogd" ] || fout "Er is geen ingelogd account gevonden; kies 1, 3 of 4."
zet_gebruiker "$_ingelogd" ;;
3) printf '\n Bruikbare accounts:\n'
bruikbare_gebruikers | sed 's/^/ /'
printf '\n Naam: '
read -r naam </dev/tty || naam=""
naam="$(printf '%s' "$naam" | tr -d '[:space:]')"
[ -n "$naam" ] || fout "Geen naam opgegeven."
zet_gebruiker "$naam" ;;
4) zet_gebruiker root ;;
*) fout "Onbekende keuze: $keuze" ;;
esac
}
zet_mappen_klaar() {
# De hoofdmap en de twee mappen die Server Up zélf beheert alvast op de
# juiste eigenaar zetten, zodat de eerste start niet op rechten struikelt.
#
# appdata blijft er bewust buiten: die mappen zijn van de apps (postgres
# draait als 999, een linuxserver-image als jouw PUID). Er een chown
# overheen halen breekt precies die containers.
[ "${SU_UID:-0}" = "0" ] && [ "$DROOG" -eq 0 ] && return 0
[ "${SU_NAAM:-root}" = "root" ] && return 0
_basis="${BASE_DIR:-/opt/serverup}"
if [ "$DROOG" -eq 1 ]; then
printf ' [droog] %s, %s/stacks en %s/backups op %s zetten (appdata blijft ongemoeid)\n' \
"$_basis" "$_basis" "$_basis" "$SU_NAAM"
return 0
fi
$SUDO mkdir -p "$_basis/stacks" "$_basis/backups" "$_basis/appdata"
$SUDO chown "$SU_UID:$SU_GID" "$_basis"
$SUDO chown -R "$SU_UID:$SU_GID" "$_basis/stacks" "$_basis/backups"
goed "Mappen klaargezet onder $_basis"
}
lees_env_terug() {
# Wat er in .env staat is de waarheid, niet wat dit script als standaard
# heeft. Zonder dit klopt bij --update de wachtlus en het slotadres niet
# meer zodra je ooit een andere poort hebt gekozen.
[ -f "$1" ] || return 0
# Lezen kan meestal zonder sudo; alleen als het bestand van root is en
# afgeschermd, is de omweg nodig.
if [ -r "$1" ]; then
_lees="cat"
elif [ -n "$SUDO" ]; then
_lees="$SUDO cat"
elif heeft sudo; then
_lees="sudo cat"
else
_lees="cat"
fi
_inhoud="$($_lees "$1" 2>/dev/null || true)"
_poort="$(printf '%s\n' "$_inhoud" | sed -n 's/^PORT=//p' | tail -1)"
_bind="$(printf '%s\n' "$_inhoud" | sed -n 's/^BIND=//p' | tail -1)"
# SU_UID/SU_GID en BASE_DIR overnemen, zodat --update niet opnieuw vraagt
# onder welk account het moet draaien en niet de verkeerde mappen chownt.
if [ -z "$SU_UID" ]; then
_uid="$(printf '%s\n' "$_inhoud" | sed -n 's/^SU_UID=//p' | tail -1)"
case "$_uid" in ''|*[!0-9]*) ;; *) SU_UID="$_uid"
_gid="$(printf '%s\n' "$_inhoud" | sed -n 's/^SU_GID=//p' | tail -1)"
case "$_gid" in ''|*[!0-9]*) SU_GID="$_uid" ;; *) SU_GID="$_gid" ;; esac ;;
esac
fi
if [ -z "$BASE_DIR" ]; then
_basis="$(printf '%s\n' "$_inhoud" | sed -n 's/^BASE_DIR=//p' | tail -1)"
case "$_basis" in /*) BASE_DIR="$_basis" ;; esac
fi
if [ "$POORT_EXPLICIET" -eq 0 ]; then
case "$_poort" in ''|*[!0-9]*) ;; *) POORT="$_poort" ;; esac
fi
if [ "$BIND_EXPLICIET" -eq 0 ] && [ -n "$_bind" ] && geldig_bind "$_bind"; then
BIND="$_bind"
fi
return 0
}
schrijf_env() {
stap "Instellingen"
env_bestand="$DOEL/.env"
if [ -f "$env_bestand" ]; then
# Een bestaande .env bevat jouw instellingen; die blijven staan. Alleen
# wat je nu expliciet meegeeft wordt doorgevoerd — dat is precies het
# verschil dat eerder ontbrak, waardoor --bind niets deed zodra er al
# een .env stond.
gewijzigd=0
if [ "$BIND_EXPLICIET" -eq 1 ]; then
if [ "$DROOG" -eq 1 ]; then
printf ' [droog] BIND=%s in bestaande .env zetten\n' "$BIND"
else
zet_env_regel "$env_bestand" BIND "$BIND"
fi
gewijzigd=1
fi
if [ "$POORT_EXPLICIET" -eq 1 ]; then
if [ "$DROOG" -eq 1 ]; then
printf ' [droog] PORT=%s in bestaande .env zetten\n' "$POORT"
else
zet_env_regel "$env_bestand" PORT "$POORT"
fi
gewijzigd=1
fi
# Het account is een keuze die je net gemaakt hebt; die hoort er altijd
# in te komen, ook bij een --update van een installatie die nog als root
# draaide.
if [ -n "$SU_UID" ]; then
if [ "$DROOG" -eq 1 ]; then
printf ' [droog] SU_UID=%s SU_GID=%s in bestaande .env zetten\n' \
"$SU_UID" "$SU_GID"
else
zet_env_regel "$env_bestand" SU_UID "$SU_UID"
zet_env_regel "$env_bestand" SU_GID "$SU_GID"
fi
gewijzigd=1
fi
if [ "$gewijzigd" -eq 1 ]; then
goed "Bestaande .env bijgewerkt (BIND=$BIND, PORT=$POORT)"
else
lees_env_terug "$env_bestand"
goed "Bestaande .env blijft ongemoeid (BIND=$BIND, PORT=$POORT)"
fi
return 0
fi
if [ "$DROOG" -eq 1 ]; then
printf ' [droog] .env aanmaken met BIND=%s PORT=%s, draaiend als %s\n' \
"$BIND" "$POORT" "${SU_NAAM:-root}"
return 0
fi
if [ -f "$DOEL/.env.example" ]; then
$SUDO cp "$DOEL/.env.example" "$env_bestand"
else
printf '' | $SUDO tee "$env_bestand" >/dev/null
fi
$SUDO sh -c "sed -i '/^BIND=/d;/^PORT=/d' '$env_bestand'"
$SUDO sh -c "printf 'BIND=%s\nPORT=%s\n' '$BIND' '$POORT' >> '$env_bestand'"
if [ -n "$SU_UID" ]; then
zet_env_regel "$env_bestand" SU_UID "$SU_UID"
zet_env_regel "$env_bestand" SU_GID "$SU_GID"
fi
goed "Aangemaakt: $env_bestand (BIND=$BIND, PORT=$POORT)"
}
# ── Starten ──────────────────────────────────────────────────────────────────
start() {
stap "Bouwen en starten"
if [ "$DROOG" -eq 0 ] && [ ! -f "$DOEL/docker-compose.yml" ]; then
fout "Geen docker-compose.yml in $DOEL — het ophalen van de broncode is misgegaan."
fi
log "Dit duurt de eerste keer enkele minuten."
doe $SUDO docker compose -f "$DOEL/docker-compose.yml" --project-directory "$DOEL" \
up -d --build --remove-orphans
goed "Container gestart"
}
wacht_op_gereed() {
[ "$DROOG" -eq 1 ] && return 0
stap "Wachten tot Server Up reageert"
printf ' '
i=0
while [ "$i" -lt 45 ]; do
if curl -fsS "http://127.0.0.1:${POORT}/healthz" >/dev/null 2>&1; then
# Spatie na de dubbele punt toestaan: of json compact is,
# hangt af van de Flask-versie.
versie="$(curl -fsS "http://127.0.0.1:${POORT}/healthz" 2>/dev/null \
| sed -n 's/.*"version" *: *"\([^"]*\)".*/\1/p')"
printf '\n'
goed "Server Up${versie:+ }${versie:-} draait"
return 0
fi
# Een puntje per poging, zodat je ziet dat er nog iets gebeurt.
printf '.'
i=$((i + 1))
sleep 2
done
printf '\n'
waarschuw "Server Up reageerde niet binnen anderhalve minuut."
log "Bekijk de logs met: $SUDO docker compose -f $DOEL/docker-compose.yml logs --tail=50"
return 1
}
# ── Beheerdersaccount ────────────────────────────────────────────────────────
# Tussen "container draait" en "eerste account" ligt een venster waarin iedereen
# die de pagina bereikt zichzelf beheerder kan maken. Dat venster hoort zo kort
# mogelijk te zijn, dus kan het script het account meteen aanmaken.
WW_GEGENEREERD=0
BEHEERDER_WW_TOON=""
json_tekst() {
# Escapet een waarde voor gebruik tussen JSON-aanhalingstekens.
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}
genereer_wachtwoord() {
LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom 2>/dev/null | head -c 24
}
vraag_wachtwoord() {
# Twee keer intypen, met de echo uit. Mislukt dat, dan valt hij terug op
# een gegenereerd wachtwoord in plaats van er een half account van te maken.
oude_stty=""
if stty -g </dev/tty >/dev/null 2>&1; then oude_stty="$(stty -g </dev/tty)"; fi
poging=0
while [ "$poging" -lt 3 ]; do
poging=$((poging + 1))
if [ -n "$oude_stty" ]; then stty -echo </dev/tty; fi
printf ' Wachtwoord voor %s (minstens 10 tekens): ' "$BEHEERDER"
read -r ww1 </dev/tty || ww1=""
printf '\n Nogmaals: '
read -r ww2 </dev/tty || ww2=""
if [ -n "$oude_stty" ]; then stty "$oude_stty" </dev/tty; fi
printf '\n'
if [ "$ww1" != "$ww2" ]; then
waarschuw "De twee invoeren verschillen."
elif [ "${#ww1}" -lt 10 ]; then
waarschuw "Minstens 10 tekens, dat is ook wat de server eist."
else
BEHEERDER_WW="$ww1"
return 0
fi
done
waarschuw "Geen bruikbaar wachtwoord ingevoerd; er wordt er een aangemaakt."
return 1
}
bepaal_wachtwoord() {
# Volgorde: bestand, omgevingsvariabele, vragen, anders zelf aanmaken.
[ -n "$BEHEERDER_WW" ] && return 0
if [ -n "$BEHEERDER_WW_BESTAND" ]; then
[ -r "$BEHEERDER_WW_BESTAND" ] || fout "Kan $BEHEERDER_WW_BESTAND niet lezen."
BEHEERDER_WW="$(head -n 1 "$BEHEERDER_WW_BESTAND")"
[ -n "$BEHEERDER_WW" ] || fout "$BEHEERDER_WW_BESTAND is leeg."
return 0
fi
if [ -n "${SU_ADMIN_PASSWORD:-}" ]; then
BEHEERDER_WW="$SU_ADMIN_PASSWORD"
return 0
fi
if [ "$JA" -eq 0 ] && [ -e /dev/tty ]; then
vraag_wachtwoord && return 0
fi
BEHEERDER_WW="$(genereer_wachtwoord)"
[ -n "$BEHEERDER_WW" ] || fout "Kon geen wachtwoord aanmaken (/dev/urandom niet leesbaar). Gebruik --admin-password-file."
WW_GEGENEREERD=1
return 0
}
vraag_beheerder() {
# Niets meegegeven, maar wel een terminal? Dan hier aanbieden, want dit is
# het moment waarop het uitmaakt.
[ -n "$BEHEERDER" ] && return 0
[ "$JA" -eq 1 ] && return 0
[ "$DROOG" -eq 1 ] && return 0
[ -e /dev/tty ] || return 0
stap "Beheerdersaccount"
if ! vraag "Nu meteen een beheerdersaccount aanmaken?"; then
return 0
fi
printf ' Gebruikersnaam: '
read -r naam </dev/tty || naam=""
naam="$(printf '%s' "$naam" | tr -d '[:space:]')"
if [ -z "$naam" ]; then
waarschuw "Geen naam opgegeven; overgeslagen."
return 0
fi
BEHEERDER="$naam"
return 0
}
beheerder_vooraf() {
# Alles wat om invoer vraagt gebeurt vóór het bouwen, zodat je daarna kunt
# weglopen terwijl het image gebouwd wordt.
[ "$DROOG" -eq 1 ] && return 0
vraag_beheerder
[ -n "$BEHEERDER" ] || return 0
bepaal_wachtwoord
return 0
}
maak_beheerder() {
[ -n "$BEHEERDER" ] || return 0
if [ "$DROOG" -eq 1 ]; then
stap "Beheerdersaccount"
printf ' [droog] account %s aanmaken via POST /api/auth/setup\n' "$BEHEERDER"
return 0
fi
bepaal_wachtwoord
# Het wachtwoord gaat via stdin naar curl, niet via de opdrachtregel:
# argumenten zijn voor elke gebruiker op de server zichtbaar in 'ps'.
antwoord="$(mktemp 2>/dev/null || echo /tmp/su-setup.$$)"
code="$(printf '{"username":"%s","password":"%s"}' \
"$(json_tekst "$BEHEERDER")" "$(json_tekst "$BEHEERDER_WW")" \
| curl -sS -o "$antwoord" -w '%{http_code}' \
-X POST -H 'Content-Type: application/json' \
--data-binary @- \
"http://127.0.0.1:${POORT}/api/auth/setup" 2>/dev/null || true)"
# Bij een mislukte verbinding schrijft curl zelf al "000"; kan curl helemaal
# niet draaien, dan blijft het leeg. Beide gevallen zijn hetzelfde verhaal.
case "$code" in ''|*[!0-9]*) code="000" ;; esac
case "$code" in
200) goed "Beheerder '$BEHEERDER' aangemaakt"
# Alleen een zelf aangemaakt wachtwoord moet nog getoond worden;
# een ingetypt of meegegeven wachtwoord kent de gebruiker al.
if [ "$WW_GEGENEREERD" -eq 1 ]; then
BEHEERDER_WW_TOON="$BEHEERDER_WW"
fi ;;
409) waarschuw "Er bestond al een account; er is niets gewijzigd."
BEHEERDER=""; WW_GEGENEREERD=0 ;;
000) waarschuw "Server Up was niet bereikbaar; account niet aangemaakt."
BEHEERDER=""; WW_GEGENEREERD=0 ;;
*) melding="$(sed -n 's/.*"msg" *: *"\([^"]*\)".*/\1/p' "$antwoord" 2>/dev/null)"
waarschuw "Aanmaken mislukt (HTTP $code)${melding:+: }${melding:-}"
BEHEERDER=""; WW_GEGENEREERD=0 ;;
esac
rm -f "$antwoord"
BEHEERDER_WW=""
}
afsluiting() {
if [ "$BIND" = "0.0.0.0" ]; then
ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
adres="http://${ip:-<server-ip>}:${POORT}"
else
adres="http://localhost:${POORT}"
fi
printf '\n%s────────────────────────────────────────────────────────%s\n' "$C_KOP" "$C_UIT"
printf '%s Server Up draait op %s%s\n' "$C_OK" "$adres" "$C_UIT"
printf '%s────────────────────────────────────────────────────────%s\n\n' "$C_KOP" "$C_UIT"
if [ -n "$BEHEERDER" ]; then
printf ' %s✔%s Inloggen als: %s%s%s\n' "$C_OK" "$C_UIT" "$C_KOP" "$BEHEERDER" "$C_UIT"
if [ "$WW_GEGENEREERD" -eq 1 ]; then
printf ' Wachtwoord: %s%s%s\n' "$C_KOP" "$BEHEERDER_WW_TOON" "$C_UIT"
printf ' %sSchrijf dit nu over — het staat nergens anders.%s\n' "$C_WAARSCHUW" "$C_UIT"
fi
printf '\n'
else
printf ' %sMaak nu meteen een beheerdersaccount aan.%s\n' "$C_WAARSCHUW" "$C_UIT"
printf ' Zolang dat niet gebeurd is, kan iedereen die deze pagina\n'
printf ' bereikt het account claimen.\n\n'
fi
if [ "$BIND" = "127.0.0.1" ]; then
printf ' Alleen bereikbaar vanaf deze server zelf. Van buitenaf erbij?\n'
printf ' 1. Zet een reverse proxy met TLS ervoor\n'
printf ' 2. sed -i "s/^BIND=.*/BIND=0.0.0.0/" %s/.env\n' "$DOEL"
printf ' 3. %s docker compose -f %s/docker-compose.yml up -d\n\n' "${SUDO:-}" "$DOEL"
else
printf ' %sDe interface staat open op je netwerk. Server Up beheert%s\n' "$C_WAARSCHUW" "$C_UIT"
printf ' Docker als root, dus zet er een reverse proxy met TLS voor.\n\n'
fi
printf ' Bijwerken: sh %s/install.sh --update\n' "$DOEL"
printf ' Logs: %s docker compose -f %s/docker-compose.yml logs -f\n' "${SUDO:-}" "$DOEL"
printf ' Documentatie: %s/README.md\n\n' "$DOEL"
}
verwijderen() {
stap "Server Up verwijderen"
[ -d "$DOEL" ] || fout "Niets gevonden in $DOEL"
if ! vraag "Container stoppen en verwijderen? Je gegevens blijven staan."; then
log "Afgebroken."
exit 0
fi
doe $SUDO docker compose -f "$DOEL/docker-compose.yml" --project-directory "$DOEL" \
down --remove-orphans
goed "Container gestopt en verwijderd"
printf '\n'
log "Je gegevens staan nog in het docker-volume 'su-data' en in je stackmappen."
log "Alles definitief weg? Dan:"
log " $SUDO docker volume rm su-data"
log " $SUDO rm -rf $DOEL"
printf '\n'
}
# ── Hoofdprogramma ───────────────────────────────────────────────────────────
# De testsuite laadt dit bestand om losse functies te kunnen aanroepen — vooral
# de vragen die bepalen waarop de server straks luistert. Zonder deze uitgang
# zou het script bij het inladen meteen een hele installatie beginnen.
if [ -n "${SU_INSTALL_ALLEEN_FUNCTIES:-}" ]; then
return 0 2>/dev/null || exit 0
fi
printf '\n%sServer Up — installatiescript %s%s\n' "$C_KOP" "$SCRIPT_VERSIE" "$C_UIT"
[ "$DROOG" -eq 1 ] && waarschuw "Proefdraai: er wordt niets gewijzigd."
case "$ACTIE" in
uninstall)
bepaal_rechten
verwijderen
;;
update)
[ -d "$DOEL" ] || fout "Geen installatie gevonden in $DOEL. Installeer eerst zonder --update."
controleer_systeem
controleer_docker
haal_broncode
# Bestaande installaties draaien nog als root. Eén keer vragen of dat zo
# moet blijven; wie al gekozen heeft, wordt niet opnieuw lastiggevallen
# (kies_gebruiker ziet SU_UID uit .env staan).
kies_gebruiker
# Ook bij bijwerken: expliciete --bind/--port doorvoeren, en anders
# overnemen wat er al staat.
schrijf_env
zet_mappen_klaar
start
wacht_op_gereed || true
printf '\n'
goed "Bijgewerkt."
;;
create-admin)
# Losse actie voor een installatie die al draait: handig als het
# aanmaken tijdens de installatie is misgegaan, of overgeslagen.
# Docker en schrijfrechten zijn hier niet nodig — alleen .env lezen
# en één verzoek naar de draaiende server — dus geen bepaal_rechten.
lees_env_terug "$DOEL/.env"
if [ -z "$BEHEERDER" ]; then
if [ -e /dev/tty ] && [ "$JA" -eq 0 ]; then
printf '\n Gebruikersnaam: '
read -r naam </dev/tty || naam=""
BEHEERDER="$(printf '%s' "$naam" | tr -d '[:space:]')"
fi
[ -n "$BEHEERDER" ] || fout "Geef de gebruikersnaam op met --admin NAAM."
fi
stap "Beheerdersaccount"
maak_beheerder
if [ -n "$BEHEERDER" ] && [ "$WW_GEGENEREERD" -eq 1 ]; then
printf '\n Wachtwoord: %s%s%s\n' "$C_KOP" "$BEHEERDER_WW_TOON" "$C_UIT"
printf ' %sSchrijf dit nu over — het staat nergens anders.%s\n\n' "$C_WAARSCHUW" "$C_UIT"
fi
;;
install)
controleer_systeem
controleer_docker
kies_bind
kies_gebruiker
beheerder_vooraf
haal_broncode
schrijf_env
zet_mappen_klaar
start
wacht_op_gereed || true
maak_beheerder
afsluiting
;;
esac