roosterwijs/scripts/install.sh
Ramon f795e85333
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
Installatiescript met opslagkeuze, standaard /srv/server-up (v0.2.04 beta)
Nieuw: scripts/install.sh — installeert de stack in zeven zichtbare stappen
met drie vragen, standaard in /srv/server-up (aan te passen met
ROOSTERWIJS_DIR). Het script:
- controleert git, docker, de compose-plugin en of de daemon bereikbaar is;
- maakt de doelmap aan, of legt uit welk commando daarvoor nodig is als de
  rechten ontbreken (roept zelf geen sudo aan);
- kloont of werkt de checkout bij;
- laat kiezen waar de gegevens komen (zie hieronder);
- genereert .env met een willekeurige DJANGO_SECRET_KEY en een sterk
  databasewachtwoord, en zet hostnaam, CSRF-origin en DJANGO_SECURE op basis
  van twee vragen; een bestaande .env wordt nooit zomaar overschreven;
- bouwt en start de containers;
- meldt of er een account is en biedt aan er een aan te maken.

Opslagkeuze bij de installatie:
- docker-compose.yml gebruikt nu ${DATA_DB}, ${DATA_MEDIA} en ${DATA_STATIC}.
  Een naam = Docker-volume, een pad = gewone map naast de code.
- De standaardwaarden zijn de bestaande named volumes, dus installaties die
  al draaien veranderen niet en er gaan geen gegevens verloren.
- Kies je voor mappen, dan komt alles onder /srv/server-up/data/.

Verder:
- .gitignore: /data/ uitgesloten. Zonder dat zou de gegevensmap als lokale
  wijziging gelden en zou deploy.sh weigeren te draaien.
- deploy.sh maakt de gegevensmappen aan als de opslagkeuze paden gebruikt,
  zodat Docker ze niet als root aanmaakt.
- DEPLOY.md en README: installatie via het script, de opslagkeuze in een
  tabel, en de dump/restore-stappen die nodig zijn bij het omzetten van
  volumes naar mappen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAQ4Np13v8fwbTtKHKnwDz
2026-08-19 15:17:08 +02:00

