#!/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 # ── 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/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:-}" "$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 > '$1'" } 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)" 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 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\n' "$BIND" "$POORT" 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'" 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/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" 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 # Ook bij bijwerken: expliciete --bind/--port doorvoeren, en anders # overnemen wat er al staat. schrijf_env 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