#!/usr/bin/env bash # # docker-opensuse-ct.sh — interactive (whiptail) installer that provisions an # openSUSE LXC container on Proxmox. # # Same UX pattern as opensuse-ct.sh: Default/Advanced menu, whiptail # prompts, then a scripted pct create + pct exec install flow. # # Run this ON the Proxmox host as root: # ./opensuse-ct.sh set -euo pipefail APP="opensuse" # ---------- helpers ---------- msg_info() { echo -e "\e[1;34m[*]\e[0m $1"; } msg_ok() { echo -e "\e[1;32m[OK]\e[0m $1"; } msg_err() { echo -e "\e[1;31m[ERR]\e[0m $1" >&2; } die() { msg_err "$1"; exit 1; } command -v pct >/dev/null 2>&1 || die "pct not found — run this on a Proxmox VE host." command -v whiptail >/dev/null 2>&1 || { msg_info "whiptail not found, installing..."; apt-get update -qq && apt-get install -y whiptail; } # ---------- welcome + mode ---------- whiptail --title "$APP LXC Installer" --msgbox \ "This will create an openSUSE LXC container on this Proxmox host." \ 10 65 MODE=$(whiptail --title "$APP LXC Installer" --menu "Choose setup mode" 12 65 2 \ "default" "Quick install — sane defaults, minimal prompts" \ "advanced" "Advanced — customize every setting" \ 3>&1 1>&2 2>&3) || exit 1 # ---------- gather settings ---------- NEXTID=$(pvesh get /cluster/nextid) if [[ "$MODE" == "default" ]]; then CTID="$NEXTID" CT_HOSTNAME="opensuse" CT_STORAGE=$(pvesm status -content rootdir | awk 'NR==2{print $1}') [[ -n "$CT_STORAGE" ]] || die "No rootdir-capable storage found." DISK_GB=4 CORES=2 MEMORY=1024 SWAP=512 BRIDGE="vmbr0" VLAN="" NET_MODE="dhcp" UNPRIVILEGED=1 ROOT_PASSWORD=$(tr -dc 'A-Za-z0-9' &1 1>&2 2>&3) || exit 1 CT_HOSTNAME=$(whiptail --inputbox "Hostname" 8 58 "opensuse" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 mapfile -t STORAGES < <(pvesm status -content rootdir | awk 'NR>1{print $1}') [[ ${#STORAGES[@]} -gt 0 ]] || die "No rootdir-capable storage found." RADIO_ARGS=(); first=1 for s in "${STORAGES[@]}"; do if [[ $first -eq 1 ]]; then RADIO_ARGS+=("$s" "" ON); first=0; else RADIO_ARGS+=("$s" "" OFF); fi done CT_STORAGE=$(whiptail --radiolist "Storage for rootfs" 15 60 6 "${RADIO_ARGS[@]}" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 DISK_GB=$(whiptail --inputbox "Disk size (GB)" 8 70 "16" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 CORES=$(whiptail --inputbox "CPU cores" 8 58 "2" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 MEMORY=$(whiptail --inputbox "Memory (MB)" 8 58 "2048" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 SWAP=$(whiptail --inputbox "Swap (MB)" 8 58 "512" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 mapfile -t BRIDGES < <(ip -o link show type bridge | awk -F': ' '{print $2}') [[ ${#BRIDGES[@]} -gt 0 ]] || die "No bridges found on this host." RADIO_ARGS=(); first=1 for b in "${BRIDGES[@]}"; do if [[ $first -eq 1 ]]; then RADIO_ARGS+=("$b" "" ON); first=0; else RADIO_ARGS+=("$b" "" OFF); fi done BRIDGE=$(whiptail --radiolist "Network bridge" 15 60 6 "${RADIO_ARGS[@]}" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 VLAN=$(whiptail --inputbox "VLAN tag (blank = untagged)" 8 58 "" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 NET_MODE=$(whiptail --menu "Network mode" 12 58 2 \ "dhcp" "DHCP" \ "static" "Static IP" \ --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 if [[ "$NET_MODE" == "static" ]]; then STATIC_IP_CIDR=$(whiptail --inputbox "IP/CIDR, e.g. 192.168.10.60/24" 8 58 "" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 STATIC_GW=$(whiptail --inputbox "Gateway" 8 58 "" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 fi if whiptail --title "$APP LXC" --yesno \ "Create as unprivileged container?" 12 70; then UNPRIVILEGED=1 else UNPRIVILEGED=0 fi ROOT_PASSWORD=$(whiptail --passwordbox "Root password for the CT" 8 58 --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 SSH_PUBKEY_FILE=$(whiptail --inputbox "SSH public key file to inject (blank = skip)" 8 58 "$HOME" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 fi pct status "$CTID" >/dev/null 2>&1 && die "CT $CTID already exists." # ---------- template ---------- mapfile -t TEMPLATES < <(pveam available -section system | awk '{print $2}' | grep -i opensuse || true) if [[ ${#TEMPLATES[@]} -eq 0 ]]; then msg_info "Refreshing template index..." pveam update mapfile -t TEMPLATES < <(pveam available -section system | awk '{print $2}' | grep -i opensuse || true) fi [[ ${#TEMPLATES[@]} -gt 0 ]] || die "No openSUSE templates found in the Proxmox appliance list." if [[ "$MODE" == "default" ]]; then TEMPLATE="${TEMPLATES[-1]}" # newest listed else RADIO_ARGS=(); first=1 for t in "${TEMPLATES[@]}"; do if [[ $first -eq 1 ]]; then RADIO_ARGS+=("$t" "" ON); first=0; else RADIO_ARGS+=("$t" "" OFF); fi done TEMPLATE=$(whiptail --radiolist "Choose openSUSE template" 20 72 10 "${RADIO_ARGS[@]}" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 fi if ! pveam list local | grep -q "$TEMPLATE"; then msg_info "Downloading template $TEMPLATE..." pveam download local "$TEMPLATE" msg_ok "Template downloaded." fi # ---------- confirm ---------- SUMMARY="CTID: $CTID Hostname: $CT_HOSTNAME Template: $TEMPLATE Storage: $CT_STORAGE (${DISK_GB}G) CPU/RAM: ${CORES} cores / ${MEMORY}MB (+${SWAP}MB swap) Bridge: $BRIDGE${VLAN:+ (vlan $VLAN)} Network: ${NET_MODE}${STATIC_IP_CIDR:+ $STATIC_IP_CIDR via $STATIC_GW} Priv: $([[ $UNPRIVILEGED -eq 1 ]] && echo unprivileged || echo privileged) Features: nesting=1,keyctl=1" whiptail --title "Confirm" --yesno "$SUMMARY Proceed?" 20 65 || exit 1 # ---------- build net string ---------- if [[ "$NET_MODE" == "static" ]]; then NET_STR="name=eth0,bridge=${BRIDGE},ip=${STATIC_IP_CIDR},gw=${STATIC_GW}" else NET_STR="name=eth0,bridge=${BRIDGE},ip=dhcp" fi [[ -n "$VLAN" ]] && NET_STR="${NET_STR},tag=${VLAN}" # ---------- create + start ---------- CREATE_ARGS=( "$CTID" "local:vztmpl/${TEMPLATE}" --hostname "$CT_HOSTNAME" --cores "$CORES" --memory "$MEMORY" --swap "$SWAP" --rootfs "${CT_STORAGE}:${DISK_GB}" --net0 "$NET_STR" --unprivileged "$UNPRIVILEGED" --onboot 1 --features "nesting=1,keyctl=1" --password "$ROOT_PASSWORD" ) [[ -n "${SSH_PUBKEY_FILE:-}" && -f "${SSH_PUBKEY_FILE:-}" ]] && CREATE_ARGS+=(--ssh-public-keys "$SSH_PUBKEY_FILE") msg_info "Creating CT $CTID ($CT_HOSTNAME)..." pct create "${CREATE_ARGS[@]}" >/dev/null msg_ok "Container created." msg_info "Starting CT $CTID..." pct start "$CTID" msg_info "Waiting for network..." for i in {1..30}; do pct exec "$CTID" -- sh -c "true" >/dev/null 2>&1 && pct exec "$CTID" -- ping -c1 -W2 1.1.1.1 >/dev/null 2>&1 && break sleep 2 [[ $i -eq 30 ]] && die "CT never came up with network access." done msg_ok "Network is up." # ---------- install docker inside the CT ---------- run() { pct exec "$CTID" -- bash -c "$1"; } msg_info "Refreshing zypper repos..." run "zypper --non-interactive refresh" >/dev/null msg_ok "Repos refreshed." msg_info "Installing prerequisites (curl)..." run "zypper --non-interactive install curl" >/dev/null msg_ok "Prerequisites installed." msg_info "updating os..." run "zypper --non-interactive up" >/dev/null msg_ok "OS upgraded." msg_info "Install nano and ssh utils..." run "zypper in openssh-clients nano" >/dev/null msg_ok "Tools installed." # ---------- summary ---------- CT_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}') whiptail --title "$APP LXC Installer — Done" --msgbox \ "LXC is running. CT ID: $CTID IP: ${CT_IP} Root pass: ${ROOT_PASSWORD} Enter with: pct enter $CTID (also printed below in the terminal)" 18 65 cat <