- install.sh: ensure_example legt .env.example an falls fehlend; env_hint weist klar auf die anpassbare .env hin; wait_for_backend pollt /api/health und meldet 502-Ursachen mit Log-Hinweis. - web/api.js: HTML-Fehlerseiten (z.B. nginx 502) nicht mehr roh anzeigen, stattdessen verständliche Meldung. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
245 lines
7.4 KiB
Bash
245 lines
7.4 KiB
Bash
#!/usr/bin/env bash
|
||
#
|
||
# Project-Good – Installer für selfhostbare Lebensmittel-Lagerverwaltung.
|
||
# Installiert bei Bedarf Docker und startet den kompletten Stack via Docker Compose.
|
||
#
|
||
# Nutzung:
|
||
# ./install.sh interaktiv (fragt Admin-Passwort etc.)
|
||
# ./install.sh --yes nicht-interaktiv (erzeugt Zufalls-Admin-Passwort)
|
||
#
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
ENV_FILE="$SCRIPT_DIR/.env"
|
||
EXAMPLE_FILE="$SCRIPT_DIR/.env.example"
|
||
ASSUME_YES=0
|
||
[[ "${1:-}" == "--yes" || "${1:-}" == "-y" ]] && ASSUME_YES=1
|
||
|
||
info() { printf '\033[0;32m==>\033[0m %s\n' "$*"; }
|
||
warn() { printf '\033[0;33m[!]\033[0m %s\n' "$*"; }
|
||
error() { printf '\033[0;31m[x]\033[0m %s\n' "$*" >&2; }
|
||
|
||
rand() { LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c "${1:-32}"; }
|
||
|
||
# root/sudo bestimmen
|
||
if [[ "$(id -u)" -eq 0 ]]; then
|
||
SUDO=""
|
||
elif command -v sudo >/dev/null 2>&1; then
|
||
SUDO="sudo"
|
||
else
|
||
SUDO=""
|
||
fi
|
||
|
||
# --- Paketinstallation (paketmanager-unabhängig) ------------------------------
|
||
pkg_install() {
|
||
local pkg="$1"
|
||
if command -v apt-get >/dev/null 2>&1; then
|
||
$SUDO apt-get update -y && $SUDO apt-get install -y "$pkg"
|
||
elif command -v dnf >/dev/null 2>&1; then
|
||
$SUDO dnf install -y "$pkg"
|
||
elif command -v yum >/dev/null 2>&1; then
|
||
$SUDO yum install -y "$pkg"
|
||
elif command -v apk >/dev/null 2>&1; then
|
||
$SUDO apk add --no-cache "$pkg"
|
||
elif command -v pacman >/dev/null 2>&1; then
|
||
$SUDO pacman -Sy --noconfirm "$pkg"
|
||
elif command -v zypper >/dev/null 2>&1; then
|
||
$SUDO zypper install -y "$pkg"
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# --- curl sicherstellen (wird für die Docker-Installation gebraucht) ----------
|
||
ensure_curl() {
|
||
if command -v curl >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
warn "curl ist nicht installiert – wird jetzt automatisch installiert …"
|
||
if ! pkg_install curl; then
|
||
error "curl konnte nicht installiert werden (kein bekannter Paketmanager gefunden). Bitte curl manuell installieren und erneut ausführen."
|
||
exit 1
|
||
fi
|
||
command -v curl >/dev/null 2>&1 || { error "curl-Installation fehlgeschlagen."; exit 1; }
|
||
info "curl installiert."
|
||
}
|
||
|
||
# --- Docker prüfen / installieren ---------------------------------------------
|
||
ensure_docker() {
|
||
if command -v docker >/dev/null 2>&1; then
|
||
info "Docker gefunden: $(docker --version)"
|
||
else
|
||
warn "Docker ist nicht installiert."
|
||
if [[ "$ASSUME_YES" -eq 1 ]]; then
|
||
reply="j"
|
||
else
|
||
read -r -p "Soll Docker jetzt automatisch installiert werden? [J/n] " reply
|
||
reply="${reply:-j}"
|
||
fi
|
||
case "$reply" in
|
||
[JjYy]*)
|
||
ensure_curl
|
||
info "Installiere Docker über get.docker.com …"
|
||
curl -fsSL https://get.docker.com | sh
|
||
;;
|
||
*)
|
||
error "Docker wird benötigt. Bitte manuell installieren und erneut ausführen."
|
||
exit 1
|
||
;;
|
||
esac
|
||
fi
|
||
|
||
# docker compose (v2) oder docker-compose (v1) ermitteln
|
||
if docker compose version >/dev/null 2>&1; then
|
||
COMPOSE="docker compose"
|
||
elif command -v docker-compose >/dev/null 2>&1; then
|
||
COMPOSE="docker-compose"
|
||
else
|
||
error "Docker Compose wurde nicht gefunden. Bitte Docker Compose v2 installieren."
|
||
exit 1
|
||
fi
|
||
info "Verwende: $COMPOSE"
|
||
|
||
if ! docker info >/dev/null 2>&1; then
|
||
error "Docker-Daemon nicht erreichbar. Läuft Docker? Ggf. 'sudo' nötig oder Nutzer zur 'docker'-Gruppe hinzufügen."
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# --- Beispiel-.env sicherstellen ----------------------------------------------
|
||
ensure_example() {
|
||
if [[ -f "$EXAMPLE_FILE" ]]; then
|
||
return
|
||
fi
|
||
info "Lege Beispiel-Konfiguration an: $EXAMPLE_FILE"
|
||
cat > "$EXAMPLE_FILE" <<'EOF'
|
||
# Kopiere diese Datei zu ".env" und passe die Werte an.
|
||
# install.sh erzeugt eine .env automatisch mit sicheren Zufallswerten.
|
||
|
||
# Port, unter dem die Web-UI erreichbar ist
|
||
WEB_PORT=8080
|
||
|
||
# Datenbank
|
||
POSTGRES_USER=project_good
|
||
POSTGRES_PASSWORD=project_good
|
||
POSTGRES_DB=project_good
|
||
|
||
# Sicherheit – im Betrieb unbedingt ändern!
|
||
JWT_SECRET=change-me-please
|
||
|
||
# Erster Admin-Benutzer (wird nur beim allerersten Start angelegt)
|
||
ADMIN_USERNAME=admin
|
||
ADMIN_PASSWORD=changeme
|
||
|
||
# CORS (bei Betrieb hinter dem mitgelieferten nginx nicht nötig)
|
||
CORS_ORIGINS=*
|
||
EOF
|
||
}
|
||
|
||
# --- Hinweis auf die Konfigurationsdatei --------------------------------------
|
||
env_hint() {
|
||
echo
|
||
info "Konfiguration liegt in: $ENV_FILE"
|
||
echo " Vorlage/Beispiel: $EXAMPLE_FILE"
|
||
warn "Dort kannst du bei Bedarf Werte anpassen: WEB_PORT (Port der Web-UI),"
|
||
warn "ADMIN_USERNAME/ADMIN_PASSWORD sowie die Datenbank- und JWT-Secrets."
|
||
warn "Nach Änderungen an der .env neu starten: $COMPOSE --env-file .env up -d"
|
||
}
|
||
|
||
# --- .env erzeugen ------------------------------------------------------------
|
||
create_env() {
|
||
ensure_example
|
||
|
||
if [[ -f "$ENV_FILE" ]]; then
|
||
info ".env existiert bereits – bestehende Konfiguration wird verwendet."
|
||
env_hint
|
||
return
|
||
fi
|
||
|
||
info "Erzeuge Konfiguration (.env) aus den Beispiel-Vorgaben …"
|
||
|
||
local web_port="8080"
|
||
local admin_user="admin"
|
||
local admin_pass
|
||
admin_pass="$(rand 16)"
|
||
|
||
if [[ "$ASSUME_YES" -eq 0 ]]; then
|
||
read -r -p "Port für die Web-UI [8080]: " input; web_port="${input:-8080}"
|
||
read -r -p "Admin-Benutzername [admin]: " input; admin_user="${input:-admin}"
|
||
read -r -s -p "Admin-Passwort [leer = zufällig generieren]: " input; echo
|
||
[[ -n "$input" ]] && admin_pass="$input"
|
||
fi
|
||
|
||
cat > "$ENV_FILE" <<EOF
|
||
WEB_PORT=$web_port
|
||
POSTGRES_USER=project_good
|
||
POSTGRES_PASSWORD=$(rand 24)
|
||
POSTGRES_DB=project_good
|
||
JWT_SECRET=$(rand 48)
|
||
ADMIN_USERNAME=$admin_user
|
||
ADMIN_PASSWORD=$admin_pass
|
||
CORS_ORIGINS=*
|
||
EOF
|
||
chmod 600 "$ENV_FILE"
|
||
|
||
GENERATED_ADMIN_USER="$admin_user"
|
||
GENERATED_ADMIN_PASS="$admin_pass"
|
||
GENERATED_WEB_PORT="$web_port"
|
||
|
||
env_hint
|
||
}
|
||
|
||
# --- Warten bis das Backend über die Web-UI erreichbar ist --------------------
|
||
wait_for_backend() {
|
||
local port="$1" tries=30 i
|
||
ensure_curl
|
||
info "Warte darauf, dass das Backend bereit ist …"
|
||
for ((i = 1; i <= tries; i++)); do
|
||
if curl -fsS "http://localhost:${port}/api/health" >/dev/null 2>&1; then
|
||
info "Backend ist erreichbar. ✅"
|
||
return 0
|
||
fi
|
||
sleep 2
|
||
done
|
||
warn "Backend antwortet nach ~$((tries * 2))s noch nicht – das erklärt einen 502 in der Web-UI."
|
||
warn "Bitte die Logs prüfen: ( cd \"$SCRIPT_DIR\" && $COMPOSE logs backend )"
|
||
return 1
|
||
}
|
||
|
||
# --- Start --------------------------------------------------------------------
|
||
main() {
|
||
info "Project-Good-Installer startet …"
|
||
ensure_docker
|
||
create_env
|
||
|
||
info "Baue Container (das kann beim ersten Mal einige Minuten dauern) …"
|
||
( cd "$SCRIPT_DIR" && $COMPOSE --env-file "$ENV_FILE" build )
|
||
|
||
info "Starte Dienste …"
|
||
( cd "$SCRIPT_DIR" && $COMPOSE --env-file "$ENV_FILE" up -d )
|
||
|
||
# Werte aus .env lesen (auch bei bestehender .env)
|
||
# shellcheck disable=SC1090
|
||
local web_port admin_user
|
||
web_port="$(grep -E '^WEB_PORT=' "$ENV_FILE" | cut -d= -f2-)"
|
||
admin_user="$(grep -E '^ADMIN_USERNAME=' "$ENV_FILE" | cut -d= -f2-)"
|
||
|
||
wait_for_backend "$web_port" || true
|
||
|
||
echo
|
||
info "Fertig! 🎉"
|
||
echo " Web-UI: http://localhost:${web_port} (bzw. http://<server-ip>:${web_port})"
|
||
echo " Admin-Login: ${admin_user}"
|
||
if [[ -n "${GENERATED_ADMIN_PASS:-}" ]]; then
|
||
echo " Admin-Passwort: ${GENERATED_ADMIN_PASS}"
|
||
warn "Bitte dieses Passwort notieren – es wird nicht erneut angezeigt."
|
||
else
|
||
echo " Admin-Passwort: (wie in .env gesetzt)"
|
||
fi
|
||
echo
|
||
echo " Logs: ( cd $SCRIPT_DIR && $COMPOSE logs -f )"
|
||
echo " Stoppen: ( cd $SCRIPT_DIR && $COMPOSE down )"
|
||
}
|
||
|
||
main "$@"
|