252 lines
10 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Roosterwijs installeren op een server.
#
# Gebruik: ./scripts/install.sh
# ROOSTERWIJS_DIR=/pad/naar/map ./scripts/install.sh
#
# Het script doorloopt zeven stappen, stelt onderweg drie vragen en raakt
# nooit iets aan zonder dat te melden. Afbreken kan altijd met Ctrl-C.
set -euo pipefail
DOELMAP="${ROOSTERWIJS_DIR:-/srv/server-up}"
# ---------------------------------------------------------------- vormgeving
# Kleuren alleen als we naar een terminal schrijven (en NO_COLOR niet gezet is),
# zodat de uitvoer in een logbestand leesbaar blijft.
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
VET=$'\033[1m'; DOF=$'\033[2m'; STOP=$'\033[0m'
GROEN=$'\033[32m'; GEEL=$'\033[33m'; ROOD=$'\033[31m'; BLAUW=$'\033[36m'
else
VET=''; DOF=''; STOP=''; GROEN=''; GEEL=''; ROOD=''; BLAUW=''
fi
LIJN=$(printf '─%.0s' $(seq 1 62))
STAP_NR=0
STAP_TOTAAL=7
kop() {
printf '\n%s%s%s\n' "$BLAUW" "$LIJN" "$STOP"
printf ' %sROOSTERWIJS%s %s· installatie%s\n' "$VET" "$STOP" "$DOF" "$STOP"
printf '%s%s%s\n' "$BLAUW" "$LIJN" "$STOP"
}
stap() { STAP_NR=$((STAP_NR + 1)); printf '\n %s[%d/%d]%s %s%s%s\n' "$BLAUW" "$STAP_NR" "$STAP_TOTAAL" "$STOP" "$VET" "$1" "$STOP"; }
ok() { printf ' %s✓%s %s\n' "$GROEN" "$STOP" "$1"; }
info() { printf ' %s·%s %s\n' "$DOF" "$STOP" "$1"; }
let_op(){ printf ' %s!%s %s\n' "$GEEL" "$STOP" "$1"; }
fout() { printf '\n %s✗ %s%s\n\n' "$ROOD" "$1" "$STOP" >&2; exit 1; }
# vraag "Tekst" "standaardwaarde" -> antwoord in $ANTWOORD
vraag() {
local tekst="$1" standaard="${2:-}" invoer
if [ -n "$standaard" ]; then
printf ' %s%s%s %s[%s]%s: ' "$VET" "$tekst" "$STOP" "$DOF" "$standaard" "$STOP"
else
printf ' %s%s%s: ' "$VET" "$tekst" "$STOP"
fi
read -r invoer </dev/tty 2>/dev/null || invoer=""
ANTWOORD="${invoer:-$standaard}"
}
# jaNee "Vraag" "j|n" -> 0 = ja, 1 = nee
jaNee() {
local tekst="$1" standaard="${2:-j}" invoer
local hint="j/N"; [ "$standaard" = "j" ] && hint="J/n"
printf ' %s%s%s %s(%s)%s: ' "$VET" "$tekst" "$STOP" "$DOF" "$hint" "$STOP"
read -r invoer </dev/tty 2>/dev/null || invoer=""
invoer="${invoer:-$standaard}"
case "$invoer" in [jJyY]*) return 0 ;; *) return 1 ;; esac
}
# Wachtwoord/sleutel genereren zonder Python op de server te vereisen.
genereer_geheim() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | cut -c1-50
else
LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50
fi
}
kop
# ------------------------------------------------------------- 1. vereisten
stap "Vereisten controleren"
command -v git >/dev/null 2>&1 || fout "git ontbreekt. Installeer git en probeer opnieuw."
ok "git $(git --version | awk '{print $3}')"
command -v docker >/dev/null 2>&1 || fout "docker ontbreekt. Zie https://docs.docker.com/engine/install/"
ok "docker $(docker --version | awk '{print $3}' | tr -d ,)"
docker compose version >/dev/null 2>&1 || fout "'docker compose' werkt niet. Installeer de compose-plugin."
ok "docker compose $(docker compose version --short 2>/dev/null || echo aanwezig)"
docker info >/dev/null 2>&1 || fout "Kan niet met de Docker-daemon praten. Draait Docker, en zit je in de groep 'docker'?"
ok "Docker-daemon bereikbaar"
# --------------------------------------------------------------- 2. doelmap
stap "Doelmap $DOELMAP"
if [ ! -d "$DOELMAP" ]; then
if mkdir -p "$DOELMAP" 2>/dev/null; then
ok "aangemaakt"
else
printf '\n'
let_op "Geen rechten om $DOELMAP aan te maken."
info "Draai eenmalig, als beheerder:"
printf '\n %ssudo mkdir -p %s && sudo chown "$USER":"$USER" %s%s\n\n' "$VET" "$DOELMAP" "$DOELMAP" "$STOP"
fout "Start daarna dit script opnieuw."
fi
else
[ -w "$DOELMAP" ] || fout "$DOELMAP bestaat, maar je hebt er geen schrijfrechten."
ok "bestaat en is beschrijfbaar"
fi
# ------------------------------------------------------------------ 3. code
stap "Code ophalen"
if [ -d "$DOELMAP/.git" ]; then
ok "checkout gevonden"
if jaNee "Bijwerken naar de laatste versie?" j; then
git -C "$DOELMAP" fetch origin --quiet
tak="$(git -C "$DOELMAP" rev-parse --abbrev-ref HEAD)"
git -C "$DOELMAP" reset --hard "origin/$tak" --quiet
ok "bijgewerkt naar $(git -C "$DOELMAP" rev-parse --short HEAD) op $tak"
else
info "overgeslagen; huidige versie blijft staan"
fi
elif [ -n "$(ls -A "$DOELMAP" 2>/dev/null)" ]; then
fout "$DOELMAP is niet leeg en bevat geen git-checkout. Maak de map leeg of kies een andere met ROOSTERWIJS_DIR=..."
else
herkomst=""
if git -C "$(dirname "$0")/.." rev-parse --git-dir >/dev/null 2>&1; then
herkomst="$(git -C "$(dirname "$0")/.." remote get-url origin 2>/dev/null || true)"
fi
vraag "Git-URL van de repository" "$herkomst"
[ -n "$ANTWOORD" ] || fout "Zonder URL kan de code niet worden opgehaald."
git clone "$ANTWOORD" "$DOELMAP" --quiet || fout "Klonen mislukt. Klopt de URL en heb je toegang (SSH-sleutel)?"
ok "gekloond naar $DOELMAP"
fi
cd "$DOELMAP"
# --------------------------------------------------------------- 4. opslag
stap "Waar komen de gegevens te staan?"
printf '\n'
printf ' %s1)%s %sDocker-volumes%s %s(standaard)%s\n' "$VET" "$STOP" "$VET" "$STOP" "$DOF" "$STOP"
printf ' Docker beheert de opslag. Netjes afgeschermd, maar je\n'
printf ' komt er alleen bij via docker-commandos.\n\n'
printf ' %s2)%s %sGewone mappen%s onder %s/data\n' "$VET" "$STOP" "$VET" "$STOP" "$DOELMAP"
printf ' Database, media en statics staan als zichtbare mappen\n'
printf ' naast de code. Makkelijk back-uppen en inzien.\n\n'
vraag "Keuze (1 of 2)" "1"
case "$ANTWOORD" in
2)
OPSLAG="mappen"
DATA_DB="./data/postgres"; DATA_MEDIA="./data/media"; DATA_STATIC="./data/static"
mkdir -p data/postgres data/media data/static
ok "mappen aangemaakt onder $DOELMAP/data"
;;
*)
OPSLAG="volumes"
DATA_DB="pgdata"; DATA_MEDIA="media_volume"; DATA_STATIC="static_volume"
ok "Docker beheert de opslag (volumes pgdata, media_volume, static_volume)"
;;
esac
# ------------------------------------------------------------------ 5. .env
stap "Instellingen (.env)"
if [ -f .env ]; then
ok ".env bestaat al"
if jaNee "Laten staan en ongewijzigd gebruiken?" j; then
info "bestaande instellingen blijven gelden"
if ! grep -q '^DATA_DB=' .env; then
{ echo ""; echo "# Opslagkeuze (toegevoegd door install.sh)"
echo "DATA_DB=$DATA_DB"; echo "DATA_MEDIA=$DATA_MEDIA"; echo "DATA_STATIC=$DATA_STATIC"; } >> .env
ok "opslagkeuze toegevoegd aan .env"
fi
else
cp .env ".env.backup-$(date +%Y%m%d-%H%M%S)"
ok "oude .env bewaard als .env.backup-*"
rm -f .env
fi
fi
if [ ! -f .env ]; then
vraag "Hostnaam waarop de app bereikbaar is" "rooster.example.nl"
HOSTNAAM="$ANTWOORD"
if jaNee "Draait er TLS (https) voor deze server?" j; then
SCHEMA="https"; SECURE=1
else
SCHEMA="http"; SECURE=0
fi
BASIS_URL="$SCHEMA://$HOSTNAAM"
[ "$SCHEMA" = "http" ] && BASIS_URL="$BASIS_URL:8080"
cat > .env <<ENVEOF
# Aangemaakt door scripts/install.sh op $(date '+%Y-%m-%d %H:%M').
# Dit bestand bevat geheimen en staat in .gitignore.
# --- Django ---------------------------------------------------------------
DJANGO_SECRET_KEY=$(genereer_geheim)
DJANGO_DEBUG=0
DJANGO_ALLOWED_HOSTS=$HOSTNAAM,localhost,127.0.0.1
DJANGO_CSRF_TRUSTED_ORIGINS=$BASIS_URL
# 1 = veilige cookies, HSTS en https-redirect. Vereist dat de proxy
# X-Forwarded-Proto: https meestuurt (zie DEPLOY.md).
DJANGO_SECURE=$SECURE
# --- Opslag ---------------------------------------------------------------
# Een naam = Docker-volume, een pad = gewone map naast de code.
DATA_DB=$DATA_DB
DATA_MEDIA=$DATA_MEDIA
DATA_STATIC=$DATA_STATIC
# --- PostgreSQL -----------------------------------------------------------
POSTGRES_DB=rooster
POSTGRES_USER=rooster
POSTGRES_PASSWORD=$(genereer_geheim)
# --- E-mail (wachtwoordreset) --------------------------------------------
# Leeg = e-mail gaat naar de log van de backend-container.
EMAIL_HOST=
EMAIL_PORT=587
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
EMAIL_USE_TLS=1
DEFAULT_FROM_EMAIL=Roosterwijs <noreply@$HOSTNAAM>
FRONTEND_BASE_URL=$BASIS_URL
ENVEOF
chmod 600 .env
ok "aangemaakt met willekeurige sleutel en databasewachtwoord"
info "hostnaam: $HOSTNAAM · $( [ "$SECURE" = 1 ] && echo 'TLS aan' || echo 'TLS uit' )"
fi
# ------------------------------------------------------------ 6. bouwen
stap "Containers bouwen en starten"
info "dit duurt de eerste keer enkele minuten"
docker compose up -d --build || fout "Bouwen mislukt. Bekijk de uitvoer hierboven."
ok "db, backend en nginx draaien"
# --------------------------------------------------------- 7. beheerder
stap "Beheerder"
aantal="$(docker compose exec -T backend python manage.py shell -c \
'from django.contrib.auth import get_user_model; print(get_user_model().objects.count())' 2>/dev/null | tr -dc '0-9')"
aantal="${aantal:-0}"
if [ "$aantal" -gt 0 ]; then
ok "er zijn al $aantal account(s); geen nieuwe nodig"
else
let_op "Er is nog geen account. Zonder account kun je nergens inloggen."
if jaNee "Nu een beheerder aanmaken?" j; then
docker compose exec backend python manage.py createsuperuser </dev/tty 2>/dev/null || \
let_op "Aanmaken afgebroken; doe het later met het commando hieronder."
fi
fi
# ------------------------------------------------------------------ klaar
printf '\n%s%s%s\n' "$GROEN" "$LIJN" "$STOP"
printf ' %s✓ Installatie afgerond%s\n' "$GROEN$VET" "$STOP"
printf '%s%s%s\n\n' "$GROEN" "$LIJN" "$STOP"
printf ' %sMap%s %s\n' "$DOF" "$STOP" "$DOELMAP"
if [ "$OPSLAG" = "mappen" ]; then
printf ' %sGegevens%s %s/data (database, media, statics)\n' "$DOF" "$STOP" "$DOELMAP"
else
printf ' %sGegevens%s Docker-volumes (docker volume ls)\n' "$DOF" "$STOP"
fi
printf ' %sBijwerken%s ./scripts/deploy.sh\n' "$DOF" "$STOP"
printf ' %sLogs%s docker compose logs -f\n' "$DOF" "$STOP"
printf ' %sBeheerder%s docker compose exec backend python manage.py createsuperuser\n\n' "$DOF" "$STOP"