Skip to content

Instantly share code, notes, and snippets.

@noralstat
Created February 22, 2026 05:18
Show Gist options
  • Select an option

  • Save noralstat/93cef470d9710c453f79382b79c03b03 to your computer and use it in GitHub Desktop.

Select an option

Save noralstat/93cef470d9710c453f79382b79c03b03 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# iVentoy LXC Installer for Proxmox VE
# Run this from a Proxmox VE host shell as root.
set -euo pipefail
# Defaults
CTID_DEFAULT=$(pvesh get /cluster/nextid 2>/dev/null || echo 200)
HOSTNAME_DEFAULT="iventoy"
DISK_SIZE_DEFAULT="8"
RAM_DEFAULT="1024"
CORES_DEFAULT="2"
BRIDGE_DEFAULT="vmbr0"
TEMPLATE_PREFIX_DEFAULT="debian-13-standard"
# Colors
GN="\033[1;32m"
RD="\033[1;31m"
YW="\033[1;33m"
CY="\033[1;36m"
CL="\033[0m"
info() { echo -e " ${CY}[INFO]${CL} $1"; }
ok() { echo -e " ${GN}[OK]${CL} $1"; }
err() { echo -e " ${RD}[ERROR]${CL} $1"; exit 1; }
warn() { echo -e " ${YW}[WARN]${CL} $1"; }
require_cmd() {
command -v "$1" >/dev/null 2>&1 || err "Missing required command: $1"
}
prompt_default() {
local prompt="$1"
local default_value="$2"
local var_name="$3"
local input
read -r -p " ${prompt} [${default_value}]: " input
printf -v "$var_name" "%s" "${input:-$default_value}"
}
validate_positive_int() {
local value="$1"
local label="$2"
[[ "$value" =~ ^[0-9]+$ ]] || err "$label must be a positive integer."
[[ "$value" -gt 0 ]] || err "$label must be greater than zero."
}
storage_supports_content() {
local storage="$1"
local content="$2"
pvesm status -content "$content" 2>/dev/null | awk 'NR>1 && $3=="active" {print $1}' | grep -Fxq "$storage"
}
latest_template_from_prefix() {
local prefix="$1"
grep -oE "${prefix}[^[:space:]]*" | sort -V | tail -n1 || true
}
info "Validating Proxmox host prerequisites..."
[[ "$(id -u)" -eq 0 ]] || err "Run as root."
require_cmd pct
require_cmd pvesh
require_cmd pvesm
require_cmd pveam
require_cmd awk
require_cmd grep
ok "Host checks passed."
echo ""
echo -e "${GN}===========================================${CL}"
echo -e "${GN} iVentoy LXC Installer for PVE ${CL}"
echo -e "${GN}===========================================${CL}"
echo ""
prompt_default "Container ID" "$CTID_DEFAULT" CTID
[[ "$CTID" =~ ^[0-9]+$ ]] || err "Container ID must be numeric."
pct status "$CTID" >/dev/null 2>&1 && err "CT $CTID already exists. Pick a different ID."
prompt_default "Hostname" "$HOSTNAME_DEFAULT" HOSTNAME
prompt_default "Disk size in GB" "$DISK_SIZE_DEFAULT" DISK_SIZE
prompt_default "RAM in MB" "$RAM_DEFAULT" RAM
prompt_default "CPU cores" "$CORES_DEFAULT" CORES
prompt_default "Network bridge" "$BRIDGE_DEFAULT" BRIDGE
validate_positive_int "$DISK_SIZE" "Disk size"
validate_positive_int "$RAM" "RAM"
validate_positive_int "$CORES" "CPU cores"
[[ -d "/sys/class/net/${BRIDGE}" ]] || err "Network bridge ${BRIDGE} does not exist on the host."
info "Detecting storage..."
ROOTFS_STORAGE_DEFAULT=$(pvesm status -content rootdir 2>/dev/null | awk 'NR>1 && $3=="active" {print $1; exit}')
[[ -n "$ROOTFS_STORAGE_DEFAULT" ]] || err "No active storage with rootdir content found."
prompt_default "Storage for rootfs" "$ROOTFS_STORAGE_DEFAULT" STORAGE
storage_supports_content "$STORAGE" rootdir || err "Storage ${STORAGE} does not support rootdir or is inactive."
TEMPLATE_STORAGE_DEFAULT=$(pvesm status -content vztmpl 2>/dev/null | awk 'NR>1 && $3=="active" {print $1; exit}')
[[ -n "$TEMPLATE_STORAGE_DEFAULT" ]] || err "No active storage with vztmpl content found."
prompt_default "Template storage" "$TEMPLATE_STORAGE_DEFAULT" TEMPLATE_STORAGE
storage_supports_content "$TEMPLATE_STORAGE" vztmpl || err "Storage ${TEMPLATE_STORAGE} does not support vztmpl or is inactive."
TEMPLATE=$(pveam list "$TEMPLATE_STORAGE" 2>/dev/null | latest_template_from_prefix "$TEMPLATE_PREFIX_DEFAULT")
if [[ -z "$TEMPLATE" ]]; then
info "Template ${TEMPLATE_PREFIX_DEFAULT} not found locally. Refreshing template list..."
pveam update >/dev/null
TEMPLATE=$(pveam available --section system 2>/dev/null | latest_template_from_prefix "$TEMPLATE_PREFIX_DEFAULT")
fi
if [[ -z "$TEMPLATE" ]]; then
warn "Debian 13 template not found. Falling back to latest Debian standard template."
TEMPLATE=$(pveam available --section system 2>/dev/null | grep -oE "debian-[0-9]+-standard[^[:space:]]*" | sort -V | tail -n1 || true)
fi
[[ -n "$TEMPLATE" ]] || err "Could not find a Debian standard LXC template."
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -Fq "$TEMPLATE"; then
info "Downloading template ${TEMPLATE} to ${TEMPLATE_STORAGE}..."
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null
fi
ok "Template: $TEMPLATE"
info "Creating CT $CTID ($HOSTNAME)..."
pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" \
--hostname "$HOSTNAME" \
--ostype debian \
--memory "$RAM" \
--cores "$CORES" \
--rootfs "${STORAGE}:${DISK_SIZE}" \
--net0 "name=eth0,bridge=${BRIDGE},ip=dhcp" \
--unprivileged 0 \
--features "nesting=1,keyctl=1" \
--onboot 1 \
--start 0
ok "Container $CTID created."
info "Starting CT $CTID..."
pct start "$CTID"
sleep 3
info "Installing iVentoy inside CT $CTID..."
pct exec "$CTID" -- bash -s <<'IN_CONTAINER'
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
for attempt in 1 2 3 4 5; do
if apt-get update -qq >/dev/null 2>&1; then
break
fi
if [ "$attempt" -eq 5 ]; then
echo "ERROR: apt-get update failed after multiple attempts."
exit 1
fi
sleep 2
done
apt-get install -y -qq ca-certificates curl tar >/dev/null 2>&1
RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/ventoy/PXE/releases/latest)
RELEASE=$(printf '%s\n' "$RELEASE_JSON" | grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"v?[^"]+"' | head -n1 | cut -d'"' -f4 | sed 's/^v//')
ASSET=$(printf '%s\n' "$RELEASE_JSON" | grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"]*linux-free\.tar\.gz"' | head -n1 | cut -d'"' -f4)
if [ -z "$RELEASE" ]; then
echo "ERROR: Could not determine latest iVentoy release."
exit 1
fi
if [ -z "$ASSET" ]; then
ASSET="iventoy-${RELEASE}-linux-free.tar.gz"
fi
echo "Installing iVentoy v${RELEASE}..."
TMP_ARCHIVE="/tmp/iventoy.tar.gz"
if ! curl -fsSL "https://github.com/ventoy/PXE/releases/download/v${RELEASE}/${ASSET}" -o "$TMP_ARCHIVE"; then
curl -fsSL "https://github.com/ventoy/PXE/releases/download/${RELEASE}/${ASSET}" -o "$TMP_ARCHIVE"
fi
TOP_DIR=$(tar -tf "$TMP_ARCHIVE" | head -n1 | cut -d/ -f1)
[ -n "$TOP_DIR" ] || { echo "ERROR: Unexpected iVentoy archive layout."; exit 1; }
mkdir -p /opt/iventoy /opt/iventoy/iso
tar -xzf "$TMP_ARCHIVE" -C /tmp
cp -a "/tmp/${TOP_DIR}/." /opt/iventoy/
rm -rf "$TMP_ARCHIVE" "/tmp/${TOP_DIR}"
chmod +x /opt/iventoy/iventoy.sh
cat > /etc/systemd/system/iventoy.service <<'SERVICE_EOF'
[Unit]
Description=iVentoy PXE Service
Documentation=https://www.iventoy.com
Wants=network-online.target
After=network-online.target
[Service]
Type=forking
WorkingDirectory=/opt/iventoy
Environment=IVENTOY_API_ALL=1
Environment=IVENTOY_AUTO_RUN=1
Environment=LIBRARY_PATH=/opt/iventoy/lib/lin64
Environment=LD_LIBRARY_PATH=/opt/iventoy/lib/lin64
ExecStart=/opt/iventoy/iventoy.sh -R start
ExecStop=/opt/iventoy/iventoy.sh -R stop
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
SERVICE_EOF
systemctl daemon-reload
systemctl enable --now iventoy >/dev/null 2>&1
systemctl is-active --quiet iventoy || { echo "ERROR: iVentoy service is not active."; exit 1; }
echo "iVentoy service started successfully."
IN_CONTAINER
ok "iVentoy installed."
sleep 2
IP=$(pct exec "$CTID" -- bash -lc "hostname -I 2>/dev/null | awk '{print \$1}'" || true)
echo ""
echo -e "${GN}===========================================${CL}"
echo -e "${GN} Installation Complete ${CL}"
echo -e "${GN}===========================================${CL}"
echo ""
echo -e " ${CY}Container ID:${CL} $CTID"
echo -e " ${CY}Hostname:${CL} $HOSTNAME"
if [[ -n "${IP:-}" ]]; then
echo -e " ${CY}Web UI:${CL} ${GN}http://${IP}:26000${CL}"
else
echo -e " ${CY}Web UI:${CL} ${YW}CT IP not detected yet. Check with: pct exec $CTID -- hostname -I${CL}"
fi
echo ""
echo -e " ${YW}ISO path in CT:${CL} /opt/iventoy/iso/"
echo -e " ${YW}Example push:${CL} pct push $CTID myfile.iso /opt/iventoy/iso/myfile.iso"
echo ""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment