#!/usr/bin/env bash # proxmox-lxc-gen.sh — TUI wizard for configuring and deploying Proxmox LXC containers. # # Walks through a step-by-step dialog interface covering every relevant LXC # parameter, then writes three files to an output directory: # # pct-create-.sh — runnable pct create command (execute on Proxmox host) # lxc-.conf — ready-to-drop /etc/pve/lxc/ config file # deploy-guide-.txt — step-by-step deployment notes # # Optionally SSHs to the Proxmox host to run pct create directly. # # Requires: dialog (pacman -S dialog / apt install dialog / dnf install dialog) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SHELL_SCRIPTS_DIR="$SCRIPT_DIR/../Setup-shell-4-containers" # ─── Helpers ────────────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m' log() { printf "${GREEN}[+]${NC} %s\n" "$*"; } warn() { printf "${YELLOW}[!]${NC} %s\n" "$*"; } error() { printf "${RED}[✗]${NC} %s\n" "$*" >&2; } info() { printf "${CYAN}[i]${NC} %s\n" "$*"; } section() { printf "\n${BLUE}━━━ %s ━━━${NC}\n" "$*"; } [[ $EUID -eq 0 ]] && { error "Run as a normal user (not root)."; exit 1; } # ─── dialog dependency ──────────────────────────────────────────────────────── if ! command -v dialog &>/dev/null; then warn "dialog not found — attempting install..." if command -v pacman &>/dev/null; then sudo pacman -S --noconfirm dialog elif command -v apt-get &>/dev/null; then sudo apt-get install -y dialog elif command -v dnf &>/dev/null; then sudo dnf install -y dialog else error "Install dialog manually, then re-run."; exit 1 fi fi # ─── dialog theme ───────────────────────────────────────────────────────────── TMP_D="$(mktemp -d)" trap 'rm -rf "$TMP_D"' EXIT BACKTITLE="Proxmox LXC Generator" export DIALOGRC="$TMP_D/dialogrc" cat > "$DIALOGRC" <<'DLRC' use_shadow = ON use_colors = ON screen_color = (BLACK,BLACK,ON) title_color = (CYAN,BLACK,ON) border_color = (CYAN,BLACK,ON) button_active_color = (BLACK,CYAN,ON) button_inactive_color = (WHITE,BLACK,OFF) menubox_color = (WHITE,BLACK,OFF) menubox_border_color = (CYAN,BLACK,ON) item_color = (WHITE,BLACK,OFF) item_selected_color = (BLACK,CYAN,ON) tag_color = (CYAN,BLACK,ON) tag_selected_color = (BLACK,CYAN,ON) check_color = (WHITE,BLACK,OFF) check_selected_color = (BLACK,CYAN,ON) inputbox_color = (WHITE,BLACK,OFF) inputbox_border_color = (CYAN,BLACK,ON) uarrow_color = (CYAN,BLACK,ON) darrow_color = (CYAN,BLACK,ON) DLRC # Dialog wrapper: redirect stdout↔stderr so dialog output is captured. D() { dialog --backtitle "$BACKTITLE" "$@" 3>&1 1>&2 2>&3; } abort() { clear; echo "Aborted."; exit 0; } # ─── Screen 1: Identity ─────────────────────────────────────────────────────── IDENTITY=$(D --title " Container Identity " \ --form "\nSet the container ID, hostname, and optional metadata." \ 14 66 4 \ "Container ID (VMID):" 1 1 "100" 1 26 8 9 \ "Hostname:" 2 1 "" 2 26 32 253 \ "Description (optional):" 3 1 "" 3 26 36 512 \ "Tags (comma-separated):" 4 1 "" 4 26 36 512 \ ) || abort VMID=$( awk 'NR==1' <<< "$IDENTITY") CT_HOSTNAME=$(awk 'NR==2' <<< "$IDENTITY") CT_DESC=$( awk 'NR==3' <<< "$IDENTITY") CT_TAGS=$( awk 'NR==4' <<< "$IDENTITY") [[ -z "$VMID" || ! "$VMID" =~ ^[0-9]{1,9}$ ]] && { error "VMID must be a positive integer (got: '${VMID}')."; exit 1; } [[ -z "$CT_HOSTNAME" ]] && { error "Hostname is required."; exit 1; } # ─── Screen 2: OS / Template ────────────────────────────────────────────────── TMPL_DISTRO=$(D --title " OS / Template " \ --menu "\nSelect the base distribution.\nThe matching CT template will be located via pveam on Proxmox." \ 20 66 9 \ "debian" "Debian 12 Bookworm — stable, recommended default" \ "ubuntu" "Ubuntu 24.04 LTS — popular, snap support" \ "alpine" "Alpine Linux — minimal, musl, ~10 MB" \ "arch" "Arch Linux — rolling release" \ "fedora" "Fedora — recent packages, rpm" \ "rocky" "Rocky Linux 9 — RHEL-compatible enterprise" \ "centos" "CentOS Stream 9 — RHEL upstream" \ "void" "Void Linux — runit init, musl/glibc" \ "custom" "Custom template — enter filename manually" \ ) || abort case "$TMPL_DISTRO" in debian) CT_OSTYPE="debian"; TMPL_PATTERN="debian-12" ;; ubuntu) CT_OSTYPE="ubuntu"; TMPL_PATTERN="ubuntu-24.04" ;; alpine) CT_OSTYPE="alpine"; TMPL_PATTERN="alpine-3" ;; arch) CT_OSTYPE="archlinux"; TMPL_PATTERN="archlinux-base" ;; fedora) CT_OSTYPE="fedora"; TMPL_PATTERN="fedora-" ;; rocky) CT_OSTYPE="centos"; TMPL_PATTERN="rockylinux-9" ;; centos) CT_OSTYPE="centos"; TMPL_PATTERN="centos-9-stream" ;; void) CT_OSTYPE="unmanaged"; TMPL_PATTERN="voidlinux-" ;; custom) CT_OSTYPE="unmanaged"; TMPL_PATTERN="" ;; esac if [[ "$TMPL_DISTRO" == "custom" ]]; then CT_TEMPLATE=$(D --title " Custom Template " \ --inputbox \ "Enter the exact template filename as shown by pveam available. Example: debian-12-standard_12.7-1_amd64.tar.zst" \ 10 72 "") || abort [[ -z "$CT_TEMPLATE" ]] && { error "Template filename is required."; exit 1; } else CT_TEMPLATE="$TMPL_PATTERN" # resolved to the full name on Proxmox at deploy time fi # ─── Screen 3: Resources ────────────────────────────────────────────────────── RESOURCES=$(D --title " Resources " \ --form "\nAllocate compute and storage resources for the container." \ 15 66 5 \ "Memory (MB):" 1 1 "512" 1 22 10 10 \ "Swap (MB):" 2 1 "512" 2 22 10 10 \ "CPU cores:" 3 1 "1" 3 22 5 4 \ "Disk size (GB):" 4 1 "8" 4 22 10 10 \ "Rootfs storage:" 5 1 "local-lvm" 5 22 24 64 \ ) || abort CT_MEMORY=$(awk 'NR==1' <<< "$RESOURCES") CT_SWAP=$( awk 'NR==2' <<< "$RESOURCES") CT_CORES=$( awk 'NR==3' <<< "$RESOURCES") CT_DISK=$( awk 'NR==4' <<< "$RESOURCES") CT_STORAGE=$(awk 'NR==5' <<< "$RESOURCES") for _f in CT_MEMORY CT_SWAP CT_CORES CT_DISK; do [[ "${!_f}" =~ ^[0-9]+$ ]] || { error "$_f must be a number (got: '${!_f}')."; exit 1; } done # ─── Screen 4: Network ──────────────────────────────────────────────────────── CT_NET_MODE=$(D --title " Network — IP Assignment " \ --radiolist "\nChoose how the container obtains its IP address:" \ 10 56 2 \ "dhcp" "DHCP (automatic — recommended for most setups)" ON \ "static" "Static IP address (you specify CIDR + gateway)" OFF \ ) || abort CT_IP="" CT_GW="" CT_DNS="8.8.8.8" CT_SEARCHDOMAIN="" if [[ "$CT_NET_MODE" == "static" ]]; then STATIC=$(D --title " Static Network " \ --form "\nEnter static network settings for this container." \ 14 68 4 \ "IP address (CIDR):" 1 1 "192.168.1.100/24" 1 24 22 18 \ "Gateway:" 2 1 "192.168.1.1" 2 24 16 15 \ "DNS server:" 3 1 "8.8.8.8" 3 24 16 15 \ "Search domain:" 4 1 "" 4 24 36 255 \ ) || abort CT_IP=$( awk 'NR==1' <<< "$STATIC") CT_GW=$( awk 'NR==2' <<< "$STATIC") CT_DNS=$( awk 'NR==3' <<< "$STATIC") CT_SEARCHDOMAIN=$(awk 'NR==4' <<< "$STATIC") fi NET_ADV=$(D --title " Network — Interface " \ --form "\nConfigure the virtual NIC attached to this container." \ 12 60 3 \ "Bridge:" 1 1 "vmbr0" 1 18 16 16 \ "VLAN tag (blank=none):" 2 1 "" 2 18 6 4 \ "Enable firewall [0/1]:" 3 1 "1" 3 18 2 1 \ ) || abort CT_BRIDGE=$( awk 'NR==1' <<< "$NET_ADV") CT_VLAN=$( awk 'NR==2' <<< "$NET_ADV") CT_FIREWALL=$(awk 'NR==3' <<< "$NET_ADV") [[ "$CT_FIREWALL" != "1" ]] && CT_FIREWALL=0 # ─── Screen 5: Security ─────────────────────────────────────────────────────── UNPRIV=$(D --title " Privilege Level " \ --radiolist \ "\nUnprivileged containers map UIDs to avoid host root exposure (recommended). Privileged containers share the host UID namespace — needed for some services (e.g. FreeIPA, NFS server) but should only be used when necessary." \ 13 72 2 \ "unprivileged" "Unprivileged — UID remapping, recommended" ON \ "privileged" "Privileged — full host UIDs, reduced isolation" OFF \ ) || abort [[ "$UNPRIV" == "unprivileged" ]] && CT_UNPRIVILEGED=1 || CT_UNPRIVILEGED=0 FEATURES_RAW=$(D --title " LXC Features " \ --separate-output \ --checklist \ "\nSelect kernel features to enable for this container. Use Space to toggle, Enter to confirm." \ 18 72 7 \ "nesting" "nesting=1 — run Docker or nested LXC inside this container" OFF \ "fuse" "fuse=1 — allow FUSE mounts (sshfs, rclone, etc.)" OFF \ "keyctl" "keyctl=1 — Linux keyring API (required by some services)" OFF \ "mounts" "mounts=fuse — permit FUSE bind-mounts" OFF \ "apparmor" "Unconfined AppArmor profile (privileged workaround)" OFF \ "cgroup" "cgroup:rw mounts (needed by systemd inside container)" OFF \ "seccomp" "seccomp=0 — disable seccomp filtering (last resort only)" OFF \ ) || true # OK if nothing selected CT_FEATURES="" CT_APPARMOR=0 CT_CGROUP=0 CT_SECCOMP="" while IFS= read -r _feat; do [[ -z "$_feat" ]] && continue case "$_feat" in nesting) CT_FEATURES+="nesting=1," ;; fuse) CT_FEATURES+="fuse=1," ;; keyctl) CT_FEATURES+="keyctl=1," ;; mounts) CT_FEATURES+="mounts=fuse," ;; apparmor) CT_APPARMOR=1 ;; cgroup) CT_CGROUP=1 ;; seccomp) CT_SECCOMP="lxc.seccomp.profile:" ;; esac done <<< "$FEATURES_RAW" CT_FEATURES="${CT_FEATURES%,}" # ─── Screen 6: Options ──────────────────────────────────────────────────────── # Root password via passwordbox (hidden input), remaining options via form. CT_ROOT_PW=$(D --title " Root Password " \ --insecure \ --passwordbox \ "Enter the root password for this container. Leave blank to disable password auth (SSH key recommended)." \ 10 62 "") || abort OPTIONS=$(D --title " Container Options " \ --form "\nFinal container settings." \ 13 68 4 \ "SSH pubkey file (blank=skip):" 1 1 "" 1 34 30 256 \ "Start on boot [0/1]:" 2 1 "0" 2 34 2 1 \ "Start after creation [y/n]:" 3 1 "n" 3 34 2 1 \ "Console/TTY count:" 4 1 "2" 4 34 3 3 \ ) || abort CT_SSHKEY=$( awk 'NR==1' <<< "$OPTIONS") CT_ONBOOT=$( awk 'NR==2' <<< "$OPTIONS") CT_AUTOSTART=$(awk 'NR==3' <<< "$OPTIONS") CT_TTY=$( awk 'NR==4' <<< "$OPTIONS") [[ "$CT_ONBOOT" != "1" ]] && CT_ONBOOT=0 [[ "${CT_AUTOSTART,,}" == "y"* ]] && CT_AUTOSTART=true || CT_AUTOSTART=false CT_SSHKEY="${CT_SSHKEY/#\~/$HOME}" if [[ -n "$CT_SSHKEY" && ! -f "$CT_SSHKEY" ]]; then warn "SSH key file not found: $CT_SSHKEY (will be ignored)" CT_SSHKEY="" fi # ─── Screen 7: Proxmox target ───────────────────────────────────────────────── PVE_FORM=$(D --title " Proxmox Host " \ --form \ "\nLeave 'Proxmox host' blank to generate output files only (no deployment). SSH target format: root@192.168.1.10 or root@pve.example.com" \ 15 72 4 \ "Proxmox host (blank=local only):" 1 1 "" 1 36 30 255 \ "Template storage (pveam target):" 2 1 "local" 2 36 20 64 \ "Output directory:" 3 1 "$HOME/proxmox-lxc" 3 36 30 255 \ "Proxmox node name:" 4 1 "pve" 4 36 20 64 \ ) || abort PVE_HOST=$( awk 'NR==1' <<< "$PVE_FORM") TMPL_STORAGE=$(awk 'NR==2' <<< "$PVE_FORM") OUTPUT_DIR=$( awk 'NR==3' <<< "$PVE_FORM") PVE_NODE=$( awk 'NR==4' <<< "$PVE_FORM") OUTPUT_DIR="${OUTPUT_DIR/#\~/$HOME}" [[ -z "$TMPL_STORAGE" ]] && TMPL_STORAGE="local" [[ -z "$PVE_NODE" ]] && PVE_NODE="pve" # ─── Assemble net0 string ───────────────────────────────────────────────────── _NET="name=eth0,bridge=${CT_BRIDGE}" [[ -n "$CT_VLAN" && "$CT_VLAN" =~ ^[0-9]+$ ]] && _NET+=",tag=${CT_VLAN}" if [[ "$CT_NET_MODE" == "dhcp" ]]; then _NET+=",ip=dhcp,ip6=auto" else _NET+=",ip=${CT_IP}" [[ -n "$CT_GW" ]] && _NET+=",gw=${CT_GW}" fi [[ "$CT_FIREWALL" == "1" ]] && _NET+=",firewall=1" # ─── Confirm summary ────────────────────────────────────────────────────────── SUMMARY=" VMID : $VMID Hostname : $CT_HOSTNAME Description : ${CT_DESC:-(none)} Tags : ${CT_TAGS:-(none)} OS / template : $TMPL_DISTRO ($CT_TEMPLATE) OS type : $CT_OSTYPE Memory : ${CT_MEMORY} MB Swap : ${CT_SWAP} MB CPU cores : $CT_CORES Disk : ${CT_DISK} GB on $CT_STORAGE Network : $_NET DNS : ${CT_DNS}${CT_SEARCHDOMAIN:+ / $CT_SEARCHDOMAIN} Unprivileged : $([[ "$CT_UNPRIVILEGED" == 1 ]] && echo yes || echo 'NO — privileged') Features : ${CT_FEATURES:-(none)} AppArmor : $([[ "$CT_APPARMOR" == 1 ]] && echo unconfined || echo default) cgroup rw : $([[ "$CT_CGROUP" == 1 ]] && echo yes || echo no) Root PW set : $([[ -n "$CT_ROOT_PW" ]] && echo yes || echo no) SSH pubkey : ${CT_SSHKEY:-(none)} Start on boot : $CT_ONBOOT Auto-start : $CT_AUTOSTART TTY count : $CT_TTY Output dir : $OUTPUT_DIR Proxmox host : ${PVE_HOST:-(local — files only)}" D --title " Configuration Summary " \ --ok-label "Generate" \ --cancel-label "Abort" \ --scrolltext \ --msgbox "$SUMMARY" 38 68 || abort clear # ─── Generate output files ──────────────────────────────────────────────────── mkdir -p "$OUTPUT_DIR" section "Generating output files → $OUTPUT_DIR" # ── pct-create script ───────────────────────────────────────────────────────── PCT_SCRIPT="$OUTPUT_DIR/pct-create-${VMID}.sh" { printf '#!/usr/bin/env bash\n' printf '# pct-create-%s.sh — generated by proxmox-lxc-gen.sh %s\n' "$VMID" "$(date -Iseconds)" printf '# Run on the Proxmox host as root.\n\n' printf 'set -euo pipefail\n\n' if [[ "$TMPL_DISTRO" != "custom" ]]; then printf '# ── Resolve template ──────────────────────────────────────────────────────────\n' printf 'TEMPLATE=$(pveam available --section system 2>/dev/null \\\n' printf ' | awk '"'"'/%s/ {print $2; exit}'"'"')\n' "$CT_TEMPLATE" printf '[[ -z "$TEMPLATE" ]] && TEMPLATE=$(pveam available --section turnkeylinux 2>/dev/null \\\n' printf ' | awk '"'"'/%s/ {print $2; exit}'"'"') || true\n' "$CT_TEMPLATE" printf 'if [[ -z "$TEMPLATE" ]]; then\n' printf ' echo "ERROR: no template matching '"'"'%s'"'"' found." >&2\n' "$CT_TEMPLATE" printf ' echo "Run: pveam update && pveam available --section system" >&2\n' printf ' exit 1\n' printf 'fi\n' printf 'echo "Resolved template: $TEMPLATE"\n\n' printf '# Download template if not already cached\n' printf 'pveam download %s "$TEMPLATE" 2>/dev/null \\\n' "$TMPL_STORAGE" printf ' && echo "Template downloaded." \\\n' printf ' || echo "Template already present (or download failed — continuing)"\n\n' else printf 'TEMPLATE="%s"\n\n' "$CT_TEMPLATE" fi printf '# ── Create the container ──────────────────────────────────────────────────────\n' printf 'pct create %s \\\n' "$VMID" printf ' %s:vztmpl/"$TEMPLATE" \\\n' "$TMPL_STORAGE" printf ' --hostname "%s" \\\n' "$CT_HOSTNAME" printf ' --ostype "%s" \\\n' "$CT_OSTYPE" printf ' --memory %s \\\n' "$CT_MEMORY" printf ' --swap %s \\\n' "$CT_SWAP" printf ' --cores %s \\\n' "$CT_CORES" printf ' --rootfs "%s:%s" \\\n' "$CT_STORAGE" "$CT_DISK" printf ' --net0 "%s" \\\n' "$_NET" [[ -n "$CT_DNS" ]] && printf ' --nameserver "%s" \\\n' "$CT_DNS" [[ -n "$CT_SEARCHDOMAIN" ]] && printf ' --searchdomain "%s" \\\n' "$CT_SEARCHDOMAIN" printf ' --unprivileged %s \\\n' "$CT_UNPRIVILEGED" [[ -n "$CT_FEATURES" ]] && printf ' --features "%s" \\\n' "$CT_FEATURES" printf ' --onboot %s \\\n' "$CT_ONBOOT" printf ' --tty %s \\\n' "$CT_TTY" [[ -n "$CT_ROOT_PW" ]] && printf ' --password "%s" \\\n' "$CT_ROOT_PW" [[ -n "$CT_SSHKEY" ]] && printf ' --ssh-public-keys "%s" \\\n' "$CT_SSHKEY" [[ -n "$CT_DESC" ]] && printf ' --description "%s" \\\n' "${CT_DESC//\"/\\\"}" [[ -n "$CT_TAGS" ]] && printf ' --tags "%s" \\\n' "$CT_TAGS" printf ' --start 0\n\n' if [[ "$CT_APPARMOR" == 1 || "$CT_CGROUP" == 1 || -n "$CT_SECCOMP" ]]; then printf '# ── Apply extra lxc.* options ────────────────────────────────────────────────\n' printf 'CONF="/etc/pve/lxc/%s.conf"\n' "$VMID" [[ "$CT_APPARMOR" == 1 ]] && printf 'printf '"'"'lxc.apparmor.profile: unconfined\nlxc.cap.drop:\n'"'"' >> "$CONF"\n' [[ "$CT_CGROUP" == 1 ]] && printf 'printf '"'"'lxc.mount.auto: proc:rw sys:rw cgroup:rw\nlxc.cgroup2.devices.allow: a\n'"'"' >> "$CONF"\n' [[ -n "$CT_SECCOMP" ]] && printf 'printf '"'"'lxc.seccomp.profile:\n'"'"' >> "$CONF"\n' printf '\n' fi if [[ "$CT_AUTOSTART" == true ]]; then printf '# ── Start the container ──────────────────────────────────────────────────────\n' printf 'pct start %s\n' "$VMID" printf 'echo "Container %s is running."\n' "$VMID" printf 'echo "Access: pct enter %s or ssh root@"\n\n' "$VMID" fi printf 'echo "Done. Container %s created."\n' "$VMID" } > "$PCT_SCRIPT" chmod 755 "$PCT_SCRIPT" log "pct create script: $(basename "$PCT_SCRIPT")" # ── LXC conf file ───────────────────────────────────────────────────────────── LXC_CONF="$OUTPUT_DIR/lxc-${VMID}.conf" { printf '# Generated by proxmox-lxc-gen.sh — %s\n' "$(date -Iseconds)" printf '# Install: cp %s /etc/pve/lxc/%s.conf\n\n' "$(basename "$LXC_CONF")" "$VMID" printf 'arch: amd64\n' printf 'cores: %s\n' "$CT_CORES" printf 'hostname: %s\n' "$CT_HOSTNAME" printf 'memory: %s\n' "$CT_MEMORY" printf 'swap: %s\n' "$CT_SWAP" printf 'net0: %s\n' "$_NET" printf 'ostype: %s\n' "$CT_OSTYPE" printf 'rootfs: %s:vm-%s-disk-0,size=%sG\n' "$CT_STORAGE" "$VMID" "$CT_DISK" printf 'unprivileged: %s\n' "$CT_UNPRIVILEGED" printf 'onboot: %s\n' "$CT_ONBOOT" printf 'tty: %s\n' "$CT_TTY" [[ -n "$CT_FEATURES" ]] && printf 'features: %s\n' "$CT_FEATURES" [[ -n "$CT_DNS" ]] && printf 'nameserver: %s\n' "$CT_DNS" [[ -n "$CT_SEARCHDOMAIN" ]] && printf 'searchdomain: %s\n' "$CT_SEARCHDOMAIN" [[ -n "$CT_TAGS" ]] && printf 'tags: %s\n' "$CT_TAGS" [[ -n "$CT_DESC" ]] && printf 'description: %s\n' "$CT_DESC" if [[ "$CT_APPARMOR" == 1 ]]; then printf '\n# AppArmor override (applied post-create)\n' printf 'lxc.apparmor.profile: unconfined\n' printf 'lxc.cap.drop:\n' fi if [[ "$CT_CGROUP" == 1 ]]; then printf '\n# cgroup access override (required by systemd in containers)\n' printf 'lxc.mount.auto: proc:rw sys:rw cgroup:rw\n' printf 'lxc.cgroup2.devices.allow: a\n' fi [[ -n "$CT_SECCOMP" ]] && printf '\nlxc.seccomp.profile:\n' } > "$LXC_CONF" log "LXC conf: $(basename "$LXC_CONF")" # ── Deploy guide ────────────────────────────────────────────────────────────── GUIDE="$OUTPUT_DIR/deploy-guide-${VMID}.txt" { printf 'Proxmox LXC deploy guide — generated %s ══════════════════════════════════════════════════════════════════════════ Container : %s (VMID %s) OS : %s (%s) Resources : %s core(s), %s MB RAM, %s GB disk on %s Network : %s Privilege : %s ━━━ Step 1 — Upload files to Proxmox ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ scp %s/pct-create-%s.sh root@:/tmp/ ' \ "$(date -Iseconds)" \ "$CT_HOSTNAME" "$VMID" \ "$TMPL_DISTRO" "$CT_OSTYPE" \ "$CT_CORES" "$CT_MEMORY" "$CT_DISK" "$CT_STORAGE" \ "$_NET" \ "$([[ "$CT_UNPRIVILEGED" == 1 ]] && echo unprivileged || echo 'PRIVILEGED')" \ "$OUTPUT_DIR" "$VMID" printf '━━━ Step 2 — Update template catalogue and create container ━━━━━━━━━━━━━ ssh root@ pveam update bash /tmp/pct-create-%s.sh The script resolves the latest matching template for "%s", downloads it if not cached, and calls pct create with all parameters from this wizard. ━━━ Step 3 — Verify ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ pct list pct status %s pct config %s ━━━ Step 4 — Access the container ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # Console (always available): pct start %s pct enter %s ' \ "$VMID" "$CT_TEMPLATE" \ "$VMID" "$VMID" \ "$VMID" "$VMID" if [[ "$CT_NET_MODE" == "static" ]]; then printf ' # SSH (static IP configured):\n ssh root@%s\n\n' "${CT_IP%%/*}" else printf ' # SSH (find DHCP IP via):\n' printf ' pct exec %s -- ip -4 addr show eth0\n\n' "$VMID" fi printf '━━━ Step 5 — Post-install shell setup (optional) ━━━━━━━━━━━━━━━━━━━━━━ # Run inside the container to set up the shell environment: ' case "$TMPL_DISTRO" in debian|ubuntu) printf ' pct exec %s -- bash /path/to/Setup-shell-4-containers/debian.sh\n' "$VMID" ;; arch) printf ' pct exec %s -- bash /path/to/Setup-shell-4-containers/arch.sh\n' "$VMID" ;; alpine) printf ' pct exec %s -- ash /path/to/Setup-shell-4-containers/alpine.sh\n' "$VMID" ;; fedora|rocky|centos) printf ' pct exec %s -- bash /path/to/Setup-shell-4-containers/fedora.sh\n' "$VMID" ;; void) printf ' pct exec %s -- bash /path/to/Setup-shell-4-containers/void.sh\n' "$VMID" ;; *) printf ' # See setup/Setup-shell-4-containers/ for distro-specific scripts\n' ;; esac printf '\n━━━ Notes ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n' [[ "$CT_UNPRIVILEGED" == 0 ]] && printf \ ' PRIVILEGED container — reduced isolation. If services fail with permission errors, verify these are in /etc/pve/lxc/%s.conf: lxc.apparmor.profile: unconfined lxc.cap.drop:\n\n' "$VMID" [[ "$CT_APPARMOR" == 1 ]] && printf \ ' AppArmor profile: unconfined — container has reduced AppArmor isolation. Only use for trusted workloads (FreeIPA, Docker-in-LXC, etc.).\n\n' [[ "$CT_CGROUP" == 1 ]] && printf \ ' cgroup:rw enabled — container has full cgroup access. Required by systemd services that manage their own cgroups.\n\n' [[ -n "$CT_FEATURES" ]] && printf ' LXC features: %s\n\n' "$CT_FEATURES" printf \ ' Useful pct commands: pct set %s --memory 2048 update a setting live pct snapshot %s before-update take a snapshot pct rollback %s before-update revert to snapshot pct destroy %s delete the container (irreversible) pvesh get /nodes/%s/lxc/%s/status/current Generated config: %s Proxmox path: /etc/pve/lxc/%s.conf ' \ "$VMID" "$VMID" "$VMID" "$VMID" \ "$PVE_NODE" "$VMID" \ "$(basename "$LXC_CONF")" "$VMID" } > "$GUIDE" log "Deploy guide: $(basename "$GUIDE")" # ─── Optional: SSH deploy ───────────────────────────────────────────────────── if [[ -n "$PVE_HOST" ]]; then section "Deploying to $PVE_HOST" info "Uploading pct-create-${VMID}.sh..." scp "$PCT_SCRIPT" "${PVE_HOST}:/tmp/pct-create-${VMID}.sh" if [[ -n "$CT_SSHKEY" ]]; then info "Uploading SSH public key..." scp "$CT_SSHKEY" "${PVE_HOST}:/tmp/lxc-sshkey-${VMID}.pub" ssh "$PVE_HOST" \ "sed -i 's|${CT_SSHKEY}|/tmp/lxc-sshkey-${VMID}.pub|' /tmp/pct-create-${VMID}.sh" fi info "Running pct create on $PVE_HOST (this may take a moment)..." if ssh "$PVE_HOST" "bash /tmp/pct-create-${VMID}.sh"; then log "Container $VMID created on $PVE_HOST" else warn "pct create reported an error — check the output above" fi ssh "$PVE_HOST" "rm -f /tmp/pct-create-${VMID}.sh /tmp/lxc-sshkey-${VMID}.pub" 2>/dev/null || true fi # ─── Final summary ──────────────────────────────────────────────────────────── section "Done" printf '\n' info "Output: $OUTPUT_DIR/" printf ' %-36s pct create command\n' "pct-create-${VMID}.sh" printf ' %-36s LXC config (→ /etc/pve/lxc/)\n' "lxc-${VMID}.conf" printf ' %-36s step-by-step notes\n' "deploy-guide-${VMID}.txt" printf '\n' if [[ -z "$PVE_HOST" ]]; then info "To deploy, copy to your Proxmox host and run:" printf ' scp %s/pct-create-%s.sh root@:/tmp/\n' "$OUTPUT_DIR" "$VMID" printf ' ssh root@ "bash /tmp/pct-create-%s.sh"\n' "$VMID" fi