#!/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.2" 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:-}" BASE_DIR_EXPLICIET=0 # Heeft de server na het starten daadwerkelijk geantwoord? Bepaalt het slot en # de exitcode; bij --dry-run is er niets om op te wachten. GEREED=1 # De commit waar een --update vandaan kwam, zodat terugrollen kan als de nieuwe # versie niet opkomt. VORIGE_COMMIT="" # Verwijdert --uninstall ook je gegevens? Standaard niet. PURGE=0 # ── 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 } # Is er een terminal om iets aan te vragen? Bewust /dev/tty en niet stdin: bij # `curl … | sh` — de manier die de README noemt — is stdin de pipe, terwijl de # terminal gewoon bereikbaar is. Op stdin testen betekende dat élke vraag # ongesteld bleef en zichzelf met "ja" beantwoordde: Docker werd van # get.docker.com gehaald zonder dat het gevraagd was, en --uninstall brak je # installatie af zonder bevestiging. # # Bestaan is niet genoeg: in een container zonder terminal bestaat /dev/tty wel # maar levert openen "No such device or address". # De openpoging staat in een subshell, niet tussen accolades: ':' is een # special builtin, en een mislukte redirect daarop beëindigt volgens POSIX de # hele shell. In een subshell sneuvelt alleen die subshell. terminal_beschikbaar() { [ -e /dev/tty ] || return 1 ( : /dev/null || return 1 return 0 } vraag() { # vraag "Vraag?" -> 0 bij ja. Met --yes of zonder terminal: altijd ja. [ "$JA" -eq 1 ] && return 0 terminal_beschikbaar || return 0 printf ' %s [J/n] ' "$1" read -r antwoord /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 # Eén vraag aan de daemon in plaats van twee: `docker version` heeft de # daemon net zo goed nodig als `docker info`, en levert meteen iets om # te tonen. Lukt het niet, dan reageert hij niet. _dv="$($SUDO docker version --format '{{.Server.Version}}' 2>/dev/null || true)" if [ -n "$_dv" ]; then goed "Docker draait ($_dv)" 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 ───────────────────────────────────────────────────────── # De token gaat nooit in de URL en nooit in een argument. In de URL schrijft git # hem verbatim in .git/config, waar hij blijft staan; als argument staat hij in # /proc//cmdline en is hij voor elke gebruiker op de server te lezen met # 'ps'. Dat is precies waarom dit script --admin-password weigert. # # In plaats daarvan: een bestand met modus 0600 dat na afloop weer weg is. Git # leest het via een credential-helper, curl via --config; in beide gevallen # staat alleen het pád op de opdrachtregel. bron_url() { printf 'https://%s/%s.git' "$GIT_HOST" "$GIT_PAD"; } TOKEN_BESTAND="" CURL_BESTAND="" opruimen() { [ -n "$TOKEN_BESTAND" ] && rm -f "$TOKEN_BESTAND" [ -n "$CURL_BESTAND" ] && rm -f "$CURL_BESTAND" TOKEN_BESTAND=""; CURL_BESTAND="" } trap opruimen EXIT HUP INT TERM zet_token_klaar() { [ -n "$TOKEN" ] || return 0 [ -n "$TOKEN_BESTAND" ] && return 0 TOKEN_BESTAND="$(mktemp "${TMPDIR:-/tmp}/su-token.XXXXXX")" CURL_BESTAND="$(mktemp "${TMPDIR:-/tmp}/su-curl.XXXXXX")" chmod 600 "$TOKEN_BESTAND" "$CURL_BESTAND" # Schema en host uit bron_url halen in plaats van https hard in te vullen: # git zoekt de opgeslagen inloggegevens op exact schema + host, dus een # verschil daarin betekent stilzwijgend geen inloggegevens. _url="$(bron_url)" _schema="${_url%%://*}" _rest="${_url#*://}" _host="${_rest%%/*}" # Formaat van `git credential store`: één URL per regel met de gegevens erin. printf '%s://%s:x-oauth-basic@%s\n' "$_schema" "$TOKEN" "$_host" > "$TOKEN_BESTAND" printf 'header = "Authorization: token %s"\n' "$TOKEN" > "$CURL_BESTAND" } git_doe() { # git_doe — met de token uit het bestand als die er is. if [ -n "$TOKEN_BESTAND" ]; then doe $SUDO git -c "credential.helper=store --file=$TOKEN_BESTAND" "$@" else doe $SUDO git "$@" fi } haal_broncode() { stap "Broncode ophalen" zet_token_klaar if [ -d "$DOEL/.git" ]; then log "Bestaande installatie bijwerken in $DOEL" # Onthouden waar we vandaan komen, zodat we terug kunnen als de nieuwe # versie niet opkomt. Voor de update pas ophalen: bij een verse # installatie is er niets om naar terug te gaan. VORIGE_COMMIT="$($SUDO git -C "$DOEL" rev-parse HEAD 2>/dev/null || echo '')" [ -n "$VORIGE_COMMIT" ] && log "Huidige versie: $(printf '%.9s' "$VORIGE_COMMIT")" git_doe -C "$DOEL" fetch --depth 1 origin "$BRANCH" # FETCH_HEAD en niet origin/$BRANCH: een tag krijgt geen origin/-ref, # dus `--branch v0.7.92-beta` installeerde prima maar kon daarna nooit # meer bijgewerkt worden ("fatal: ambiguous argument"). FETCH_HEAD wijst # na een fetch altijd naar wat er net opgehaald is, tak of tag. git_doe -C "$DOEL" reset --hard FETCH_HEAD goed "Bijgewerkt naar $BRANCH" opruimen 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 git_doe clone --depth 1 --branch "$BRANCH" "$(bron_url)" "$DOEL"; then goed "Gekloond naar $DOEL" opruimen 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 ${CURL_BESTAND:+--config "$CURL_BESTAND"} "$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 opruimen 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 terminal_beschikbaar || 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:-}" "$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/null | tail -1 } 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 1000–60000 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 ] || ! terminal_beschikbaar; 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/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" ] && [ "$BASE_DIR_EXPLICIET" -eq 0 ]; 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 # Alleen als je er expliciet om vroeg: anders zou een --update de # hoofdmap terugzetten terwijl je data ergens anders staat. if [ "$BASE_DIR_EXPLICIET" -eq 1 ] && [ -n "$BASE_DIR" ]; then if [ "$DROOG" -eq 1 ]; then printf ' [droog] BASE_DIR=%s in bestaande .env zetten\n' "$BASE_DIR" else zet_env_regel "$env_bestand" BASE_DIR "$BASE_DIR" fi waarschuw "BASE_DIR gewijzigd; bestaande data verhuist hier niet mee." log "Verplaatsen doe je vanuit Instellingen → Paden." 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 BASE_DIR=%s, draaiend als %s\n' \ "$BIND" "$POORT" "${BASE_DIR:-/opt/serverup}" "${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 [ -n "$BASE_DIR" ] && zet_env_regel "$env_bestand" BASE_DIR "$BASE_DIR" goed "Aangemaakt: $env_bestand (BIND=$BIND, PORT=$POORT)" } # ── Starten ────────────────────────────────────────────────────────────────── # Draait deze installatie vanaf een kant-en-klaar image uit een registry? Dan # scheelt ophalen minuten ten opzichte van zelf bouwen. `SU_IMAGE` is het teken: # staat er een registry-pad (met een schuine streep) in, dan valt er iets te # halen. Zonder blijft het bouwen, want dan bestaat het image nergens anders. uit_registry() { _img="$(sed -n 's/^SU_IMAGE=//p' "$DOEL/.env" 2>/dev/null | tail -1)" case "$_img" in */*) return 0 ;; *) return 1 ;; esac } start() { _compose="$SUDO docker compose -f $DOEL/docker-compose.yml --project-directory $DOEL" 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 if uit_registry; then stap "Ophalen en starten" log "Deze installatie draait vanaf een registry-image." # shellcheck disable=SC2086 if doe $_compose pull; then # shellcheck disable=SC2086 doe $_compose up -d --remove-orphans goed "Container gestart" return 0 fi waarschuw "Ophalen mislukte; ik bouw het image zelf." fi stap "Bouwen en starten" log "Dit duurt de eerste keer enkele minuten." # shellcheck disable=SC2086 doe $_compose up -d --build --remove-orphans goed "Container gestart" } wacht_op_gereed() { [ "$DROOG" -eq 1 ] && return 0 # Aantal pogingen is instelbaar: op een trage machine duurt de eerste start # langer dan anderhalve minuut, en de tests willen niet zo lang wachten. pogingen="${SU_WACHT_POGINGEN:-45}" basis="http://$(probe_adres):${POORT}" stap "Wachten tot Server Up reageert" printf ' ' i=0 # Zonder tijdslimiet blijft curl hangen op een adres waar niets naartoe # routeert, en betekent het aantal pogingen niets meer. tijden="--connect-timeout 2 --max-time 5" while [ "$i" -lt "$pogingen" ]; do if curl -fsS $tijden "${basis}/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 $tijden "${basis}/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 op ${basis}." 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/null 2>&1; then oude_stty="$(stty -g /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:-}:${POORT}" else adres="http://localhost:${POORT}" fi printf '\n%s────────────────────────────────────────────────────────%s\n' "$C_KOP" "$C_UIT" if [ "$GEREED" -eq 1 ]; then printf '%s Server Up draait op %s%s\n' "$C_OK" "$adres" "$C_UIT" else printf '%s Server Up antwoordde niet op %s%s\n' "$C_WAARSCHUW" "$adres" "$C_UIT" fi printf '%s────────────────────────────────────────────────────────%s\n\n' "$C_KOP" "$C_UIT" if [ "$GEREED" -eq 0 ]; then printf ' De container is aangemaakt, maar reageerde nog niet. Kijk wat\n' printf ' er aan de hand is met:\n' printf ' %s docker compose -f %s/docker-compose.yml logs --tail=50\n\n' "${SUDO:-}" "$DOEL" fi 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.%s Server Up beheert de\n' "$C_WAARSCHUW" "$C_UIT" printf ' Docker-daemon, dus wie erbij kan, kan alles op deze server.\n' printf ' Zet er een reverse proxy met TLS voor.\n\n' fi printf ' Draait als: %s\n' "${SU_NAAM:-root}" 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" } # ── Terugrollen ────────────────────────────────────────────────────────────── # Een update die niet opkomt liet je met een stilstaande server zitten en een # rijtje commando's om zelf uit te voeren. De vorige commit is bekend, dus dat # kan het script net zo goed zelf doen. terugrollen() { [ -n "$VORIGE_COMMIT" ] || return 1 stap "Terugrollen" log "Terug naar $(printf '%.9s' "$VORIGE_COMMIT")…" if ! $SUDO git -C "$DOEL" reset --hard "$VORIGE_COMMIT" >/dev/null 2>&1; then waarschuw "Kon niet terug naar de vorige versie." return 1 fi start if wacht_op_gereed; then goed "Terug op de vorige versie; die draait weer." return 0 fi waarschuw "Ook de vorige versie komt niet op; er is meer aan de hand." return 1 } # ── Doorlichten ────────────────────────────────────────────────────────────── # De fouten die dit script had, waren allemaal van dezelfde soort: iets klopte # niet en niemand merkte het, omdat er nergens werd nagekeken. Dit is het # tegengif — het controleert de aannames in plaats van ze te doen. DOK_FOUTEN=0 DOK_WAARSCHUWINGEN=0 dok_ok() { goed "$1"; } dok_waar() { waarschuw "$1"; DOK_WAARSCHUWINGEN=$((DOK_WAARSCHUWINGEN + 1)); } dok_fout() { printf ' %s✖%s %s\n' "$C_FOUT" "$C_UIT" "$1"; DOK_FOUTEN=$((DOK_FOUTEN + 1)); } _dok_inspect() { # _dok_inspect FORMAAT — docker inspect op de container, of leeg. $SUDO docker inspect server-up --format "$1" 2>/dev/null || true } doorlichten() { stap "Installatie" if [ -d "$DOEL" ] && [ -f "$DOEL/docker-compose.yml" ]; then dok_ok "Gevonden in $DOEL" else dok_fout "Geen installatie in $DOEL (geen docker-compose.yml)" printf '\n' return 1 fi if [ -f "$DOEL/.env" ]; then _rechten="$(stat -c '%a' "$DOEL/.env" 2>/dev/null || echo '?')" case "$_rechten" in 600|400) dok_ok ".env staat op $_rechten" ;; '?') dok_waar "Rechten van .env niet te bepalen" ;; *) dok_waar ".env staat op $_rechten; er staan tokens en wachtwoorden in. Zet hem op 600." ;; esac else dok_waar "Geen .env in $DOEL; alles draait op de standaardwaarden" fi [ -f "$DOEL/VERSION" ] && log "Versie in de map: $(cat "$DOEL/VERSION" 2>/dev/null)" stap "Docker" if $SUDO docker info >/dev/null 2>&1; then dok_ok "Daemon reageert ($($SUDO docker version --format '{{.Server.Version}}' 2>/dev/null || echo '?'))" else dok_fout "De Docker-daemon reageert niet" printf '\n' return 1 fi stap "Container" _staat="$(_dok_inspect '{{.State.Status}}')" if [ -z "$_staat" ]; then dok_fout "Er is geen container 'server-up'. Start hem met: $SUDO docker compose -f $DOEL/docker-compose.yml up -d" elif [ "$_staat" = "running" ]; then _gezond="$(_dok_inspect '{{if .State.Health}}{{.State.Health.Status}}{{end}}')" dok_ok "Draait${_gezond:+ (}${_gezond:-}${_gezond:+)}" [ "${_gezond:-healthy}" = "healthy" ] || dok_waar "De healthcheck staat op '$_gezond'" else dok_fout "De container staat op '$_staat'" fi _img="$(_dok_inspect '{{.Config.Image}}')" [ -n "$_img" ] && log "Image: $_img" stap "Bereikbaarheid" _poort="$(env_waarde PORT)"; _poort="${_poort:-$POORT}" _bind="$(env_waarde BIND)" [ -n "$_bind" ] && geldig_bind "$_bind" && BIND="$_bind" POORT="$_poort" _adres="http://$(probe_adres):${POORT}" if curl -fsS --connect-timeout 3 --max-time 8 "${_adres}/healthz" >/dev/null 2>&1; then dok_ok "Antwoordt op $_adres" else dok_fout "Geen antwoord op $_adres" log "Dit is het adres waarop compose publiceert (BIND=$BIND)." fi stap "Account" _uid_env="$(env_waarde SU_UID)" _uid_echt="$($SUDO docker exec server-up id -u 2>/dev/null || echo '')" if [ -z "$_uid_echt" ]; then dok_waar "Kon niet in de container kijken onder welk account hij draait" elif [ "$_uid_echt" = "0" ]; then if [ -n "$_uid_env" ] && [ "$_uid_env" != "0" ]; then dok_fout "SU_UID=$_uid_env staat in .env maar hij draait als root. Hercreëer de container: $SUDO docker compose -f $DOEL/docker-compose.yml up -d" else dok_waar "Draait als root. Zie docs/beveiliging.md; sh $DOEL/install.sh --update laat je kiezen." fi else dok_ok "Draait als uid $_uid_echt" [ -n "$_uid_env" ] && [ "$_uid_env" != "$_uid_echt" ] \ && dok_waar "In .env staat SU_UID=$_uid_env; hercreëer de container om dat door te voeren." fi stap "Mappen" _basis="$(env_waarde BASE_DIR)"; _basis="${_basis:-/opt/serverup}" log "BASE_DIR: $_basis" if $SUDO docker exec server-up test -d "$_basis" 2>/dev/null; then # Bestaan is niet genoeg: zonder mount schrijft hij in zijn eigen # overlay en zie je op de host niets terug. if $SUDO docker exec server-up sh -c "grep -q ' $_basis ' /proc/self/mountinfo" 2>/dev/null; then dok_ok "$_basis is in de container gekoppeld" else dok_fout "$_basis bestaat in de container maar is niet gekoppeld; wat hij daar schrijft komt niet op de host terecht" fi else dok_waar "Kon $_basis niet in de container nakijken" fi for _sub in stacks appdata backups; do if [ -d "$_basis/$_sub" ]; then log " $_sub: $(find "$_basis/$_sub" -maxdepth 1 -mindepth 1 2>/dev/null | wc -l) items, eigenaar $(stat -c '%U:%G' "$_basis/$_sub" 2>/dev/null || echo '?')" else dok_waar " $_sub ontbreekt nog ($_basis/$_sub)" fi done if heeft df; then _vrij="$(df -h "$_basis" 2>/dev/null | awk 'NR>1 {print $4; exit}')" [ -n "$_vrij" ] && log "Vrije ruimte: $_vrij" fi # Wat de mappen op de host zijn is één ding; waar Server Up zélf kijkt is # een ander. Die twee kunnen uit elkaar lopen — een LIBRARY_DIR uit een oude # config wint van de standaard — en dan zie je een lege stacklijst zonder # dat ergens staat waarom. stap "Wat Server Up zelf ziet" # Niet zelf redeneren over wat de app ziet, maar het hem vragen. Het pad # letterlijk (repr, dus mét eventuele spatie of regeleinde erin), of het # bestaat, en per map of hij als app meetelt. Dat laatste is de vraag die # ertoe doet: een map die er staat maar niet meetelt, verdwijnt uit de # lijst én laat zijn containers onder "handmatig gestart" belanden. _eigen="$($SUDO docker exec -i server-up python - 2>/dev/null <<'PYEOF' || true import json, sys sys.path.insert(0, "/app") try: import core from pathlib import Path from core import docker c = core.load() ruw = str(c.get("LIBRARY_DIR", "")) pad = Path(ruw) uit = {k: c.get(k, "") for k in ("BASE_DIR", "LIBRARY_DIR", "DATA_DIR", "BACKUP_DIR")} uit["lib_repr"] = repr(ruw) uit["lib_bestaat"] = pad.is_dir() uit["mappen"] = [] if pad.is_dir(): for d in sorted(pad.iterdir()): if d.is_dir() and not d.name.startswith("."): uit["mappen"].append([d.name, bool(docker.has_compose(d))]) print(json.dumps(uit)) except Exception as e: print(json.dumps({"fout": str(e)[:200]})) PYEOF )" if [ -z "$_eigen" ]; then dok_waar "Kon Server Up niet vragen welke paden hij gebruikt" else _lib="$(printf '%s' "$_eigen" | sed -n 's/.*"LIBRARY_DIR": *"\([^"]*\)".*/\1/p')" _dat="$(printf '%s' "$_eigen" | sed -n 's/.*"DATA_DIR": *"\([^"]*\)".*/\1/p')" _repr="$(printf '%s' "$_eigen" | sed -n 's/.*"lib_repr": *"\([^"]*\)".*/\1/p')" log "Stacks: $_lib" log "Appdata: $_dat" # Letterlijk, dus met een spatie of regeleinde er zichtbaar in. Zo'n # onzichtbaar teken maakt het pad onvindbaar terwijl het er goed uitziet. [ -n "$_repr" ] && log "Letterlijk: $_repr" case "$_eigen" in *'"lib_bestaat": false'*) dok_fout "De app vindt die map niet, ook al staat hij er" log " Kijk naar 'Letterlijk' hierboven: staat er een spatie of" log " een regeleinde in het pad? Pas het aan bij Instellingen → Paden." ;; esac # Per map: telt hij mee als app? Een map zonder compose-bestand telt niet # en verdwijnt daarmee uit de lijst. printf '%s' "$_eigen" \ | sed -n 's/.*"mappen": \[\(.*\)\].*/\1/p' \ | tr ']' '\n' \ | while IFS= read -r _rij; do _naam="$(printf '%s' "$_rij" | sed -n 's/.*\["\([^"]*\)".*/\1/p')" [ -n "$_naam" ] || continue case "$_rij" in *true*) log " $_naam — telt mee" ;; *) log " $_naam — GEEN compose-bestand, telt niet mee" ;; esac done if [ -n "$_lib" ]; then # Bestaat de map daarbinnen überhaupt, en zo ja: staat er hetzelfde # in als erbuiten? Dat laatste is de vraag die ertoe doet. Een bind # mount wijst naar een inode, niet naar een pad: is de map op de # host ná het aanmaken van de container vervangen, dan blijft de # container naar de oude — meestal lege — map kijken. Het pad staat # dan nog gewoon in /proc/self/mountinfo, dus "gekoppeld" zegt niets. if ! $SUDO docker exec server-up test -d "$_lib" 2>/dev/null; then dok_fout "De map $_lib bestaat niet binnen de container" log " Hercreëer de container: $SUDO docker compose -f $DOEL/docker-compose.yml up -d --force-recreate" else _n="$($SUDO docker exec server-up sh -c \ "ls -1A '$_lib' 2>/dev/null | wc -l" 2>/dev/null || echo 0)" _n="$(printf '%s' "$_n" | tr -d '[:space:]')" _h="$(ls -1A "$_lib" 2>/dev/null | wc -l)" _h="$(printf '%s' "$_h" | tr -d '[:space:]')" if [ "${_n:-0}" -gt 0 ] 2>/dev/null; then dok_ok "$_n app(s) zichtbaar voor Server Up" elif [ "${_h:-0}" -gt 0 ] 2>/dev/null; then dok_fout "Buiten de container staan er $_h in $_lib, binnen de container geen" log " De koppeling wijst naar een oudere map dan die er nu staat." log " Dat gebeurt als $_basis op de host is vervangen nadat de" log " container was aangemaakt. Hercreëren lost het op:" log " $SUDO docker compose -f $DOEL/docker-compose.yml up -d --force-recreate" else dok_waar "Er staat nog geen enkele app in $_lib" fi fi fi # Eigen koppelingen ónder BASE_DIR. Docker bindt met propagation # "rprivate": een koppeling die op de host ná het aanmaken van de # container ontstaat, komt daar niet doorheen. De container ziet dan de # lege map eronder, terwijl op de host alles keurig gevuld is — en de # koppeling van BASE_DIR zelf is intact, dus daar is niets aan te zien. if heeft findmnt; then _diep=0 for _mp in $(findmnt -rno TARGET -R "$_basis" 2>/dev/null); do [ "$_mp" = "$_basis" ] && continue _diep=1 if $SUDO docker exec server-up sh -c \ "grep -q ' $_mp ' /proc/self/mountinfo" 2>/dev/null; then dok_ok "$_mp is een eigen koppeling en komt mee" else dok_fout "$_mp is op de host een eigen koppeling, maar niet in de container" log " De container ziet daar de lege map eronder. Koppel hem" log " apart mee in docker-compose.yml:" log " - $_mp:$_mp" log " of zorg dat de koppeling bestaat vóór de container start." fi done [ "$_diep" -eq 0 ] && log "Geen eigen koppelingen onder $_basis" else dok_waar "findmnt ontbreekt; kon niet nagaan of er koppelingen onder $_basis liggen" fi # De app store leest uit de git-cache; is die leeg, dan is de lijst leeg. _apps="$($SUDO docker exec server-up sh -c \ 'find /data/git -name template.json 2>/dev/null | wc -l' \ 2>/dev/null || echo 0)" _apps="$(printf '%s' "$_apps" | tr -d '[:space:]')" if [ "${_apps:-0}" -gt 0 ] 2>/dev/null; then dok_ok "$_apps app(s) in de store-cache" else dok_fout "De store-cache is leeg; synchroniseer de repo's opnieuw" fi fi stap "Bijwerken vanuit de interface" _werkmap="$(_dok_inspect '{{index .Config.Labels "com.docker.compose.project.working_dir"}}')" if [ -z "$_werkmap" ]; then dok_waar "Geen compose-labels; bijwerken vanuit de interface kan niet" else log "Werkmap volgens de container: $_werkmap" case "$_img" in */*) dok_ok "Registry-image, dus de knop in de interface werkt" ;; *) log "Zelfgebouwd image; bijwerken loopt via $DOEL/install.sh --update" ;; esac fi printf '\n%s────────────────────────────────────────────────────────%s\n' "$C_KOP" "$C_UIT" if [ "$DOK_FOUTEN" -gt 0 ]; then printf '%s %s fout(en), %s waarschuwing(en)%s\n' "$C_FOUT" "$DOK_FOUTEN" "$DOK_WAARSCHUWINGEN" "$C_UIT" elif [ "$DOK_WAARSCHUWINGEN" -gt 0 ]; then printf '%s Geen fouten, %s waarschuwing(en)%s\n' "$C_WAARSCHUW" "$DOK_WAARSCHUWINGEN" "$C_UIT" else printf '%s Alles in orde%s\n' "$C_OK" "$C_UIT" fi printf '%s────────────────────────────────────────────────────────%s\n\n' "$C_KOP" "$C_UIT" [ "$DOK_FOUTEN" -eq 0 ] } 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" _basis="$(env_waarde BASE_DIR)"; _basis="${_basis:-/opt/serverup}" _naam="$(getent passwd "$(env_waarde SU_UID)" 2>/dev/null | cut -d: -f1)" if [ "$PURGE" -eq 0 ]; then printf '\n' log "Je gegevens staan nog op deze plekken:" log " docker-volume 'su-data' — instellingen, accounts, auditlog" log " $_basis — je stacks, appdata en backups" log " $DOEL — de installatie zelf" [ -n "$_naam" ] && [ "$_naam" != "root" ] \ && log " het account '$_naam', dat bij de installatie is aangemaakt" printf '\n' log "Alles definitief weg? Draai dan: sh $DOEL/install.sh --uninstall --purge" printf '\n' return 0 fi # ── Opruimen ───────────────────────────────────────────────────────────── # Per onderdeel apart vragen. Dit gooit gegevens weg die nergens anders meer # staan, dus "ja op alles" is hier geen dienst. stap "Opruimen" # Let op de vorm: `doe … >/dev/null` zou ook de [droog]-regel wegslikken, # waardoor een proefdraai niet meer laat zien wat hij zou doen. if vraag "Het docker-volume 'su-data' verwijderen (instellingen, accounts, auditlog)?"; then if doe $SUDO docker volume rm su-data; then goed "Volume su-data verwijderd" else waarschuw "Volume su-data niet gevonden of nog in gebruik" fi fi if [ -d "$_basis" ]; then _omvang="$(du -sh "$_basis" 2>/dev/null | cut -f1)" waarschuw "$_basis bevat je stacks, appdata en backups (${_omvang:-?})." waarschuw "Dit is de map met de gegevens van al je apps." if vraag "Deze map écht verwijderen?"; then doe $SUDO rm -rf "$_basis" && goed "$_basis verwijderd" else log "$_basis blijft staan." fi fi if [ -n "$_naam" ] && [ "$_naam" != "root" ]; then if vraag "Het account '$_naam' verwijderen?"; then if doe $SUDO userdel "$_naam"; then goed "Account $_naam verwijderd" else waarschuw "Kon $_naam niet verwijderen; misschien bezit het nog bestanden." fi fi fi # De installatiemap als laatste: daar staat dit script zelf in. if vraag "De installatiemap $DOEL verwijderen?"; then doe $SUDO rm -rf "$DOEL" && goed "$DOEL verwijderd" fi 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 ;; reconfigure) # Instellingen wijzigen zonder de broncode aan te raken. Handig als je # de server op je netwerk wilt zetten, een andere poort wilt, of alsnog # van root af wilt — dingen waarvoor je anders --update draaide en dus # ongewild ook nieuwe code binnenhaalde. [ -f "$DOEL/docker-compose.yml" ] || fout "Geen installatie gevonden in $DOEL." bepaal_rechten controleer_docker lees_env_terug "$DOEL/.env" printf '\n Nu ingesteld: BIND=%s PORT=%s BASE_DIR=%s, draaiend als %s\n' \ "$BIND" "$POORT" "${BASE_DIR:-/opt/serverup}" \ "$(getent passwd "$(env_waarde SU_UID)" 2>/dev/null | cut -d: -f1 || echo root)" kies_bind # Zonder --user opnieuw vragen: dat is meestal juist waarvoor je hier bent. [ "$GEBRUIKER_EXPLICIET" -eq 1 ] || SU_UID="" kies_gebruiker schrijf_env zet_mappen_klaar start if wacht_op_gereed; then printf '\n' goed "Instellingen bijgewerkt." afsluiting else fout "De container kwam niet op met de nieuwe instellingen. Bekijk de logs." fi ;; doctor) # Alleen kijken, nooit wijzigen. Daarom geen bepaal_rechten: die kan # afbreken omdat je niet kunt schrijven, terwijl lezen prima gaat. if [ "$(id -u)" -ne 0 ] && ! docker info >/dev/null 2>&1 && heeft sudo; then SUDO="sudo" fi doorlichten ;; 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 # Niet wegkijken als hij niet opkomt: "Bijgewerkt." bij een kapotte # update zag er precies zo uit als een geslaagde, en een script dat dit # aanroept zag exitcode 0. if wacht_op_gereed; then printf '\n' goed "Bijgewerkt." else printf '\n' waarschuw "De nieuwe versie kwam niet omhoog." if terugrollen; then printf '\n' fout "Bijwerken mislukt; je draait weer op de vorige versie. Bekijk de logs voordat je het opnieuw probeert." fi log "Bekijk de logs met:" log " $SUDO docker compose -f $DOEL/docker-compose.yml logs --tail=50" fout "Bijwerken mislukt en terugrollen lukte niet." fi ;; 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 terminal_beschikbaar && [ "$JA" -eq 0 ]; then printf '\n Gebruikersnaam: ' read -r naam