#!/usr/bin/env bash # ============================================================================== # Proxmox LXC: Debian + Docker + Portainer (optional) # Aufruf auf dem PVE-Host: # bash <(curl -fsSL https://your-host/debian-docker-portainer-lxc.sh) # ============================================================================== set -euo pipefail # ── Farben ───────────────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; 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 prüfen ───────────────────────────────────────────────────── [[ $EUID -ne 0 ]] && error "Dieses Skript muss als root auf dem Proxmox-Host ausgeführt werden." command -v pct &>/dev/null || error "'pct' nicht gefunden – läuft das Skript auf einem PVE-Host?" command -v pvesh &>/dev/null || error "'pvesh' nicht gefunden – läuft das Skript auf einem PVE-Host?" # ══════════════════════════════════════════════════════════════════════════════ echo -e "\n${BOLD}╔══════════════════════════════════════════════════════════╗${NC}" echo -e "${BOLD}║ Proxmox LXC – Debian + Docker + Portainer Installer ║${NC}" echo -e "${BOLD}╚══════════════════════════════════════════════════════════╝${NC}\n" # ── Standardwerte ────────────────────────────────────────────────────────────── DEFAULT_CTID=$(pvesh get /cluster/nextid) DEFAULT_HOSTNAME="debian-docker" DEFAULT_PASSWORD="ChangeMe123!" DEFAULT_DISK=8 # GB DEFAULT_RAM=2048 # MB DEFAULT_SWAP=512 # MB DEFAULT_CORES=2 DEFAULT_BRIDGE="vmbr0" DEFAULT_IP="dhcp" # oder z.B. 192.168.1.100/24 DEFAULT_GW="" # Gateway (nur bei statischer IP) # ── Interaktive Eingabe ──────────────────────────────────────────────────────── 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:-y}" echo -ne "${BOLD}${prompt}${NC} [${YELLOW}${default}${NC}]: " read -r yn [[ "${yn:-$default}" =~ ^[Jj1Yy] ]] } # ── Storage-Auswahl (dynamisch) ─────────────────────────────────────────────── select_storage() { echo -e "${CYAN}── Storage-Auswahl ─────────────────────────────────────────${NC}" echo -e " Verfügbare Storages auf diesem Host:\n" # pvesm status liefert: Name, Type, Status, Total, Used, Available, % # Wir filtern nur aktive Storages, die LXC-Container unterstützen mapfile -t STORAGE_LINES < <( pvesm status --content rootdir 2>/dev/null \ | awk 'NR>1 && $3=="active" {print $1, $2, $5, $4}' \ | sort ) if [[ ${#STORAGE_LINES[@]} -eq 0 ]]; then # Fallback: alle aktiven Storages anzeigen (ohne Content-Filter) mapfile -t STORAGE_LINES < <( pvesm status 2>/dev/null \ | awk 'NR>1 && $3=="active" {print $1, $2, $5, $4}' \ | sort ) fi if [[ ${#STORAGE_LINES[@]} -eq 0 ]]; then warn "Keine aktiven Storages gefunden – bitte manuell eingeben." ask "Storage-Name" "local-lvm" STORAGE return fi local i=1 local -a STORAGE_NAMES=() printf " ${BOLD}%-4s %-20s %-14s %-12s %-12s${NC}\n" "Nr." "Name" "Typ" "Frei" "Gesamt" echo -e " ──────────────────────────────────────────────────────────" for line in "${STORAGE_LINES[@]}"; do read -r sname stype sused stotal <<< "$line" # Freien Speicher berechnen (Werte in KB von pvesm) local sfree=0 if [[ "$stotal" =~ ^[0-9]+$ ]] && [[ "$sused" =~ ^[0-9]+$ ]]; then sfree=$(( stotal - sused )) fi # Lesbare Größen human_size() { local kb=$1 if (( kb >= 1073741824 )); then printf "%.1f TiB" "$(echo "scale=1; $kb/1073741824" | bc)" elif (( kb >= 1048576 )); then printf "%.1f GiB" "$(echo "scale=1; $kb/1048576" | bc)" elif (( kb >= 1024 )); then printf "%.1f MiB" "$(echo "scale=1; $kb/1024" | bc)" else printf "%d KiB" "$kb"; fi } local free_str total_str if [[ "$stotal" =~ ^[0-9]+$ ]] && (( stotal > 0 )); then free_str=$(human_size "$sfree") total_str=$(human_size "$stotal") else free_str="n/a" total_str="n/a" fi # Farbe je nach freiem Platz local color="$GREEN" if [[ "$stotal" =~ ^[0-9]+$ ]] && (( stotal > 0 )); then local pct=$(( sfree * 100 / stotal )) (( pct < 15 )) && color="$RED" (( pct >= 15 && pct < 30 )) && color="$YELLOW" fi printf " ${BOLD}%-4s${NC} ${color}%-20s${NC} %-14s %-12s %-12s\n" \ "[$i]" "$sname" "$stype" "$free_str" "$total_str" STORAGE_NAMES+=("$sname") (( i++ )) done echo "" local DEFAULT_NUM=1 while true; do echo -ne "${BOLD}Auswahl (Nummer eingeben)${NC} [${YELLOW}${DEFAULT_NUM}${NC}]: " read -r sel sel="${sel:-$DEFAULT_NUM}" if [[ "$sel" =~ ^[0-9]+$ ]] && (( sel >= 1 && sel <= ${#STORAGE_NAMES[@]} )); then STORAGE="${STORAGE_NAMES[$((sel-1))]}" success "Storage gewählt: ${BOLD}${STORAGE}${NC}" break else warn "Ungültige Eingabe. Bitte eine Zahl zwischen 1 und ${#STORAGE_NAMES[@]} eingeben." fi done echo "" } echo -e "${CYAN}── Container-Grundkonfiguration ────────────────────────────${NC}" ask "Container-ID" "$DEFAULT_CTID" CTID ask "Hostname" "$DEFAULT_HOSTNAME" HOSTNAME ask "Root-Passwort" "$DEFAULT_PASSWORD" PASSWORD echo "" select_storage ask "Disk-Größe (GB)" "$DEFAULT_DISK" DISK ask "RAM (MB)" "$DEFAULT_RAM" RAM ask "Swap (MB)" "$DEFAULT_SWAP" SWAP ask "CPU-Kerne" "$DEFAULT_CORES" CORES ask "Netzwerk-Bridge" "$DEFAULT_BRIDGE" BRIDGE echo "" echo -e "${CYAN}── Netzwerk ────────────────────────────────────────────────${NC}" echo -e " Für DHCP einfach '${YELLOW}dhcp${NC}' eingeben." echo -e " Für statische IP z.B. '${YELLOW}192.168.1.100/24${NC}'" ask "IP-Adresse" "$DEFAULT_IP" IP_ADDR if [[ "$IP_ADDR" != "dhcp" ]]; then ask "Gateway" "$DEFAULT_GW" GATEWAY fi echo "" echo -e "${CYAN}── Optionale Komponenten ───────────────────────────────────${NC}" if ask_yn "Portainer CE installieren? (Web-UI für Docker)" "j"; then INSTALL_PORTAINER=true ask "Portainer HTTPS-Port" "9443" PORTAINER_PORT else INSTALL_PORTAINER=false fi echo "" echo -e "${CYAN}── SSH-Zugriff ─────────────────────────────────────────────${NC}" 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}Authentifizierungsmethode:${NC}" echo -e " ${YELLOW}[1]${NC} Nur Passwort" echo -e " ${YELLOW}[2]${NC} Nur SSH-Key (du gibst deinen Public Key ein)" echo -e " ${YELLOW}[3]${NC} Passwort ${BOLD}und${NC} SSH-Key (beide erlaubt)" 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 " Gib deinen SSH-Public-Key ein (z.B. Inhalt von ${YELLOW}~/.ssh/id_rsa.pub${NC}):" echo -ne " ${BOLD}Public Key:${NC} " read -r SSH_PUBKEY if [[ -z "$SSH_PUBKEY" ]]; then warn "Kein Public Key eingegeben – falle auf Passwort-Auth zurück." SSH_AUTH="password" fi fi else ENABLE_ROOT_SSH=false SSH_PORT="22" SSH_AUTH="password" fi if ask_yn "Unprivilegierten Container erstellen? (empfohlen)" "j"; then UNPRIVILEGED=1 else UNPRIVILEGED=0 fi # ── Zusammenfassung ──────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}╔══════════════ Zusammenfassung ════════════════╗${NC}" printf "${BOLD}║${NC} %-22s %-20s ${BOLD}║${NC}\n" "Container-ID:" "$CTID" printf "${BOLD}║${NC} %-22s %-20s ${BOLD}║${NC}\n" "Hostname:" "$HOSTNAME" printf "${BOLD}║${NC} %-22s %-20s ${BOLD}║${NC}\n" "Storage:" "$STORAGE" printf "${BOLD}║${NC} %-22s %-20s ${BOLD}║${NC}\n" "Disk:" "${DISK} GB" printf "${BOLD}║${NC} %-22s %-20s ${BOLD}║${NC}\n" "RAM / Swap:" "${RAM} MB / ${SWAP} MB" printf "${BOLD}║${NC} %-22s %-20s ${BOLD}║${NC}\n" "CPU-Kerne:" "$CORES" printf "${BOLD}║${NC} %-22s %-20s ${BOLD}║${NC}\n" "Netzwerk:" "$IP_ADDR" printf "${BOLD}║${NC} %-22s %-20s ${BOLD}║${NC}\n" "Portainer:" "$( $INSTALL_PORTAINER && echo "Ja (:${PORTAINER_PORT})" || echo "Nein" )" printf "${BOLD}║${NC} %-22s %-20s ${BOLD}║${NC}\n" "Root-SSH:" "$( $ENABLE_ROOT_SSH && echo "Ja (Port ${SSH_PORT}, Auth: ${SSH_AUTH})" || echo "Nein" )" printf "${BOLD}║${NC} %-22s %-20s ${BOLD}║${NC}\n" "Unprivilegiert:" "$( [[ $UNPRIVILEGED -eq 1 ]] && echo "Ja" || echo "Nein" )" echo -e "${BOLD}╚══════════════════════════════════════════════╝${NC}" echo "" ask_yn "Jetzt installieren?" "j" || { echo "Abgebrochen."; exit 0; } # ── Debian-Template herunterladen ────────────────────────────────────────────── info "Suche aktuelles Debian-LXC-Template …" TEMPLATE_STORAGE="local" TEMPLATE=$(pveam available --section system | awk '/debian-1[2-9]/ {print $2}' | sort -V | tail -1) [[ -z "$TEMPLATE" ]] && error "Kein Debian-Template in der PVE-Datenbank gefunden. 'pveam update' ausführen!" DOWNLOADED=$(pveam list "$TEMPLATE_STORAGE" 2>/dev/null | awk '{print $1}' | grep -F "$TEMPLATE" || true) if [[ -z "$DOWNLOADED" ]]; then info "Lade Template herunter: $TEMPLATE" pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" success "Template geladen." else success "Template bereits vorhanden: $TEMPLATE" fi TEMPLATE_PATH="${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" # ── LXC-Container erstellen ──────────────────────────────────────────────────── info "Erstelle LXC-Container $CTID …" NET_CONFIG="name=eth0,bridge=${BRIDGE}" if [[ "$IP_ADDR" == "dhcp" ]]; then NET_CONFIG+=",ip=dhcp" else NET_CONFIG+=",ip=${IP_ADDR}" [[ -n "${GATEWAY:-}" ]] && NET_CONFIG+=",gw=${GATEWAY}" fi 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 "keyctl=1,nesting=1" \ --onboot 1 \ --start 0 success "Container $CTID erstellt." # ── Container starten ────────────────────────────────────────────────────────── info "Starte Container …" pct start "$CTID" sleep 5 # ── Hilfsfunktion: Befehl im Container ausführen ────────────────────────────── lxc_exec() { pct exec "$CTID" -- bash -c "$*"; } # ── System aktualisieren ─────────────────────────────────────────────────────── info "System aktualisieren (apt update & upgrade) …" lxc_exec "apt-get update -qq && apt-get upgrade -y -qq" lxc_exec "apt-get install -y -qq ca-certificates curl gnupg lsb-release wget" success "System aktuell." # ── Docker installieren (offizieller Weg) ───────────────────────────────────── info "Docker Engine installieren …" lxc_exec " install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/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/debian \ \$(. /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." # ── Docker testen ────────────────────────────────────────────────────────────── info "Docker-Funktionstest …" if lxc_exec "docker run --rm hello-world" &>/dev/null; then success "Docker läuft korrekt." else 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 # ── Proxmox Hinweise-Panel setzen ───────────────────────────────────────────── info "Setze Proxmox-Hinweise (Notes) für Container $CTID …" # IP ermitteln (kurz nach Start warten) sleep 2 NOTES_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}' || echo "") [[ -z "$NOTES_IP" ]] && NOTES_IP="(IP via DHCP – siehe Konsole)" # Debian-Logo SVG (inline Base64 als data-URI vermeiden → direkte SVG-Strings) DEBIAN_SVG='' DOCKER_SVG='' # Portainer-URL if $INSTALL_PORTAINER; then PORTAINER_URL="https://${NOTES_IP}:${PORTAINER_PORT}" else PORTAINER_URL="" fi # SSH-Zeile SSH_LINE="" if $ENABLE_ROOT_SSH; then if [[ "$SSH_PORT" == "22" ]]; then SSH_LINE="ssh root@${NOTES_IP}" else SSH_LINE="ssh root@${NOTES_IP} -p ${SSH_PORT}" fi fi # HTML für den Proxmox Notes-Bereich zusammenbauen NOTE_CONTENT="
" NOTE_CONTENT+="
" NOTE_CONTENT+="${DEBIAN_SVG}" NOTE_CONTENT+="${DOCKER_SVG}" NOTE_CONTENT+="
" NOTE_CONTENT+="
🌐 IP-Adresse: ${NOTES_IP}
" if $INSTALL_PORTAINER; then NOTE_CONTENT+="
🐳 Portainer: ${PORTAINER_URL}
" fi if $ENABLE_ROOT_SSH; then NOTE_CONTENT+="
🔑 SSH: ${SSH_LINE}
" fi NOTE_CONTENT+="
" NOTE_CONTENT+="Installiert mit gitea.vourx.com" NOTE_CONTENT+="  |  Debian Docs" NOTE_CONTENT+="  |  Docker Docs" NOTE_CONTENT+="
" # In Proxmox als Container-Description (Hinweise) setzen pvesh set /nodes/$(hostname)/lxc/${CTID}/config --description "${NOTE_CONTENT}" 2>/dev/null \ && success "Proxmox-Hinweise gesetzt (sichtbar unter Container → Hinweise)." \ || warn "Hinweise konnten nicht automatisch gesetzt werden." # ── SSH vollständig konfigurieren ───────────────────────────────────────────── if $ENABLE_ROOT_SSH; then info "Installiere und konfiguriere SSH-Server …" # openssh-server sicherstellen (Debian Minimal hat es evtl. nicht) lxc_exec "apt-get install -y -qq openssh-server" # sshd_config: Root-Login und Port setzen 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 " # Authentifizierungsmethode konfigurieren if [[ \"\$SSH_AUTH\" == \"password\" ]]; then lxc_exec " sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config grep -q '^PasswordAuthentication' /etc/ssh/sshd_config || echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config sed -i 's/^#*PubkeyAuthentication.*/PubkeyAuthentication no/' /etc/ssh/sshd_config " elif [[ \"\$SSH_AUTH\" == \"key\" ]]; then lxc_exec " sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config sed -i 's/^#*PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config grep -q '^PubkeyAuthentication' /etc/ssh/sshd_config || echo 'PubkeyAuthentication yes' >> /etc/ssh/sshd_config mkdir -p /root/.ssh && chmod 700 /root/.ssh echo '${SSH_PUBKEY}' >> /root/.ssh/authorized_keys chmod 600 /root/.ssh/authorized_keys " else # both lxc_exec " sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config grep -q '^PasswordAuthentication' /etc/ssh/sshd_config || echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config sed -i 's/^#*PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config grep -q '^PubkeyAuthentication' /etc/ssh/sshd_config || echo 'PubkeyAuthentication yes' >> /etc/ssh/sshd_config mkdir -p /root/.ssh && chmod 700 /root/.ssh echo '${SSH_PUBKEY}' >> /root/.ssh/authorized_keys chmod 600 /root/.ssh/authorized_keys " fi # SSH-Dienst aktivieren & (neu-)starten lxc_exec "systemctl enable ssh --now && systemctl restart ssh" # Verbindungstest vom PVE-Host (nc prüft ob Port offen ist) info "Prüfe SSH-Erreichbarkeit auf Port ${SSH_PORT} …" 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} -p ${SSH_PORT}" else warn "SSH-Port noch nicht erreichbar – evtl. kurz warten und erneut testen." fi fi # ── 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} %-18s ${BOLD}%s${NC}\n" "Container-ID:" "$CTID" printf "${GREEN}║${NC} %-18s ${BOLD}%s${NC}\n" "Hostname:" "$HOSTNAME" printf "${GREEN}║${NC} %-18s ${BOLD}%s${NC}\n" "IP-Adresse:" "$CONTAINER_IP" echo -e "${GREEN}║${NC}" printf "${GREEN}║${NC} %-18s ${BOLD}%s${NC}\n" "Docker:" "installiert & aktiv" if $INSTALL_PORTAINER; then printf "${GREEN}║${NC} %-18s ${BOLD}https://%s:%s${NC}\n" "Portainer UI:" "$CONTAINER_IP" "$PORTAINER_PORT" fi if $ENABLE_ROOT_SSH; then echo -e "${GREEN}║${NC}" echo -e "${GREEN}║${NC} ${CYAN}── SSH-Verbindung ─────────────────────────────────${NC}" if [[ "$SSH_PORT" == "22" ]]; then printf "${GREEN}║${NC} ${BOLD}ssh root@%s${NC}\n" "$CONTAINER_IP" else printf "${GREEN}║${NC} ${BOLD}ssh root@%s -p %s${NC}\n" "$CONTAINER_IP" "$SSH_PORT" fi printf "${GREEN}║${NC} %-18s ${BOLD}%s${NC}\n" "Auth-Methode:" "$SSH_AUTH" 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}"