Dotfiles/setup/tools/proxmox-lxc-gen.sh

588 lines
28 KiB
Bash
Executable File

#!/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-<vmid>.sh — runnable pct create command (execute on Proxmox host)
# lxc-<vmid>.conf — ready-to-drop /etc/pve/lxc/ config file
# deploy-guide-<vmid>.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@<container-ip>"\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@<proxmox>:/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@<proxmox>
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@<proxmox>:/tmp/\n' "$OUTPUT_DIR" "$VMID"
printf ' ssh root@<proxmox> "bash /tmp/pct-create-%s.sh"\n' "$VMID"
fi