Files
certain/deploy/install.sh
T
GuidoandClaude Opus 4.8 2c795a57c8 feat: Phase 0 – Gerüst (FastAPI+SQLite, Multi-User-Login, React-App-Shell, Deploy)
Grundgerüst für Certain Finance:
- Backend: FastAPI + SQLAlchemy + SQLite, Alembic-Migrationen (Batch-Mode),
  argon2-Passwort-Hashing, server-seitige Sitzungen im Cookie, Erst-Admin,
  Benutzerverwaltung mit Temporär-Passwort, Einstellungen je Benutzer.
- Ein Prozess, ein Port: uvicorn liefert API (/api) und gebautes SPA aus,
  inkl. SPA-Deeplink-Fallback.
- Frontend: React 18 + Vite + JSX, Design-Tokens (Light/Dark), i18n DE/EN,
  Inline-SVG-Icons (keine Emojis), Login, Passwort-ändern, Dashboard-Platzhalter,
  Benutzer- und Einstellungsseiten.
- Deploy: nativer LXC-Installer + Update-Skript (systemd), env-Vorlage, Doku.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 18:27:42 +02:00

192 lines
6.6 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.
#!/usr/bin/env bash
#
# Certain Finance – nativer Installer für einen Debian/Ubuntu-LXC (Proxmox).
# Richtet Python-venv, gebautes Frontend, systemd-Dienst und Konfiguration ein.
# HTTPS macht dein vorhandener Nginx Proxy Manager – hier wird KEIN Reverse-Proxy
# installiert; der Dienst lauscht nur intern auf 127.0.0.1:<APP_PORT>.
#
# Nutzung (als root im geklonten Repo):
# ./deploy/install.sh interaktiv (fragt Port/Admin)
# ./deploy/install.sh --yes nicht-interaktiv (Zufalls-Admin-Passwort)
#
set -euo pipefail
APP_SLUG="certain-finance" # Einzige Stelle mit dem Betriebs-Slug (Dienst/Pfade)
DEFAULT_PORT=8071
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
ETC_DIR="/etc/$APP_SLUG"
ENV_FILE="$ETC_DIR/env"
DATA_DIR="/var/lib/$APP_SLUG"
SERVICE_USER="$APP_SLUG"
UNIT_FILE="/etc/systemd/system/$APP_SLUG.service"
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}"; }
if [[ "$(id -u)" -ne 0 ]]; then
error "Bitte als root ausführen (z. B. 'sudo ./deploy/install.sh')."
exit 1
fi
if ! command -v apt-get >/dev/null 2>&1; then
error "Dieses Skript ist für Debian/Ubuntu (apt) gedacht. Kein apt-get gefunden."
exit 1
fi
# --- Systempakete ------------------------------------------------------------
info "Installiere Systempakete (Python, Build-Tools, git, curl) …"
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
apt-get install -y python3 python3-venv python3-dev build-essential curl ca-certificates git
# --- Node.js (nur zum Bauen des Frontends nötig) -----------------------------
ensure_node() {
local major=0
if command -v node >/dev/null 2>&1; then
major="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)"
fi
if [[ "$major" -ge 18 ]]; then
info "Node.js gefunden: $(node --version)"
return
fi
warn "Node.js fehlt oder ist zu alt – installiere Node 20 LTS (NodeSource) …"
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
info "Node.js installiert: $(node --version)"
}
ensure_node
# --- Dienst-Benutzer und Verzeichnisse --------------------------------------
if ! id -u "$SERVICE_USER" >/dev/null 2>&1; then
info "Lege Dienst-Benutzer '$SERVICE_USER' an …"
useradd --system --user-group --home-dir "$DATA_DIR" --shell /usr/sbin/nologin "$SERVICE_USER"
fi
mkdir -p "$DATA_DIR" "$ETC_DIR"
chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR"
# --- Python-venv + Abhängigkeiten -------------------------------------------
info "Richte Python-venv ein und installiere Abhängigkeiten …"
python3 -m venv "$REPO_DIR/backend/.venv"
"$REPO_DIR/backend/.venv/bin/pip" install --upgrade pip >/dev/null
"$REPO_DIR/backend/.venv/bin/pip" install -r "$REPO_DIR/backend/requirements.txt"
# --- Frontend bauen ----------------------------------------------------------
info "Baue das Frontend (Node wird nur hier gebraucht) …"
(
cd "$REPO_DIR/web"
if [[ -f package-lock.json ]]; then npm ci; else npm install; fi
npm run build
)
# --- Konfiguration (.env) ----------------------------------------------------
GIT_SHA="$(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null || echo '')"
if [[ -f "$ENV_FILE" ]]; then
info "Konfiguration existiert bereits: $ENV_FILE (unverändert übernommen)."
# Git-Commit aktualisieren (für /api/version).
if grep -q '^APP_GIT_SHA=' "$ENV_FILE"; then
sed -i "s|^APP_GIT_SHA=.*|APP_GIT_SHA=$GIT_SHA|" "$ENV_FILE"
else
echo "APP_GIT_SHA=$GIT_SHA" >> "$ENV_FILE"
fi
ADMIN_USER="$(grep -E '^APP_ADMIN_USERNAME=' "$ENV_FILE" | cut -d= -f2-)"
GENERATED_PASS=""
else
local_port="$DEFAULT_PORT"
admin_user="admin"
admin_pass="$(rand 16)"
if [[ "$ASSUME_YES" -eq 0 ]]; then
read -r -p "Interner Port [$DEFAULT_PORT]: " x; local_port="${x:-$DEFAULT_PORT}"
read -r -p "Admin-Benutzername [admin]: " x; admin_user="${x:-admin}"
read -r -s -p "Admin-Passwort [leer = zufällig]: " x; echo
[[ -n "$x" ]] && admin_pass="$x"
fi
info "Erzeuge Konfiguration: $ENV_FILE"
cat > "$ENV_FILE" <<EOF
APP_DATA_DIR=$DATA_DIR
APP_PORT=$local_port
APP_SESSION_SECRET=$(rand 48)
APP_COOKIE_SECURE=true
APP_ADMIN_USERNAME=$admin_user
APP_ADMIN_PASSWORD=$admin_pass
APP_DEFAULT_BASE_CURRENCY=EUR
APP_ECB_FETCH_ON_START=true
APP_GIT_SHA=$GIT_SHA
EOF
ADMIN_USER="$admin_user"
GENERATED_PASS="$admin_pass"
fi
chown root:root "$ENV_FILE"
chmod 600 "$ENV_FILE"
# --- systemd-Unit ------------------------------------------------------------
info "Schreibe systemd-Unit: $UNIT_FILE"
cat > "$UNIT_FILE" <<EOF
[Unit]
Description=Certain Finance
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=$SERVICE_USER
Group=$SERVICE_USER
WorkingDirectory=$REPO_DIR/backend
EnvironmentFile=$ENV_FILE
ExecStart=$REPO_DIR/backend/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port \${APP_PORT}
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
ReadWritePaths=$DATA_DIR
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "$APP_SLUG" >/dev/null 2>&1 || true
info "Starte Dienst … (wendet Migrationen an und legt ggf. den ersten Admin an)"
systemctl restart "$APP_SLUG"
# --- Gesundheitscheck --------------------------------------------------------
PORT="$(grep -E '^APP_PORT=' "$ENV_FILE" | cut -d= -f2-)"
PORT="${PORT:-$DEFAULT_PORT}"
info "Warte auf das Backend (127.0.0.1:$PORT) …"
ok=0
for _ in $(seq 1 30); do
if curl -fsS "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; then ok=1; break; fi
sleep 2
done
echo
if [[ "$ok" -eq 1 ]]; then
info "Fertig – Certain Finance läuft. 🎉"
else
warn "Backend antwortet noch nicht. Logs: journalctl -u $APP_SLUG -e"
fi
echo " Interner Dienst: http://127.0.0.1:$PORT (nur lokal)"
echo " Admin-Login: ${ADMIN_USER:-admin}"
if [[ -n "${GENERATED_PASS:-}" ]]; then
echo " Admin-Passwort: ${GENERATED_PASS}"
warn "Bitte dieses Passwort notieren – es wird nicht erneut angezeigt."
fi
echo
echo " Nginx Proxy Manager: neuen Proxy-Host anlegen ->"
echo " Forward Hostname/IP: <IP-dieses-LXC>"
echo " Forward Port: $PORT"
echo " SSL: Let's Encrypt aktivieren; 'Block Common Exploits' ok."
echo " (Websockets nicht nötig. Für spätere Uploads client_max_body_size >= 20M.)"
echo
echo " Logs: journalctl -u $APP_SLUG -f"
echo " Neustart: systemctl restart $APP_SLUG"
echo " Update: ./deploy/update.sh"