Files
certain/deploy/install.sh
T
GuidoandClaude Opus 4.8 8ed4bf5254 feat: Phase 2 – Budget-Engine + Direktzugriff (http://IP:Port)
Budget:
- Modelle + Migration 0003: budget_template_lines, planned_income_lines,
  budget_periods, budget_period_lines.
- services/budget: Perioden-Grenzen (einstellbarer Starttag, Label nach Endmonat),
  Standard-Vorlage vs. Monats-Override (Snapshot-on-Customize), Soll/Ist inkl.
  budgetierter Sparen-Transfers, geplante Einnahmen (Soll/Ist).
- routers/budgets: Vorlage, geplante Einnahmen (CRUD), Monats-Soll/Ist,
  Monats-Override, Zurücksetzen. pytest: Perioden, Soll/Ist, Override/Reset, Einnahmen.
- Frontend: Budget-Seite (Monat + Standard-Budget), Fortschrittsbalken grün/rot,
  Monats-Navigation, editierbare Soll-Beträge; Sparen-Option im Transfer-Formular.

Deployment-Fix (Nutzer-Anforderung: http://IP:Port muss funktionieren):
- Dienst bindet an APP_BIND_HOST=0.0.0.0 (direkt im LAN erreichbar).
- APP_COOKIE_SECURE=false als Standard (sonst kein Login über reines HTTP);
  für reinen HTTPS-Betrieb umstellbar. Doku + install.sh entsprechend angepasst.

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

197 lines
6.9 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
# Direktzugriff (http://IP:Port) für ältere Konfigurationen nachrüsten.
grep -q '^APP_BIND_HOST=' "$ENV_FILE" || echo "APP_BIND_HOST=0.0.0.0" >> "$ENV_FILE"
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_BIND_HOST=0.0.0.0
APP_PORT=$local_port
APP_SESSION_SECRET=$(rand 48)
APP_COOKIE_SECURE=false
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 \${APP_BIND_HOST} --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
LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
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 " Web-UI (direkt): http://${LAN_IP:-<LXC-IP>}:$PORT"
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 " Optional über HTTPS (Nginx Proxy Manager): neuen Proxy-Host ->"
echo " Forward Hostname/IP: ${LAN_IP:-<LXC-IP>}"
echo " Forward Port: $PORT (Scheme: http)"
echo " SSL: Let's Encrypt aktivieren. Dann in $ENV_FILE APP_COOKIE_SECURE=true"
echo " setzen und 'systemctl restart $APP_SLUG'. (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"