Files
multi-os-installer-LXC/multi-os-install.sh
T

767 lines
40 KiB
Bash
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
# ==============================================================================
# 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 ""