Initial commit

This commit is contained in:
Brückner
2026-06-03 15:20:06 +02:00
commit eed01b9665
34 changed files with 11921 additions and 0 deletions

22
deploy/deploy.sh Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Pull latest, rebuild, bounce the service. Works for both instances:
# deploy.sh -> main (prod, /opt/ghostgrid, port 3000)
# deploy.sh dev -> dev (stage, /opt/ghostgrid-dev, port 3001)
set -euo pipefail
BRANCH="${1:-main}"
case "$BRANCH" in
main) DIR=/opt/ghostgrid; SVC=ghostgrid ;;
dev) DIR=/opt/ghostgrid-dev; SVC=ghostgrid-dev ;;
*) echo "usage: deploy.sh [main|dev]"; exit 1 ;;
esac
cd "$DIR"
git fetch --prune origin
git checkout "$BRANCH"
git pull --ff-only origin "$BRANCH"
npm ci
npm run build
sudo systemctl restart "$SVC"
echo "Deployed $BRANCH ($SVC). Status:"
systemctl --no-pager status "$SVC" | head -n 5

View File

@ -0,0 +1,19 @@
[Unit]
Description=GhostGrid Development
After=network.target
[Service]
Type=simple
User=ghostgrid
Group=ghostgrid
WorkingDirectory=/opt/ghostgrid-dev
Environment=NODE_ENV=production
Environment=HOST=127.0.0.1
Environment=PORT=3001
EnvironmentFile=-/opt/ghostgrid-dev/.env
ExecStart=/usr/bin/node dist/server.cjs
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

19
deploy/ghostgrid.service Normal file
View File

@ -0,0 +1,19 @@
[Unit]
Description=GhostGrid Production
After=network.target
[Service]
Type=simple
User=ghostgrid
Group=ghostgrid
WorkingDirectory=/opt/ghostgrid
Environment=NODE_ENV=production
Environment=HOST=127.0.0.1
Environment=PORT=3000
EnvironmentFile=-/opt/ghostgrid/.env
ExecStart=/usr/bin/node dist/server.cjs
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

284
deploy/proxmox-ghostgrid.sh Normal file
View 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