#!/usr/bin/env bash # ============================================================================== # Proxmox LXC – Multi-OS Installer # Unterstützt: Debian / Ubuntu (immer aktuelle + vor/nachherige Version) # Aufruf auf dem PVE-Host: # bash <(curl -fsSL https://your-host/lxc-install.sh) # ============================================================================== set -euo pipefail # ── Farben ───────────────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m' info() { echo -e "${CYAN}[INFO]${NC} $*"; } success() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; } # ── Voraussetzungen ──────────────────────────────────────────────────────────── [[ $EUID -ne 0 ]] && error "Muss als root auf dem PVE-Host ausgeführt werden." command -v pct &>/dev/null || error "'pct' nicht gefunden – kein PVE-Host?" command -v pvesh &>/dev/null || error "'pvesh' nicht gefunden – kein PVE-Host?" # ══════════════════════════════════════════════════════════════════════════════ clear echo -e "${BOLD}" echo -e " ╔══════════════════════════════════════════════════════════╗" echo -e " ║ Proxmox LXC – Multi-OS Installer ║" echo -e " ║ Debian • Ubuntu | + Docker • Portainer ║" echo -e " ╚══════════════════════════════════════════════════════════╝" echo -e "${NC}" # ── Hilfsfunktionen ──────────────────────────────────────────────────────────── ask() { local prompt="$1" default="$2" var_name="$3" echo -ne "${BOLD}${prompt}${NC} [${YELLOW}${default}${NC}]: " read -r input printf -v "$var_name" '%s' "${input:-$default}" } ask_yn() { local prompt="$1" default="${2:-j}" echo -ne "${BOLD}${prompt}${NC} [${YELLOW}${default}${NC}]: " read -r yn [[ "${yn:-$default}" =~ ^[Jj1Yy] ]] } # ══════════════════════════════════════════════════════════════════════════════ # SCHRITT 1 – DISTRIBUTION WÄHLEN # ══════════════════════════════════════════════════════════════════════════════ echo -e "${CYAN}╔══ Schritt 1: Distribution ═══════════════════════════════════╗${NC}" echo -e "" echo -e " ${BOLD}[1]${NC} 🌀 ${BOLD}Debian${NC} – schlank, stabil, bewährt" echo -e " ${BOLD}[2]${NC} 🟠 ${BOLD}Ubuntu${NC} – breite Paketbasis, LTS-Support" echo -e "" while true; do echo -ne "${BOLD} Auswahl${NC} [${YELLOW}1${NC}]: " read -r dist_sel case "${dist_sel:-1}" in 1) DISTRO="debian"; DISTRO_LABEL="Debian"; DISTRO_MATCH="debian"; break ;; 2) DISTRO="ubuntu"; DISTRO_LABEL="Ubuntu"; DISTRO_MATCH="ubuntu"; break ;; *) warn "Bitte 1 oder 2 eingeben." ;; esac done echo "" # ══════════════════════════════════════════════════════════════════════════════ # SCHRITT 2 – VERSION WÄHLEN (live aus PVE-Datenbank, immer aktuell) # ══════════════════════════════════════════════════════════════════════════════ echo -e "${CYAN}╔══ Schritt 2: Version ════════════════════════════════════════╗${NC}" echo "" info "Lese verfügbare ${DISTRO_LABEL}-Templates aus der PVE-Datenbank …" # Template-Liste aktualisieren (leise, falls nötig) pveam update &>/dev/null || true # Alle passenden Templates holen, nach Versionsnummer sortieren mapfile -t ALL_TEMPLATES < <( pveam available --section system 2>/dev/null \ | awk '{print $2}' \ | grep -i "^${DISTRO_MATCH}-" \ | sort -V ) [[ ${#ALL_TEMPLATES[@]} -eq 0 ]] && error "Keine ${DISTRO_LABEL}-Templates gefunden. Bitte 'pveam update' ausführen." # ── Versionsnummern extrahieren und deduplizieren ────────────────────────────── # Debian-Muster: debian-12-standard_12.7-1_amd64.tar.zst → major=12 # Ubuntu-Muster: ubuntu-24.04-standard_24.04-1_amd64.tar.zst → major=24.04 declare -A VER_TO_TEMPLATE # major_version → neuestes Template declare -a VER_ORDER=() for t in "${ALL_TEMPLATES[@]}"; do if [[ "$DISTRO" == "debian" ]]; then ver=$(echo "$t" | grep -oP 'debian-\K[0-9]+' | head -1) else ver=$(echo "$t" | grep -oP 'ubuntu-\K[0-9]+\.[0-9]+' | head -1) fi [[ -z "$ver" ]] && continue # Letztes (neustes) Template pro Version merken (sort -V sorgt dafür) VER_TO_TEMPLATE["$ver"]="$t" # Version in geordneter Liste nur einmal speichern if [[ ! " ${VER_ORDER[*]} " =~ " ${ver} " ]]; then VER_ORDER+=("$ver") fi done [[ ${#VER_ORDER[@]} -eq 0 ]] && error "Konnte keine Versionsnummern aus den Templates extrahieren." # ── Immer genau 3 Versionen anzeigen: vorherige / aktuelle / nächste ─────────── # PVE kennt oft nur 2 Versionen → nächste Version wird synthetisch erzeugt. # Debian: Major-Integer hochzählen (12 → 13 → 14) # Ubuntu: Jahreszahl um 2 erhöhen (24.04 → 26.04 → 28.04) TOTAL=${#VER_ORDER[@]} CURRENT_IDX=$(( TOTAL - 1 )) # höchste bekannte = aktuell stabil PREV_IDX=$(( CURRENT_IDX - 1 )) # Nächste Version berechnen CURRENT_VER="${VER_ORDER[$CURRENT_IDX]}" if [[ "$DISTRO" == "debian" ]]; then NEXT_VER=$(( CURRENT_VER + 1 )) # z.B. 12 → 13 else # Ubuntu: 24.04 → 26.04 (Jahr +2, Minor bleibt .04) IFS='.' read -r uyr umn <<< "$CURRENT_VER" NEXT_VER="$(( uyr + 2 )).${umn}" fi # Prüfen ob PVE die nächste Version bereits kennt NEXT_VER_IN_PVE=false NEXT_TEMPLATE_PVE="" if [[ -n "${VER_TO_TEMPLATE[$NEXT_VER]+x}" ]]; then NEXT_VER_IN_PVE=true NEXT_TEMPLATE_PVE="${VER_TO_TEMPLATE[$NEXT_VER]}" fi # Vorherige Version: entweder aus PVE oder synthetisch if (( PREV_IDX >= 0 )); then PREV_VER="${VER_ORDER[$PREV_IDX]}" PREV_IN_PVE=true PREV_TEMPLATE="${VER_TO_TEMPLATE[$PREV_VER]}" else # Synthetisch rückwärts berechnen if [[ "$DISTRO" == "debian" ]]; then PREV_VER=$(( CURRENT_VER - 1 )) else IFS='.' read -r uyr umn <<< "$CURRENT_VER" PREV_VER="$(( uyr - 2 )).${umn}" fi PREV_IN_PVE=false PREV_TEMPLATE="" fi # ── Anzeige-Arrays aufbauen ──────────────────────────────────────────────────── declare -a SHOW_VERS=() declare -a SHOW_LABELS=() declare -a SHOW_AVAIL=() # true/false ob Template in PVE vorhanden # Slot 1: Vorherige Version SHOW_VERS+=("$PREV_VER") if $PREV_IN_PVE; then SHOW_LABELS+=("Vorherige Version (${DISTRO_LABEL} ${PREV_VER})") SHOW_AVAIL+=("true") else SHOW_LABELS+=("Vorherige Version (${DISTRO_LABEL} ${PREV_VER}) [kein Template in PVE]") SHOW_AVAIL+=("false") fi # Slot 2: Aktuelle Version (Standard) SHOW_VERS+=("$CURRENT_VER") SHOW_LABELS+=("Aktuelle Version (${DISTRO_LABEL} ${CURRENT_VER}) ← empfohlen") SHOW_AVAIL+=("true") DEFAULT_VER_IDX=2 # 1-basiert → Slot 2 = aktuell # Slot 3: Nächste Version SHOW_VERS+=("$NEXT_VER") if $NEXT_VER_IN_PVE; then SHOW_LABELS+=("Nächste Version (${DISTRO_LABEL} ${NEXT_VER}) [experimentell]") SHOW_AVAIL+=("true") else SHOW_LABELS+=("Nächste Version (${DISTRO_LABEL} ${NEXT_VER}) [noch kein Template in PVE]") SHOW_AVAIL+=("false") fi # ── Versionsauswahl ausgeben ─────────────────────────────────────────────────── echo -e " ${DISTRO_LABEL}-Versionen (aus PVE-Datenbank + Berechnung):\n" for i in "${!SHOW_VERS[@]}"; do num=$(( i + 1 )) avail="${SHOW_AVAIL[$i]}" if (( num == DEFAULT_VER_IDX )); then echo -e " ${BOLD}[${num}]${NC} ${GREEN}${SHOW_LABELS[$i]}${NC}" elif [[ "$avail" == "false" ]]; then echo -e " ${BOLD}[${num}]${NC} ${RED}${SHOW_LABELS[$i]}${NC} ← nicht wählbar" else echo -e " ${BOLD}[${num}]${NC} ${SHOW_LABELS[$i]}" fi done echo "" while true; do echo -ne "${BOLD} Auswahl${NC} [${YELLOW}${DEFAULT_VER_IDX}${NC}]: " read -r ver_sel ver_sel="${ver_sel:-$DEFAULT_VER_IDX}" if [[ "$ver_sel" =~ ^[0-9]+$ ]] && (( ver_sel >= 1 && ver_sel <= ${#SHOW_VERS[@]} )); then # Nicht wählbar wenn kein Template vorhanden if [[ "${SHOW_AVAIL[$((ver_sel-1))]}" == "false" ]]; then warn "Für ${DISTRO_LABEL} ${SHOW_VERS[$((ver_sel-1))]} ist noch kein Template in PVE verfügbar." warn "Bitte wähle eine der verfügbaren Versionen." continue fi CHOSEN_VER="${SHOW_VERS[$((ver_sel-1))]}" # Template zuweisen (aus PVE-Map oder vorherige) if [[ "$ver_sel" == "1" ]] && $PREV_IN_PVE; then CHOSEN_TEMPLATE="$PREV_TEMPLATE" elif [[ "$ver_sel" == "3" ]] && $NEXT_VER_IN_PVE; then CHOSEN_TEMPLATE="$NEXT_TEMPLATE_PVE" else CHOSEN_TEMPLATE="${VER_TO_TEMPLATE[$CHOSEN_VER]}" fi success "Gewählt: ${BOLD}${DISTRO_LABEL} ${CHOSEN_VER}${NC} → ${CHOSEN_TEMPLATE}" break else warn "Bitte eine Zahl zwischen 1 und ${#SHOW_VERS[@]} eingeben." fi done echo "" # ── Distro-spezifische Defaults ──────────────────────────────────────────────── if [[ "$DISTRO" == "debian" ]]; then DEFAULT_RAM=512; DEFAULT_DISK=4; DEFAULT_HOSTNAME="debian" DISTRO_BADGE="https://img.shields.io/badge/Debian%20${CHOSEN_VER}-A81D33?style=for-the-badge&logo=debian&logoColor=white" DISTRO_LINK="https://www.debian.org" DOCS_LABEL="Debian Docs"; DOCS_URL="https://www.debian.org/releases/" PKG_LABEL="Debian Packages"; PKG_URL="https://packages.debian.org" SSH_CFG_MODE="sshd_config" # Debian: direkt in sshd_config else DEFAULT_RAM=1024; DEFAULT_DISK=8; DEFAULT_HOSTNAME="ubuntu-server" VER_NICE="${CHOSEN_VER} LTS" DISTRO_BADGE="https://img.shields.io/badge/Ubuntu%20${CHOSEN_VER// /%20}%20LTS-E95420?style=for-the-badge&logo=ubuntu&logoColor=white" DISTRO_LINK="https://ubuntu.com/server" DOCS_LABEL="Ubuntu Server Docs"; DOCS_URL="https://ubuntu.com/server/docs" PKG_LABEL="Ubuntu Packages"; PKG_URL="https://packages.ubuntu.com" SSH_CFG_MODE="sshd_config_d" # Ubuntu: Drop-in /etc/ssh/sshd_config.d/ fi # ══════════════════════════════════════════════════════════════════════════════ # SCHRITT 3 – CONTAINER-KONFIGURATION # ══════════════════════════════════════════════════════════════════════════════ echo -e "${CYAN}╔══ Schritt 3: Container-Konfiguration ════════════════════════╗${NC}" echo "" DEFAULT_CTID=$(pvesh get /cluster/nextid) echo -e "${CYAN}── Grundkonfiguration ───────────────────────────────────────${NC}" ask "Container-ID" "$DEFAULT_CTID" CTID ask "Hostname" "$DEFAULT_HOSTNAME" HOSTNAME ask "Root-Passwort" "ChangeMe123!" PASSWORD echo "" # ── Storage-Auswahl ──────────────────────────────────────────────────────────── echo -e "${CYAN}── Storage-Auswahl ──────────────────────────────────────────${NC}" echo -e " Verfügbare Storages:\n" mapfile -t STORAGE_LINES < <( pvesm status --content rootdir 2>/dev/null \ | awk 'NR>1 && $3=="active" {print $1, $2, $5, $4}' | sort ) [[ ${#STORAGE_LINES[@]} -eq 0 ]] && mapfile -t STORAGE_LINES < <( pvesm status 2>/dev/null | awk 'NR>1 && $3=="active" {print $1, $2, $5, $4}' | sort ) if [[ ${#STORAGE_LINES[@]} -eq 0 ]]; then warn "Keine Storages gefunden – manuelle Eingabe." ask "Storage-Name" "local-lvm" STORAGE else local_human_size() { local kb=$1 (( kb >= 1073741824 )) && { printf "%.1f TiB" "$(echo "scale=1;$kb/1073741824"|bc)"; return; } (( kb >= 1048576 )) && { printf "%.1f GiB" "$(echo "scale=1;$kb/1048576"|bc)"; return; } (( kb >= 1024 )) && { printf "%.1f MiB" "$(echo "scale=1;$kb/1024"|bc)"; return; } printf "%d KiB" "$kb" } declare -a ST_NAMES=() printf " ${BOLD}%-4s %-20s %-14s %-12s %-12s${NC}\n" "Nr." "Name" "Typ" "Frei" "Gesamt" echo -e " ──────────────────────────────────────────────────────────" i=1 for line in "${STORAGE_LINES[@]}"; do read -r sname stype sused stotal <<< "$line" sfree=0 [[ "$stotal" =~ ^[0-9]+$ && "$sused" =~ ^[0-9]+$ ]] && sfree=$(( stotal - sused )) if [[ "$stotal" =~ ^[0-9]+$ ]] && (( stotal > 0 )); then fs=$(local_human_size "$sfree"); ts=$(local_human_size "$stotal") pct=$(( sfree * 100 / stotal )) col="$GREEN"; (( pct < 15 )) && col="$RED"; (( pct>=15 && pct<30 )) && col="$YELLOW" else fs="n/a"; ts="n/a"; col="$NC" fi printf " ${BOLD}%-4s${NC} ${col}%-20s${NC} %-14s %-12s %-12s\n" "[$i]" "$sname" "$stype" "$fs" "$ts" ST_NAMES+=("$sname"); (( i++ )) done echo "" while true; do echo -ne "${BOLD} Auswahl${NC} [${YELLOW}1${NC}]: " read -r sel; sel="${sel:-1}" if [[ "$sel" =~ ^[0-9]+$ ]] && (( sel>=1 && sel<=${#ST_NAMES[@]} )); then STORAGE="${ST_NAMES[$((sel-1))]}"; success "Storage: ${BOLD}${STORAGE}${NC}"; break else warn "Ungültige Eingabe."; fi done fi echo "" echo -e "${CYAN}── Ressourcen ───────────────────────────────────────────────${NC}" ask "Disk-Größe (GB)" "$DEFAULT_DISK" DISK ask "RAM (MB)" "$DEFAULT_RAM" RAM ask "Swap (MB)" "512" SWAP ask "CPU-Kerne" "2" CORES ask "Netzwerk-Bridge" "vmbr0" BRIDGE echo "" echo -e "${CYAN}── Netzwerk ─────────────────────────────────────────────────${NC}" echo -e " DHCP: '${YELLOW}dhcp${NC}' | Statisch z.B.: '${YELLOW}192.168.1.100/24${NC}'" ask "IP-Adresse" "dhcp" IP_ADDR GATEWAY="" [[ "$IP_ADDR" != "dhcp" ]] && ask "Gateway" "" GATEWAY echo "" echo -e "${CYAN}── Container-Typ ────────────────────────────────────────────${NC}" ask_yn "Unprivilegiert? (empfohlen)" "j" && UNPRIVILEGED=1 || UNPRIVILEGED=0 echo "" echo -e "${CYAN}── Zeitzone ─────────────────────────────────────────────────${NC}" ask "Zeitzone" "Europe/Berlin" TIMEZONE echo "" # ══════════════════════════════════════════════════════════════════════════════ # SCHRITT 4 – SOFTWARE # ══════════════════════════════════════════════════════════════════════════════ echo -e "${CYAN}╔══ Schritt 4: Software ════════════════════════════════════════╗${NC}" echo "" # ── Docker ──────────────────────────────────────────────────────────────────── echo -e "${CYAN}── Docker ───────────────────────────────────────────────────${NC}" if ask_yn "🐳 Docker Engine installieren?" "j"; then INSTALL_DOCKER=true echo "" echo -e "${CYAN}── Portainer ────────────────────────────────────────────────${NC}" if ask_yn "🖥 Portainer CE (Docker Web-UI) installieren?" "j"; then INSTALL_PORTAINER=true ask "Portainer HTTPS-Port" "9443" PORTAINER_PORT else INSTALL_PORTAINER=false PORTAINER_PORT="9443" fi else INSTALL_DOCKER=false INSTALL_PORTAINER=false PORTAINER_PORT="9443" fi echo "" # ── Ubuntu-spezifische Optionen ──────────────────────────────────────────────── ENABLE_UNATTENDED=false DISABLE_SNAP=false DISABLE_PRO_MOTD=false if [[ "$DISTRO" == "ubuntu" ]]; then echo -e "${CYAN}── Ubuntu-Optionen ──────────────────────────────────────────${NC}" ask_yn "Automatische Sicherheitsupdates (unattended-upgrades)?" "j" \ && ENABLE_UNATTENDED=true || true ask_yn "Snap deaktivieren? (spart Ressourcen im LXC)" "j" \ && DISABLE_SNAP=true || true ask_yn "Ubuntu Pro / ESM Werbemeldungen im MOTD deaktivieren?" "j" \ && DISABLE_PRO_MOTD=true || true echo "" fi # ── Optionale Pakete ─────────────────────────────────────────────────────────── echo -e "${CYAN}── Optionale Pakete ─────────────────────────────────────────${NC}" ask_yn "curl, wget, git (Basis-Tools)" "j" && INSTALL_BASETOOLS=true || INSTALL_BASETOOLS=false ask_yn "htop, ncdu, net-tools (System-Monitoring)" "j" && INSTALL_MONITORING=true || INSTALL_MONITORING=false ask_yn "vim + nano (Editoren)" "j" && INSTALL_EDITORS=true || INSTALL_EDITORS=false ask_yn "cron + logrotate (Automatisierung)" "j" && INSTALL_CRON=true || INSTALL_CRON=false ask_yn "fail2ban (Brute-Force-Schutz)" "n" && INSTALL_FAIL2BAN=true || INSTALL_FAIL2BAN=false ask_yn "ufw (Firewall)" "n" && INSTALL_UFW=true || INSTALL_UFW=false echo "" # ══════════════════════════════════════════════════════════════════════════════ # SCHRITT 5 – SSH # ══════════════════════════════════════════════════════════════════════════════ echo -e "${CYAN}╔══ Schritt 5: SSH-Zugriff ══════════════════════════════════════╗${NC}" echo "" if ask_yn "🔑 SSH-Root-Login von externen Terminals aktivieren?" "j"; then ENABLE_ROOT_SSH=true ask "SSH-Port" "22" SSH_PORT echo "" echo -e " ${BOLD}Authentifizierung:${NC}" echo -e " ${YELLOW}[1]${NC} Nur Passwort" echo -e " ${YELLOW}[2]${NC} Nur SSH-Key" echo -e " ${YELLOW}[3]${NC} Passwort + SSH-Key" echo -ne "${BOLD} Auswahl${NC} [${YELLOW}1${NC}]: " read -r ssh_auth_sel case "${ssh_auth_sel:-1}" in 2) SSH_AUTH="key" ;; 3) SSH_AUTH="both" ;; *) SSH_AUTH="password" ;; esac SSH_PUBKEY="" if [[ "$SSH_AUTH" == "key" || "$SSH_AUTH" == "both" ]]; then echo "" echo -e " Public Key eingeben (Inhalt von ${YELLOW}~/.ssh/id_rsa.pub${NC}):" echo -ne " ${BOLD}Key:${NC} " read -r SSH_PUBKEY if [[ -z "$SSH_PUBKEY" ]]; then warn "Kein Key eingegeben – falle auf Passwort zurück." SSH_AUTH="password" fi fi else ENABLE_ROOT_SSH=false SSH_PORT="22" SSH_AUTH="password" fi echo "" # ══════════════════════════════════════════════════════════════════════════════ # ZUSAMMENFASSUNG # ══════════════════════════════════════════════════════════════════════════════ echo -e "${BOLD}╔════════════════════════ Zusammenfassung ════════════════════════╗${NC}" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Distribution:" "${DISTRO_LABEL} ${CHOSEN_VER}" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Template:" "$CHOSEN_TEMPLATE" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Container-ID:" "$CTID" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Hostname:" "$HOSTNAME" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Storage:" "$STORAGE" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Disk / RAM / Swap:" "${DISK}GB / ${RAM}MB / ${SWAP}MB" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "CPU / Netzwerk:" "${CORES} Kerne / ${IP_ADDR}" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Zeitzone:" "$TIMEZONE" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Unprivilegiert:" "$([[ $UNPRIVILEGED -eq 1 ]] && echo Ja || echo Nein)" echo -e "${BOLD}║${NC} ───────────────────────────────────────────────────────── ${BOLD}║${NC}" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Docker:" "$($INSTALL_DOCKER && echo Ja || echo Nein)" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Portainer CE:" "$($INSTALL_PORTAINER && echo "Ja (:${PORTAINER_PORT})" || echo Nein)" if [[ "$DISTRO" == "ubuntu" ]]; then printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Unattended-Upgrades:" "$($ENABLE_UNATTENDED && echo Ja || echo Nein)" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Snap deaktiviert:" "$($DISABLE_SNAP && echo Ja || echo Nein)" fi printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Basis-Tools:" "$($INSTALL_BASETOOLS && echo Ja || echo Nein)" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Monitoring-Tools:" "$($INSTALL_MONITORING && echo Ja || echo Nein)" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Editoren:" "$($INSTALL_EDITORS && echo Ja || echo Nein)" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Cron / Logrotate:" "$($INSTALL_CRON && echo Ja || echo Nein)" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "fail2ban:" "$($INSTALL_FAIL2BAN && echo Ja || echo Nein)" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "UFW Firewall:" "$($INSTALL_UFW && echo Ja || echo Nein)" printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Root-SSH:" "$($ENABLE_ROOT_SSH && echo "Ja (Port ${SSH_PORT}, ${SSH_AUTH})" || echo Nein)" echo -e "${BOLD}╚═════════════════════════════════════════════════════════════════╝${NC}" echo "" ask_yn "Jetzt installieren?" "j" || { echo "Abgebrochen."; exit 0; } # ══════════════════════════════════════════════════════════════════════════════ # INSTALLATION # ══════════════════════════════════════════════════════════════════════════════ # ── Template herunterladen ───────────────────────────────────────────────────── info "Prüfe Template …" DOWNLOADED=$(pveam list local 2>/dev/null | awk '{print $1}' | grep -F "$CHOSEN_TEMPLATE" || true) if [[ -z "$DOWNLOADED" ]]; then info "Lade Template herunter: ${CHOSEN_TEMPLATE} …" pveam download local "$CHOSEN_TEMPLATE" success "Template heruntergeladen." else success "Template bereits lokal vorhanden." fi TEMPLATE_PATH="local:vztmpl/${CHOSEN_TEMPLATE}" # ── LXC erstellen ───────────────────────────────────────────────────────────── info "Erstelle LXC-Container ${CTID} …" NET_CONFIG="name=eth0,bridge=${BRIDGE}" [[ "$IP_ADDR" == "dhcp" ]] && NET_CONFIG+=",ip=dhcp" || { NET_CONFIG+=",ip=${IP_ADDR}" [[ -n "$GATEWAY" ]] && NET_CONFIG+=",gw=${GATEWAY}" } FEATURES="keyctl=1,nesting=1" # Bei Docker + unprivilegiert: nesting zwingend, bei privilegiert nicht nötig aber schadet nicht $INSTALL_DOCKER && [[ $UNPRIVILEGED -eq 1 ]] && FEATURES="keyctl=1,nesting=1" pct create "$CTID" "$TEMPLATE_PATH" \ --hostname "$HOSTNAME" \ --password "$PASSWORD" \ --storage "$STORAGE" \ --rootfs "${STORAGE}:${DISK}" \ --memory "$RAM" \ --swap "$SWAP" \ --cores "$CORES" \ --net0 "$NET_CONFIG" \ --unprivileged "$UNPRIVILEGED" \ --features "$FEATURES" \ --onboot 1 --start 0 success "Container ${CTID} erstellt." info "Starte Container …" pct start "$CTID" sleep 6 lxc_exec() { pct exec "$CTID" -- bash -c "$*"; } # ── Noninteraktiv setzen (Ubuntu) ────────────────────────────────────────────── [[ "$DISTRO" == "ubuntu" ]] && \ lxc_exec "echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections 2>/dev/null || true" # ── System aktualisieren ─────────────────────────────────────────────────────── info "System aktualisieren …" lxc_exec " export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get upgrade -y -qq -o Dpkg::Options::='--force-confold' apt-get install -y -qq ca-certificates locales " success "System aktuell." # ── Zeitzone & Locale ────────────────────────────────────────────────────────── info "Zeitzone & Locale konfigurieren …" lxc_exec " export DEBIAN_FRONTEND=noninteractive ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime echo '${TIMEZONE}' > /etc/timezone dpkg-reconfigure -f noninteractive tzdata 2>/dev/null || true sed -i 's/^# *de_DE.UTF-8/de_DE.UTF-8/' /etc/locale.gen 2>/dev/null || true sed -i 's/^# *en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen 2>/dev/null || true locale-gen 2>/dev/null || true update-locale LANG=de_DE.UTF-8 LC_MESSAGES=en_US.UTF-8 2>/dev/null || true " success "Zeitzone: ${TIMEZONE} | Locale: de_DE.UTF-8" # ── Ubuntu-spezifisch: Snap / Pro-MOTD / Unattended ─────────────────────────── if [[ "$DISTRO" == "ubuntu" ]]; then if $DISABLE_SNAP; then info "Snap deaktivieren …" lxc_exec " systemctl stop snapd 2>/dev/null || true systemctl disable snapd 2>/dev/null || true systemctl mask snapd 2>/dev/null || true apt-get purge -y -qq snapd 2>/dev/null || true rm -rf /snap /var/snap /var/lib/snapd /var/cache/snapd 2>/dev/null || true cat > /etc/apt/preferences.d/no-snapd << 'PIN' Package: snapd Pin: release a=* Pin-Priority: -10 PIN " 2>/dev/null success "Snap deaktiviert." fi if $DISABLE_PRO_MOTD; then info "Ubuntu Pro MOTD deaktivieren …" lxc_exec " for f in 10-help-text 50-motd-news 88-esm-announce 91-contract-ua-esm-status 95-hwe-eol; do [ -f /etc/update-motd.d/\$f ] && chmod -x /etc/update-motd.d/\$f || true done command -v pro &>/dev/null && pro config set motd=false 2>/dev/null || true " 2>/dev/null success "Ubuntu Pro MOTD deaktiviert." fi if $ENABLE_UNATTENDED; then info "Unattended-Upgrades konfigurieren …" lxc_exec " export DEBIAN_FRONTEND=noninteractive apt-get install -y -qq unattended-upgrades update-notifier-common cat > /etc/apt/apt.conf.d/20auto-upgrades << 'AUTO' APT::Periodic::Update-Package-Lists \"1\"; APT::Periodic::Unattended-Upgrade \"1\"; APT::Periodic::AutocleanInterval \"7\"; APT::Periodic::Download-Upgradeable-Packages \"1\"; AUTO systemctl enable unattended-upgrades --now 2>/dev/null || true " success "Unattended-Upgrades aktiv." fi fi # ── Optionale Pakete ─────────────────────────────────────────────────────────── PKGS=() $INSTALL_BASETOOLS && PKGS+=(curl wget git) $INSTALL_MONITORING && PKGS+=(htop ncdu tree net-tools iputils-ping) $INSTALL_EDITORS && PKGS+=(vim nano) $INSTALL_CRON && PKGS+=(cron logrotate) $INSTALL_FAIL2BAN && PKGS+=(fail2ban) $INSTALL_UFW && PKGS+=(ufw) if [[ ${#PKGS[@]} -gt 0 ]]; then info "Installiere Pakete: ${PKGS[*]} …" lxc_exec "export DEBIAN_FRONTEND=noninteractive && apt-get install -y -qq ${PKGS[*]}" success "Pakete installiert." fi # ── fail2ban konfigurieren ───────────────────────────────────────────────────── if $INSTALL_FAIL2BAN; then info "fail2ban konfigurieren …" lxc_exec " cat > /etc/fail2ban/jail.local << 'F2B' [DEFAULT] bantime = 1h findtime = 10m maxretry = 5 backend = systemd [sshd] enabled = true F2B systemctl enable fail2ban --now 2>/dev/null || true " success "fail2ban aktiv (5 Versuche / 10 Min → 1h Ban)." fi # ── UFW konfigurieren ────────────────────────────────────────────────────────── if $INSTALL_UFW; then info "UFW vorkonfigurieren …" lxc_exec " ufw default deny incoming ufw default allow outgoing ufw allow ${SSH_PORT}/tcp comment 'SSH' " success "UFW konfiguriert (deaktiviert – mit 'ufw enable' aktivieren)." fi # ── Docker installieren ──────────────────────────────────────────────────────── if $INSTALL_DOCKER; then info "Docker Engine installieren (offizieller Kanal) …" lxc_exec " export DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ca-certificates curl gnupg lsb-release install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/${DISTRO}/gpg \ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg --yes chmod a+r /etc/apt/keyrings/docker.gpg echo \"deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/${DISTRO} \ \$(. /etc/os-release && echo \"\$VERSION_CODENAME\") stable\" \ > /etc/apt/sources.list.d/docker.list apt-get update -qq apt-get install -y -qq \ docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin systemctl enable docker --now " success "Docker installiert." info "Docker-Funktionstest …" lxc_exec "docker run --rm hello-world" &>/dev/null \ && success "Docker läuft korrekt." \ || warn "Docker-Test fehlgeschlagen – bitte manuell prüfen." fi # ── Portainer installieren ───────────────────────────────────────────────────── if $INSTALL_PORTAINER; then info "Portainer CE installieren (Port ${PORTAINER_PORT}) …" lxc_exec " docker volume create portainer_data docker run -d \ --name portainer \ --restart always \ -p 8000:8000 \ -p ${PORTAINER_PORT}:9443 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v portainer_data:/data \ portainer/portainer-ce:latest " success "Portainer CE gestartet." fi # ── SSH konfigurieren ────────────────────────────────────────────────────────── if $ENABLE_ROOT_SSH; then info "SSH-Server konfigurieren …" lxc_exec "export DEBIAN_FRONTEND=noninteractive && apt-get install -y -qq openssh-server" if [[ "$SSH_CFG_MODE" == "sshd_config_d" ]]; then # Ubuntu: Drop-in Datei lxc_exec " mkdir -p /etc/ssh/sshd_config.d cat > /etc/ssh/sshd_config.d/99-lxc.conf << SSHEOF PermitRootLogin yes Port ${SSH_PORT} SSHEOF " else # Debian: direkt in sshd_config lxc_exec " sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config grep -q '^PermitRootLogin' /etc/ssh/sshd_config || echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config sed -i 's/^#*Port .*/Port ${SSH_PORT}/' /etc/ssh/sshd_config grep -q '^Port ' /etc/ssh/sshd_config || echo 'Port ${SSH_PORT}' >> /etc/ssh/sshd_config " fi # Auth-Methode case "$SSH_AUTH" in password) lxc_exec " CFG=${SSH_CFG_MODE/sshd_config_d//etc/ssh/sshd_config.d/99-lxc.conf} [[ '$SSH_CFG_MODE' == 'sshd_config_d' ]] && FILE=/etc/ssh/sshd_config.d/99-lxc.conf || FILE=/etc/ssh/sshd_config echo 'PasswordAuthentication yes' >> \$FILE echo 'PubkeyAuthentication no' >> \$FILE " ;; key|both) lxc_exec " [[ '$SSH_CFG_MODE' == 'sshd_config_d' ]] && FILE=/etc/ssh/sshd_config.d/99-lxc.conf || FILE=/etc/ssh/sshd_config [[ '$SSH_AUTH' == 'both' ]] && echo 'PasswordAuthentication yes' >> \$FILE || echo 'PasswordAuthentication no' >> \$FILE echo 'PubkeyAuthentication yes' >> \$FILE mkdir -p /root/.ssh && chmod 700 /root/.ssh echo '${SSH_PUBKEY}' >> /root/.ssh/authorized_keys chmod 600 /root/.ssh/authorized_keys " ;; esac lxc_exec "systemctl enable ssh --now && systemctl restart ssh" sleep 3 CT_TEST_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}' || true) if [[ -n "$CT_TEST_IP" ]] && nc -z -w5 "$CT_TEST_IP" "$SSH_PORT" 2>/dev/null; then success "SSH erreichbar → ssh root@${CT_TEST_IP}$( [[ "$SSH_PORT" != "22" ]] && echo " -p ${SSH_PORT}" )" else warn "SSH-Port noch nicht erreichbar – kurz warten und erneut testen." fi fi # ── Portainer-Timer zurücksetzen ─────────────────────────────────────────────── if $INSTALL_PORTAINER; then info "Portainer-Timer zurücksetzen (Neustart nach Installation) …" pct exec "$CTID" -- docker restart portainer &>/dev/null \ && success "Portainer neu gestartet – 5-Minuten-Fenster beginnt jetzt." \ || warn "Portainer-Neustart fehlgeschlagen – manuell: docker restart portainer" fi # ── Proxmox-Notes setzen ─────────────────────────────────────────────────────── info "Setze Proxmox-Hinweise …" sleep 2 NOTES_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}' || echo "") [[ -z "$NOTES_IP" ]] && NOTES_IP="n/a (DHCP)" SSH_LINE="" $ENABLE_ROOT_SSH && { [[ "$SSH_PORT" == "22" ]] \ && SSH_LINE="ssh root@${NOTES_IP}" \ || SSH_LINE="ssh root@${NOTES_IP} -p ${SSH_PORT}" } NOTE="[![${DISTRO_LABEL}](${DISTRO_BADGE})](${DISTRO_LINK})" $INSTALL_DOCKER && \ NOTE+=" [![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white)](https://www.docker.com)" NOTE+=$'\n\n---\n\n' NOTE+="**🌐 IP-Adresse:** \`${NOTES_IP}\`"$'\n\n' $INSTALL_PORTAINER && \ NOTE+="**🐳 Portainer:** [https://${NOTES_IP}:${PORTAINER_PORT}](https://${NOTES_IP}:${PORTAINER_PORT})"$'\n\n' $ENABLE_ROOT_SSH && \ NOTE+="**🔑 SSH:** \`${SSH_LINE}\`"$'\n\n' INST="" $INSTALL_BASETOOLS && INST+="curl wget git " $INSTALL_MONITORING && INST+="htop ncdu net-tools " $INSTALL_EDITORS && INST+="vim nano " $INSTALL_CRON && INST+="cron logrotate " $INSTALL_FAIL2BAN && INST+="fail2ban " $INSTALL_UFW && INST+="ufw " $ENABLE_UNATTENDED && INST+="unattended-upgrades " [[ -n "$INST" ]] && NOTE+="**📦 Pakete:** \`${INST% }\`"$'\n\n' NOTE+="**🕐 Zeitzone:** ${TIMEZONE}"$'\n\n' NOTE+="---"$'\n\n' NOTE+="Installiert mit [gitea.vourx.com](https://gitea.vourx.com)" NOTE+=" | [${DOCS_LABEL}](${DOCS_URL})" NOTE+=" | [${PKG_LABEL}](${PKG_URL})" pvesh set /nodes/$(hostname)/lxc/${CTID}/config --description "${NOTE}" 2>/dev/null \ && success "Proxmox-Hinweise gesetzt." \ || warn "Hinweise konnten nicht gesetzt werden." # ── Aufräumen ────────────────────────────────────────────────────────────────── info "System aufräumen …" lxc_exec "export DEBIAN_FRONTEND=noninteractive && apt-get autoremove -y -qq && apt-get autoclean -qq" success "Aufgeräumt." # ── Abschluss ────────────────────────────────────────────────────────────────── CONTAINER_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}' || echo "unbekannt") echo "" echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}" echo -e "${GREEN}║${NC} ${BOLD}Installation abgeschlossen!${NC}" echo -e "${BOLD}${GREEN}╠══════════════════════════════════════════════════════════════╣${NC}" echo -e "${GREEN}║${NC}" printf "${GREEN}║${NC} %-22s ${BOLD}%s %s${NC}\n" "Distribution:" "$DISTRO_LABEL" "$CHOSEN_VER" printf "${GREEN}║${NC} %-22s ${BOLD}%s${NC}\n" "Container-ID:" "$CTID" printf "${GREEN}║${NC} %-22s ${BOLD}%s${NC}\n" "Hostname:" "$HOSTNAME" printf "${GREEN}║${NC} %-22s ${BOLD}%s${NC}\n" "IP-Adresse:" "$CONTAINER_IP" printf "${GREEN}║${NC} %-22s ${BOLD}%s${NC}\n" "Zeitzone:" "$TIMEZONE" echo -e "${GREEN}║${NC}" $INSTALL_DOCKER && \ printf "${GREEN}║${NC} %-22s ${BOLD}%s${NC}\n" "Docker:" "installiert & aktiv" if $INSTALL_PORTAINER; then printf "${GREEN}║${NC} %-22s ${BOLD}https://%s:%s${NC}\n" "Portainer UI:" "$CONTAINER_IP" "$PORTAINER_PORT" echo -e "${GREEN}║${NC} ${YELLOW} ⏱ Bitte innerhalb von 5 Min. Admin-Account anlegen!${NC}" fi if $ENABLE_ROOT_SSH; then echo -e "${GREEN}║${NC}" echo -e "${GREEN}║${NC} ${CYAN}── SSH ─────────────────────────────────────────────────${NC}" [[ "$SSH_PORT" == "22" ]] \ && printf "${GREEN}║${NC} ${BOLD}ssh root@%s${NC}\n" "$CONTAINER_IP" \ || printf "${GREEN}║${NC} ${BOLD}ssh root@%s -p %s${NC}\n" "$CONTAINER_IP" "$SSH_PORT" printf "${GREEN}║${NC} %-22s ${BOLD}%s${NC}\n" "Auth:" "$SSH_AUTH" fi if $INSTALL_UFW; then echo -e "${GREEN}║${NC}" echo -e "${GREEN}║${NC} ${YELLOW}🔒 UFW installiert aber noch nicht aktiv.${NC}" echo -e "${GREEN}║${NC} ${YELLOW} Mit 'ufw enable' im Container aktivieren.${NC}" fi echo -e "${GREEN}║${NC}" echo -e "${GREEN}║${NC} ${YELLOW}⚠ Root-Passwort nach erstem Login bitte ändern!${NC}" echo -e "${GREEN}║${NC}" echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}" if $INSTALL_PORTAINER; then echo -e "\n ${YELLOW}Portainer Timeout? →${NC} ${BOLD}pct exec ${CTID} -- docker restart portainer${NC}" fi echo ""