Initial commit
This commit is contained in:
284
deploy/proxmox-ghostgrid.sh
Normal file
284
deploy/proxmox-ghostgrid.sh
Normal file
@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# GhostGrid - Proxmox LXC Installer
|
||||
#
|
||||
# Run on the Proxmox host as root:
|
||||
# bash proxmox-ghostgrid.sh
|
||||
#
|
||||
# Creates an unprivileged Debian 12 LXC, installs Node.js 20 and Caddy,
|
||||
# deploys two GhostGrid instances and exposes them through Caddy with
|
||||
# internally generated TLS certificates:
|
||||
# main -> ghostgrid.service -> 127.0.0.1:3000 -> https://<main-dns>
|
||||
# dev -> ghostgrid-dev.service -> 127.0.0.1:3001 -> https://<dev-dns>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Defaults ───────────────────────────────────────────────────────────
|
||||
APP="GhostGrid"
|
||||
REPO_URL_DEFAULT="https://git.airit.rocks/jbrueckner/GhostGrid.git"
|
||||
REPO_BRANCH="main"
|
||||
DEV_BRANCH="dev"
|
||||
HOSTNAME_DEFAULT="ghostgrid"
|
||||
DISK_DEFAULT="8"
|
||||
CPU_DEFAULT="2"
|
||||
RAM_DEFAULT="1024"
|
||||
BRIDGE_DEFAULT="vmbr0"
|
||||
ROOTFS_STORAGE_DEFAULT="local-lvm"
|
||||
TEMPLATE_STORAGE_DEFAULT="local"
|
||||
APP_PORT="3000"
|
||||
DEV_PORT="3001"
|
||||
APP_DIR="/opt/ghostgrid"
|
||||
DEV_DIR="/opt/ghostgrid-dev"
|
||||
PROD_DNS_DEFAULT="ghostgrid.local"
|
||||
DEV_DNS_DEFAULT="ghostgrid-dev.local"
|
||||
DNS_RESOLVER_DEFAULT="1.1.1.1"
|
||||
|
||||
# ── Output helpers ─────────────────────────────────────────────────────
|
||||
YW=$'\033[33m'; GN=$'\033[1;92m'; RD=$'\033[01;31m'; BL=$'\033[36m'; CL=$'\033[m'
|
||||
CM="${GN}✔${CL}"; CROSS="${RD}✗${CL}"; INFO="${BL}ℹ${CL}"
|
||||
msg_info() { echo -e " ${YW}➤${CL} $1"; }
|
||||
msg_ok() { echo -e " ${CM} $1"; }
|
||||
msg_err() { echo -e " ${CROSS} ${RD}$1${CL}"; }
|
||||
header() {
|
||||
echo -e "${BL}"
|
||||
echo " ____ _ _ ____ _ _ "
|
||||
echo " / ___| |__ ___ ___| |_ / ___|_ __(_) __| |"
|
||||
echo " | | _| '_ \\ / _ \\/ __| __| | _| '__| |/ _\` |"
|
||||
echo " | |_| | | | | (_) \\__ \\ |_| |_| | | | | (_| |"
|
||||
echo " \\____|_| |_|\\___/|___/\\__|\\____|_| |_|\\__,_|"
|
||||
echo -e " Proxmox LXC Installer${CL}\n"
|
||||
}
|
||||
|
||||
trap 'msg_err "Abort in line $LINENO."; exit 1' ERR
|
||||
|
||||
ask_input() {
|
||||
local title="$1"
|
||||
local prompt="$2"
|
||||
local default_value="${3:-}"
|
||||
whiptail --inputbox "$prompt" 8 72 "$default_value" --title "$title" 3>&1 1>&2 2>&3
|
||||
}
|
||||
|
||||
ask_password() {
|
||||
local title="$1"
|
||||
local prompt="$2"
|
||||
whiptail --passwordbox "$prompt" 9 72 "" --title "$title" 3>&1 1>&2 2>&3 || true
|
||||
}
|
||||
|
||||
require_value() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
[[ -n "$value" ]] || { msg_err "$name must not be empty."; exit 1; }
|
||||
}
|
||||
|
||||
# ── Prerequisites ──────────────────────────────────────────────────────
|
||||
header
|
||||
[[ "$(id -u)" -eq 0 ]] || { msg_err "Please run this script as root on the Proxmox host."; exit 1; }
|
||||
command -v pct >/dev/null 2>&1 || { msg_err "'pct' was not found. Are you running this on a Proxmox VE host?"; exit 1; }
|
||||
command -v whiptail >/dev/null 2>&1 || { msg_err "'whiptail' was not found."; exit 1; }
|
||||
|
||||
# ── Interactive configuration ──────────────────────────────────────────
|
||||
NEXTID="$(pvesh get /cluster/nextid 2>/dev/null || echo 100)"
|
||||
|
||||
CTID=$(ask_input "$APP - LXC" "Container-ID (CTID)" "$NEXTID")
|
||||
CT_HOSTNAME=$(ask_input "$APP - LXC" "LXC hostname" "$HOSTNAME_DEFAULT")
|
||||
CPU=$(ask_input "$APP - Resources" "vCPU cores" "$CPU_DEFAULT")
|
||||
RAM=$(ask_input "$APP - Resources" "RAM in MB" "$RAM_DEFAULT")
|
||||
DISK=$(ask_input "$APP - Resources" "Disk in GB" "$DISK_DEFAULT")
|
||||
ROOTFS_STORAGE=$(ask_input "$APP - Storage" "rootfs-Storage" "$ROOTFS_STORAGE_DEFAULT")
|
||||
TEMPLATE_STORAGE=$(ask_input "$APP - Storage" "Template-Storage" "$TEMPLATE_STORAGE_DEFAULT")
|
||||
BRIDGE=$(ask_input "$APP - Network" "Network bridge" "$BRIDGE_DEFAULT")
|
||||
|
||||
if whiptail --title "$APP - Network" --yesno "Should the container use DHCP?\n\nNo = configure a static IP address during installation." 10 72; then
|
||||
NET="dhcp"
|
||||
DNS_RESOLVER=""
|
||||
else
|
||||
NET="static"
|
||||
IPADDR=$(ask_input "$APP - Network" "Static IP in CIDR notation, for example 172.16.66.50/24" "")
|
||||
GATEWAY=$(ask_input "$APP - Network" "Gateway, for example 172.16.66.1" "")
|
||||
DNS_RESOLVER=$(ask_input "$APP - Network" "DNS resolver for the container" "$DNS_RESOLVER_DEFAULT")
|
||||
require_value "Static IP" "$IPADDR"
|
||||
require_value "Gateway" "$GATEWAY"
|
||||
fi
|
||||
|
||||
PROD_DNS=$(ask_input "$APP - Caddy" "DNS name for main / production" "$PROD_DNS_DEFAULT")
|
||||
DEV_DNS=$(ask_input "$APP - Caddy" "DNS name for dev / staging" "$DEV_DNS_DEFAULT")
|
||||
require_value "Production DNS-Name" "$PROD_DNS"
|
||||
require_value "Development DNS-Name" "$DEV_DNS"
|
||||
|
||||
if [[ "$PROD_DNS" == "$DEV_DNS" ]]; then
|
||||
msg_err "Production and development DNS names must not be identical."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_URL=$(ask_input "$APP - Git" "Git-Repository (HTTPS)" "$REPO_URL_DEFAULT")
|
||||
ACCESS_TOKEN=$(ask_password "$APP - Git" "Gitea read-only access token\nLeave empty if the repository is public.")
|
||||
ROOT_PW=$(ask_password "$APP - LXC" "Root password for the container\nEmpty = no password; access only through 'pct enter ${CTID}'.")
|
||||
require_value "Git-Repository" "$REPO_URL"
|
||||
|
||||
# ── Ensure template ────────────────────────────────────────────────────
|
||||
msg_info "Updating template list"
|
||||
pveam update >/dev/null 2>&1 || true
|
||||
TEMPLATE="$(pveam available --section system 2>/dev/null | awk '/debian-12-standard/{print $2}' | sort -V | tail -n1)"
|
||||
[[ -n "$TEMPLATE" ]] || { msg_err "No Debian 12 standard template found."; exit 1; }
|
||||
|
||||
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
|
||||
msg_info "Downloading template $TEMPLATE"
|
||||
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null
|
||||
fi
|
||||
msg_ok "Template ready: $TEMPLATE"
|
||||
|
||||
# ── Create container ───────────────────────────────────────────────────
|
||||
if [[ "$NET" == "dhcp" ]]; then
|
||||
NETCONF="name=eth0,bridge=${BRIDGE},ip=dhcp"
|
||||
else
|
||||
NETCONF="name=eth0,bridge=${BRIDGE},ip=${IPADDR},gw=${GATEWAY}"
|
||||
fi
|
||||
|
||||
msg_info "Creating LXC $CTID ($CT_HOSTNAME)"
|
||||
CREATE_OPTS=(
|
||||
--hostname "$CT_HOSTNAME"
|
||||
--cores "$CPU"
|
||||
--memory "$RAM"
|
||||
--rootfs "${ROOTFS_STORAGE}:${DISK}"
|
||||
--net0 "$NETCONF"
|
||||
--unprivileged 1
|
||||
--features keyctl=1,nesting=1
|
||||
--onboot 1
|
||||
)
|
||||
[[ -n "$ROOT_PW" ]] && CREATE_OPTS+=(--password "$ROOT_PW")
|
||||
[[ "$NET" == "static" && -n "$DNS_RESOLVER" ]] && CREATE_OPTS+=(--nameserver "$DNS_RESOLVER")
|
||||
|
||||
pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${CREATE_OPTS[@]}" >/dev/null
|
||||
msg_ok "Container created"
|
||||
|
||||
msg_info "Starting container"
|
||||
pct start "$CTID" >/dev/null
|
||||
|
||||
# Wait for basic network/DNS availability.
|
||||
for _ in $(seq 1 45); do
|
||||
pct exec "$CTID" -- getent hosts deb.nodesource.com >/dev/null 2>&1 && break
|
||||
sleep 2
|
||||
done
|
||||
msg_ok "Container is running"
|
||||
|
||||
# ── In-container helper ────────────────────────────────────────────────
|
||||
run() { pct exec "$CTID" -- bash -c "export LANG=C.UTF-8 LC_ALL=C.UTF-8; $1"; }
|
||||
|
||||
msg_info "Installing base packages, Node.js 20 LTS, and Caddy"
|
||||
run "export DEBIAN_FRONTEND=noninteractive; apt-get update -qq && apt-get install -y -qq curl git ca-certificates build-essential python3 sudo gnupg debian-keyring debian-archive-keyring apt-transport-https caddy >/dev/null"
|
||||
run "curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null 2>&1 && apt-get install -y -qq nodejs >/dev/null"
|
||||
msg_ok "Node.js $(run 'node --version') installed"
|
||||
msg_ok "Caddy installed"
|
||||
|
||||
msg_info "Creating service user and cloning both branches"
|
||||
run "id ghostgrid >/dev/null 2>&1 || useradd --system --home-dir ${APP_DIR} --shell /usr/sbin/nologin ghostgrid"
|
||||
run "mkdir -p ${APP_DIR} ${DEV_DIR} && chown ghostgrid:ghostgrid ${APP_DIR} ${DEV_DIR}"
|
||||
|
||||
if [[ -n "$ACCESS_TOKEN" ]]; then
|
||||
CLONE_URL="$(echo "$REPO_URL" | sed -E "s#https://#https://oauth2:${ACCESS_TOKEN}@#")"
|
||||
else
|
||||
CLONE_URL="$REPO_URL"
|
||||
fi
|
||||
|
||||
run "sudo -u ghostgrid git clone --branch ${REPO_BRANCH} '${CLONE_URL}' ${APP_DIR}"
|
||||
run "sudo -u ghostgrid git clone --branch ${DEV_BRANCH} '${CLONE_URL}' ${DEV_DIR}"
|
||||
run "chmod 600 ${APP_DIR}/.git/config ${DEV_DIR}/.git/config"
|
||||
msg_ok "Repositories cloned (main + dev)"
|
||||
|
||||
msg_info "Creating .env file for each instance"
|
||||
for d in "${APP_DIR}" "${DEV_DIR}"; do
|
||||
SECRET="$(openssl rand -hex 32)"
|
||||
run "printf 'JWT_SECRET=\"%s\"\n' '${SECRET}' > ${d}/.env && chown ghostgrid:ghostgrid ${d}/.env && chmod 600 ${d}/.env"
|
||||
done
|
||||
msg_ok ".env files created (main + dev)"
|
||||
|
||||
msg_info "Installing dependencies and building both instances"
|
||||
for d in "${APP_DIR}" "${DEV_DIR}"; do
|
||||
run "cd ${d} && sudo -u ghostgrid npm ci --no-audit --no-fund >/dev/null 2>&1 && sudo -u ghostgrid npm run build >/dev/null 2>&1"
|
||||
done
|
||||
msg_ok "Build completed (main + dev)"
|
||||
|
||||
msg_info "Configuring systemd services"
|
||||
run "cp ${APP_DIR}/deploy/ghostgrid.service /etc/systemd/system/ghostgrid.service"
|
||||
run "cp ${DEV_DIR}/deploy/ghostgrid-dev.service /etc/systemd/system/ghostgrid-dev.service"
|
||||
run "printf 'ghostgrid ALL=(root) NOPASSWD: /usr/bin/systemctl restart ghostgrid, /usr/bin/systemctl restart ghostgrid-dev\n' > /etc/sudoers.d/ghostgrid && chmod 440 /etc/sudoers.d/ghostgrid"
|
||||
run "systemctl daemon-reload && systemctl enable --now ghostgrid ghostgrid-dev >/dev/null 2>&1"
|
||||
msg_ok "Services are active (ghostgrid + ghostgrid-dev)"
|
||||
|
||||
msg_info "Configuring Caddy with internally generated TLS certificates"
|
||||
run "cat > /etc/caddy/Caddyfile <<'CADDYEOF'
|
||||
{
|
||||
local_certs
|
||||
}
|
||||
|
||||
${PROD_DNS} {
|
||||
encode zstd gzip
|
||||
tls internal
|
||||
reverse_proxy 127.0.0.1:${APP_PORT}
|
||||
}
|
||||
|
||||
${DEV_DNS} {
|
||||
encode zstd gzip
|
||||
tls internal
|
||||
reverse_proxy 127.0.0.1:${DEV_PORT}
|
||||
}
|
||||
CADDYEOF
|
||||
caddy fmt --overwrite /etc/caddy/Caddyfile >/dev/null
|
||||
caddy validate --config /etc/caddy/Caddyfile >/dev/null
|
||||
systemctl enable --now caddy >/dev/null 2>&1
|
||||
systemctl reload caddy >/dev/null 2>&1"
|
||||
msg_ok "Caddy is active (${PROD_DNS}, ${DEV_DNS})"
|
||||
|
||||
# ── Final output ───────────────────────────────────────────────────────
|
||||
IP="$(pct exec "$CTID" -- ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | head -n1)"
|
||||
|
||||
msg_info "Writing Proxmox notes"
|
||||
pct set "$CTID" --description "# ${APP}
|
||||
|
||||
This container runs two GhostGrid instances in parallel: main (production) and dev (staging), each with its own SQLite database.
|
||||
|
||||
## Caddy HTTPS
|
||||
|
||||
- Production: https://${PROD_DNS}
|
||||
- Development: https://${DEV_DNS}
|
||||
- Caddy uses internally generated certificates through 'tls internal'.
|
||||
- Both DNS names must internally resolve to ${IP}.
|
||||
- Caddy root CA inside the container: /var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt
|
||||
|
||||
## Production (main)
|
||||
|
||||
- Branch: ${REPO_BRANCH}
|
||||
- Directory: ${APP_DIR}
|
||||
- Service: ghostgrid
|
||||
- Internal upstream: 127.0.0.1:${APP_PORT}
|
||||
|
||||
## Development (dev)
|
||||
|
||||
- Branch: ${DEV_BRANCH}
|
||||
- Directory: ${DEV_DIR}
|
||||
- Service: ghostgrid-dev
|
||||
- Internal upstream: 127.0.0.1:${DEV_PORT}
|
||||
|
||||
## Gitea
|
||||
|
||||
- Repo: ${REPO_URL}
|
||||
|
||||
## Update from Proxmox host
|
||||
|
||||
- Prod: pct exec ${CTID} -- sudo -u ghostgrid ${APP_DIR}/deploy/deploy.sh main
|
||||
- Dev: pct exec ${CTID} -- sudo -u ghostgrid ${DEV_DIR}/deploy/deploy.sh dev
|
||||
|
||||
Installed via deploy/proxmox-ghostgrid.sh" >/dev/null
|
||||
msg_ok "Notes written"
|
||||
|
||||
echo
|
||||
msg_ok "${GN}${APP} has been installed!${CL}"
|
||||
echo -e " ${INFO} CTID: ${BL}${CTID}${CL}"
|
||||
echo -e " ${INFO} LXC IP: ${BL}${IP}${CL}"
|
||||
echo -e " ${INFO} Prod (main): ${BL}https://${PROD_DNS}${CL}"
|
||||
echo -e " ${INFO} Dev (dev): ${BL}https://${DEV_DNS}${CL}"
|
||||
echo -e " ${INFO} DNS note: ${BL}${PROD_DNS} and ${DEV_DNS} must resolve to ${IP}.${CL}"
|
||||
echo -e " ${INFO} Caddy Root CA: ${BL}/var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt${CL}"
|
||||
echo -e " ${INFO} Update prod: ${BL}pct exec ${CTID} -- sudo -u ghostgrid ${APP_DIR}/deploy/deploy.sh main${CL}"
|
||||
echo -e " ${INFO} Update dev: ${BL}pct exec ${CTID} -- sudo -u ghostgrid ${DEV_DIR}/deploy/deploy.sh dev${CL}"
|
||||
echo
|
||||
Reference in New Issue
Block a user