multi-os-install.sh hinzugefügt

This commit is contained in:
2026-05-25 06:19:45 +02:00
commit 13ffdff7da
+767
View File
@@ -0,0 +1,767 @@
#!/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."
# ── Die 3 relevantesten Versionen bestimmen: vorherige / aktuelle / nächste ────
# "Aktuell" = höchste stabile Version (letzte in der sortierten Liste)
# "Vorherige" = eine davor
# "Nächste" = falls vorhanden (Alpha/Beta/RC Templates in PVE)
TOTAL=${#VER_ORDER[@]}
CURRENT_IDX=$(( TOTAL - 1 ))
# Prüfen ob es eine "nächste" Version gibt (höher als die letzte stabile)
# PVE-Datenbank enthält manchmal schon Beta-Templates → nehmen wir mit
NEXT_IDX=$(( CURRENT_IDX + 1 )) # existiert nicht → wird unten abgefangen
PREV_IDX=$(( CURRENT_IDX - 1 ))
# Verfügbare Slots bauen
declare -a SHOW_VERS=()
declare -a SHOW_LABELS=()
# Vorherige Version (falls vorhanden)
if (( PREV_IDX >= 0 )); then
SHOW_VERS+=("${VER_ORDER[$PREV_IDX]}")
SHOW_LABELS+=("Vorherige Version (${VER_ORDER[$PREV_IDX]})")
fi
# Aktuelle Version (Standard)
SHOW_VERS+=("${VER_ORDER[$CURRENT_IDX]}")
SHOW_LABELS+=("Aktuelle Version (${VER_ORDER[$CURRENT_IDX]}) ← empfohlen")
DEFAULT_VER_IDX=$(( ${#SHOW_VERS[@]} )) # 1-basierter Default für die Ausgabe
# Nächste Version (nur wenn tatsächlich ein neueres Template existiert)
# → In der Praxis liefert PVE hier manchmal schon den nächsten LTS
if (( NEXT_IDX < TOTAL )); then
SHOW_VERS+=("${VER_ORDER[$NEXT_IDX]}")
SHOW_LABELS+=("Nächste Version (${VER_ORDER[$NEXT_IDX]}) [experimentell]")
fi
echo -e " Gefundene ${DISTRO_LABEL}-Versionen:\n"
for i in "${!SHOW_VERS[@]}"; do
num=$(( i + 1 ))
if (( num == DEFAULT_VER_IDX )); then
echo -e " ${BOLD}[${num}]${NC} ${GREEN}${SHOW_LABELS[$i]}${NC}"
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
CHOSEN_VER="${SHOW_VERS[$((ver_sel-1))]}"
CHOSEN_TEMPLATE="${VER_TO_TEMPLATE[$CHOSEN_VER]}"
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 ""