Initial commit: stack
This commit is contained in:
commit
fe16a07dd7
11 changed files with 2058 additions and 0 deletions
812
install-stack.sh
Executable file
812
install-stack.sh
Executable file
|
|
@ -0,0 +1,812 @@
|
|||
#!/usr/bin/env bash
|
||||
# ┌─────────────────────────────────────────────────────────────────────────┐
|
||||
# │ HNF Manage — Proxmox Stack Installer │
|
||||
# │ Provisions: postgres · auth · portal · npm · management · noticeboard │
|
||||
# │ │
|
||||
# │ Run on the Proxmox host shell: │
|
||||
# │ bash install-stack.sh │
|
||||
# │ │
|
||||
# │ Or from Forgejo once repos are pushed: │
|
||||
# │ bash <(curl -fsSL https://git.pterois.co.uk/proxmox-helpers/stack/raw/branch/main/install-stack.sh)
|
||||
# └─────────────────────────────────────────────────────────────────────────┘
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colour helpers ────────────────────────────────────────────────────────────
|
||||
YW="\033[33m"; BL="\033[36m"; RD="\033[01;31m"
|
||||
GN="\033[1;92m"; DGN="\033[32m"; CL="\033[m"
|
||||
BFR="\\r\\033[K"; CM="${GN}✓${CL}"; CROSS="${RD}✗${CL}"
|
||||
|
||||
msg_info() { printf " ◌ ${YW}%-55s${CL}" "$*"; }
|
||||
msg_ok() { printf "${BFR} ${CM} ${DGN}%s${CL}\n" "$*"; }
|
||||
msg_error() { printf "${BFR} ${CROSS} ${RD}%s${CL}\n" "$*"; exit 1; }
|
||||
msg_warn() { printf "\n ${CROSS} ${YW}%s${CL}\n" "$*"; }
|
||||
msg_step() { printf "\n${BL}── %s ─────────────────────────────${CL}\n" "$*"; }
|
||||
|
||||
header_info() {
|
||||
clear
|
||||
printf "${BL}"
|
||||
cat <<'BANNER'
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ HNF MANAGE — PROXMOX STACK INSTALLER ║
|
||||
║ postgres · auth · portal · npm · mgmt · noticeboard ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
BANNER
|
||||
printf "${CL}\n"
|
||||
}
|
||||
|
||||
# ── Pre-flight ────────────────────────────────────────────────────────────────
|
||||
[[ $EUID -ne 0 ]] && msg_error "Must run as root on the Proxmox VE host"
|
||||
command -v pct &>/dev/null || msg_error "pct not found — run this on a Proxmox VE host"
|
||||
command -v pvesm &>/dev/null || msg_error "pvesm not found — run this on a Proxmox VE host"
|
||||
command -v whiptail &>/dev/null || { apt-get install -y -qq whiptail &>/dev/null; }
|
||||
command -v openssl &>/dev/null || { apt-get install -y -qq openssl &>/dev/null; }
|
||||
|
||||
# Defensive — when run via `bash <(curl ...)` BASH_SOURCE is a pipe, not a file
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || echo /tmp)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." 2>/dev/null && pwd || echo /tmp)"
|
||||
|
||||
header_info
|
||||
|
||||
# ── Detect storage pool ───────────────────────────────────────────────────────
|
||||
detect_storage() {
|
||||
if pvesm status 2>/dev/null | awk '{print $1}' | grep -q "^local-lvm$"; then
|
||||
echo "local-lvm"
|
||||
elif pvesm status 2>/dev/null | awk '{print $1}' | grep -q "^local-zfs$"; then
|
||||
echo "local-zfs"
|
||||
else
|
||||
echo "local"
|
||||
fi
|
||||
}
|
||||
STORAGE=$(detect_storage)
|
||||
|
||||
# ── Check vmbr1 internal bridge ───────────────────────────────────────────────
|
||||
check_vmbr1() {
|
||||
if ! ip link show vmbr1 &>/dev/null; then
|
||||
whiptail --title "vmbr1 Missing" --msgbox \
|
||||
"The internal container bridge vmbr1 does not exist yet.
|
||||
|
||||
Add the following to /etc/network/interfaces on this host,
|
||||
then run: ifreload -a
|
||||
|
||||
auto vmbr1
|
||||
iface vmbr1 inet static
|
||||
address 10.10.10.1/24
|
||||
bridge-ports none
|
||||
bridge-stp off
|
||||
bridge-fd 0
|
||||
|
||||
Then re-run this installer." 18 62
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Ensure Ubuntu 22.04 template ─────────────────────────────────────────────
|
||||
ensure_template() {
|
||||
local tmpl
|
||||
tmpl=$(pveam list local 2>/dev/null | awk '/ubuntu-22\.04/{print $1; exit}')
|
||||
if [[ -z "$tmpl" ]]; then
|
||||
msg_info "Downloading Ubuntu 22.04 LXC template"
|
||||
pveam update &>/dev/null
|
||||
pveam download local ubuntu-22.04-standard_22.04-1_amd64.tar.zst &>/dev/null
|
||||
msg_ok "Template downloaded"
|
||||
tmpl="ubuntu-22.04-standard_22.04-1_amd64.tar.zst"
|
||||
fi
|
||||
# Return full path for pct create
|
||||
echo "local:vztmpl/${tmpl##*/}"
|
||||
}
|
||||
|
||||
# ── Collect site config ───────────────────────────────────────────────────────
|
||||
collect_config() {
|
||||
SITE_NAME=$(whiptail --title "HNF Stack — Site Config" \
|
||||
--inputbox "Site name:" 8 52 "Hotel Number Four" 3>&1 1>&2 2>&3) || exit 0
|
||||
|
||||
DOMAIN=$(whiptail --title "HNF Stack — Site Config" \
|
||||
--inputbox "Public domain (e.g. manage.hotelnumberfour.com):" 8 64 "manage.hotelnumberfour.com" \
|
||||
3>&1 1>&2 2>&3) || exit 0
|
||||
|
||||
NPM_LAN_IP=$(whiptail --title "HNF Stack — Site Config" \
|
||||
--inputbox "NPM LXC static LAN IP (from your hotel LAN pool):" 8 64 "10.4.0.50" \
|
||||
3>&1 1>&2 2>&3) || exit 0
|
||||
|
||||
LAN_GW=$(whiptail --title "HNF Stack — Site Config" \
|
||||
--inputbox "LAN gateway IP:" 8 52 "10.4.0.1" 3>&1 1>&2 2>&3) || exit 0
|
||||
|
||||
OFFICE_IP=$(whiptail --title "HNF Stack — Site Config" \
|
||||
--inputbox \
|
||||
"Office IP / CIDR / DDNS hostname for offsite restriction.
|
||||
Examples: 203.0.113.5 10.4.0.0/22 hotel.dyndns.org
|
||||
Type 'disabled' to allow access from anywhere:" \
|
||||
11 64 "disabled" 3>&1 1>&2 2>&3) || exit 0
|
||||
|
||||
ADMIN_EMAIL=$(whiptail --title "HNF Stack — Admin Account" \
|
||||
--inputbox "Admin user email:" 8 52 "" 3>&1 1>&2 2>&3) || exit 0
|
||||
|
||||
ADMIN_PASS=$(whiptail --title "HNF Stack — Admin Account" \
|
||||
--passwordbox "Admin user password:" 8 52 3>&1 1>&2 2>&3) || exit 0
|
||||
|
||||
# Deploy source — per-service Forgejo repos (default) or a local copy on this host
|
||||
FORGEJO_BASE=$(whiptail --title "HNF Stack — Forgejo" \
|
||||
--inputbox \
|
||||
"Forgejo org/base URL hosting the per-service repos.
|
||||
Each service is cloned from <base>/<service>.git
|
||||
→ auth portal management noticeboard
|
||||
|
||||
Example: https://git.pterois.co.uk/proxmox-helpers" \
|
||||
13 66 "https://git.pterois.co.uk/proxmox-helpers" 3>&1 1>&2 2>&3) || exit 0
|
||||
FORGEJO_BASE="${FORGEJO_BASE%/}"
|
||||
|
||||
FORGEJO_TOKEN=$(whiptail --title "HNF Stack — Forgejo Token" \
|
||||
--passwordbox \
|
||||
"Access token for cloning private repos.
|
||||
Create in Forgejo: Settings → Applications → Generate Token
|
||||
(scope: read:repository). It is embedded in each LXC's git
|
||||
remote so the management updater can pull on webhook.
|
||||
|
||||
Leave blank if the repos are public." \
|
||||
13 66 3>&1 1>&2 2>&3) || exit 0
|
||||
|
||||
if whiptail --title "HNF Stack — Deploy Source" --yesno \
|
||||
"Deploy services from Forgejo? (recommended)\n\nNo = copy from a local repo at ${REPO_ROOT}\n(only works if you already copied the repo to this host)" \
|
||||
11 62; then
|
||||
USE_FORGEJO=true
|
||||
else
|
||||
USE_FORGEJO=false
|
||||
fi
|
||||
|
||||
BACKUP_REMOTE=$(whiptail --title "HNF Stack — Backup" \
|
||||
--inputbox \
|
||||
"Backup rsync target (leave blank to skip backup config).
|
||||
Example: backup@192.168.1.10:/backups/hnf" \
|
||||
10 64 "" 3>&1 1>&2 2>&3) || exit 0
|
||||
|
||||
# Confirm LXC allocation
|
||||
whiptail --title "HNF Stack — Confirm" --yesno \
|
||||
"LXCs to be created (storage: ${STORAGE}):
|
||||
|
||||
ID Hostname IP
|
||||
──────────────────────────────────────────
|
||||
100 hnf-postgres 10.10.10.100
|
||||
101 hnf-auth 10.10.10.101
|
||||
102 hnf-portal 10.10.10.102
|
||||
103 hnf-npm 10.10.10.103 / ${NPM_LAN_IP} (dual-homed)
|
||||
105 hnf-management 10.10.10.105
|
||||
112 hnf-noticeboard 10.10.10.112
|
||||
|
||||
Domain: ${DOMAIN}
|
||||
Admin: ${ADMIN_EMAIL}
|
||||
|
||||
Proceed?" 24 58 || exit 0
|
||||
}
|
||||
|
||||
# ── Generate secrets ──────────────────────────────────────────────────────────
|
||||
gen_secrets() {
|
||||
msg_info "Generating secrets"
|
||||
PG_SUPERPASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
||||
AUTH_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
||||
NOTICES_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
||||
CENTRAL_AUTH_SECRET=$(openssl rand -hex 32)
|
||||
WEBHOOK_SECRET=$(openssl rand -hex 24)
|
||||
NPM_ADMIN_PASS=$(openssl rand -base64 12 | tr -dc 'a-zA-Z0-9' | head -c 12)
|
||||
|
||||
cat > /root/hnf-credentials.txt <<EOF
|
||||
# HNF Manage credentials — generated $(date '+%Y-%m-%d %H:%M')
|
||||
# !! KEEP THIS FILE SAFE — store a copy offsite !!
|
||||
|
||||
SITE_NAME=${SITE_NAME}
|
||||
DOMAIN=${DOMAIN}
|
||||
ADMIN_EMAIL=${ADMIN_EMAIL}
|
||||
ADMIN_PASS=${ADMIN_PASS}
|
||||
OFFICE_IP_CHECK=${OFFICE_IP}
|
||||
|
||||
PG_SUPERPASS=${PG_SUPERPASS}
|
||||
AUTH_DB_PASS=${AUTH_DB_PASS}
|
||||
NOTICES_DB_PASS=${NOTICES_DB_PASS}
|
||||
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
||||
WEBHOOK_SECRET=${WEBHOOK_SECRET}
|
||||
|
||||
NPM_ADMIN_EMAIL=admin@${DOMAIN}
|
||||
NPM_ADMIN_PASS=${NPM_ADMIN_PASS}
|
||||
|
||||
FORGEJO_BASE=${FORGEJO_BASE}
|
||||
FORGEJO_TOKEN=${FORGEJO_TOKEN}
|
||||
|
||||
BACKUP_REMOTE=${BACKUP_REMOTE}
|
||||
EOF
|
||||
chmod 600 /root/hnf-credentials.txt
|
||||
msg_ok "Secrets generated → /root/hnf-credentials.txt"
|
||||
}
|
||||
|
||||
# ── SSH keypair for management → app LXCs ────────────────────────────────────
|
||||
gen_mgmt_ssh_key() {
|
||||
if [[ ! -f /root/.ssh/hnf_management ]]; then
|
||||
msg_info "Generating management SSH keypair"
|
||||
mkdir -p /root/.ssh
|
||||
ssh-keygen -t ed25519 -f /root/.ssh/hnf_management -N "" -C "hnf-management-deploy" &>/dev/null
|
||||
msg_ok "SSH keypair generated → /root/.ssh/hnf_management"
|
||||
else
|
||||
msg_ok "Using existing SSH keypair at /root/.ssh/hnf_management"
|
||||
fi
|
||||
MGMT_PUBKEY=$(cat /root/.ssh/hnf_management.pub)
|
||||
echo "${MGMT_PUBKEY}" >> /root/hnf-credentials.txt
|
||||
}
|
||||
|
||||
# ── LXC lifecycle helpers ─────────────────────────────────────────────────────
|
||||
TEMPLATE_PATH=""
|
||||
|
||||
get_template() {
|
||||
[[ -n "$TEMPLATE_PATH" ]] && { echo "$TEMPLATE_PATH"; return; }
|
||||
TEMPLATE_PATH=$(ensure_template)
|
||||
echo "$TEMPLATE_PATH"
|
||||
}
|
||||
|
||||
lxc_exists() { pct status "$1" &>/dev/null; }
|
||||
|
||||
lxc_running() {
|
||||
pct status "$1" 2>/dev/null | grep -q "running"
|
||||
}
|
||||
|
||||
create_lxc() {
|
||||
local id=$1 ip=$2 name=$3 mem=${4:-512} cores=${5:-1}
|
||||
if lxc_exists "$id"; then
|
||||
msg_warn "LXC $id (hnf-${name}) already exists — skipping creation"
|
||||
lxc_running "$id" || pct start "$id"
|
||||
return
|
||||
fi
|
||||
local tmpl; tmpl=$(get_template)
|
||||
pct create "$id" "$tmpl" \
|
||||
--hostname "hnf-${name}" \
|
||||
--memory "$mem" \
|
||||
--cores "$cores" \
|
||||
--rootfs "${STORAGE}:8" \
|
||||
--net0 "name=eth0,bridge=vmbr1,ip=${ip}/24,gw=10.10.10.1" \
|
||||
--features nesting=1 \
|
||||
--unprivileged 0 \
|
||||
--onboot 1 \
|
||||
--start 1 &>/dev/null
|
||||
sleep 5 # let systemd start
|
||||
}
|
||||
|
||||
create_npm_lxc() {
|
||||
local id=103
|
||||
if lxc_exists "$id"; then
|
||||
msg_warn "LXC $id (hnf-npm) already exists — skipping creation"
|
||||
lxc_running "$id" || pct start "$id"
|
||||
return
|
||||
fi
|
||||
local tmpl; tmpl=$(get_template)
|
||||
pct create "$id" "$tmpl" \
|
||||
--hostname "hnf-npm" \
|
||||
--memory 512 \
|
||||
--cores 1 \
|
||||
--rootfs "${STORAGE}:8" \
|
||||
--net0 "name=eth0,bridge=vmbr0,ip=${NPM_LAN_IP}/22,gw=${LAN_GW}" \
|
||||
--net1 "name=eth1,bridge=vmbr1,ip=10.10.10.3/24" \
|
||||
--features nesting=1 \
|
||||
--unprivileged 0 \
|
||||
--onboot 1 \
|
||||
--start 1 &>/dev/null
|
||||
sleep 5
|
||||
}
|
||||
|
||||
install_docker() {
|
||||
local id=$1
|
||||
pct exec "$id" -- bash -s &>/dev/null <<'DOCKER_INSTALL'
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq 2>/dev/null
|
||||
apt-get install -y -qq ca-certificates curl gnupg git openssh-server 2>/dev/null
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
|
||||
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg 2>/dev/null
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||
https://download.docker.com/linux/ubuntu jammy stable" \
|
||||
> /etc/apt/sources.list.d/docker.list
|
||||
apt-get update -qq 2>/dev/null
|
||||
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin 2>/dev/null
|
||||
systemctl enable --now docker 2>/dev/null
|
||||
systemctl enable --now ssh 2>/dev/null
|
||||
DOCKER_INSTALL
|
||||
}
|
||||
|
||||
install_mgmt_key() {
|
||||
local id=$1
|
||||
pct exec "$id" -- bash -c "
|
||||
mkdir -p /root/.ssh
|
||||
chmod 700 /root/.ssh
|
||||
grep -qF '${MGMT_PUBKEY}' /root/.ssh/authorized_keys 2>/dev/null || \
|
||||
echo '${MGMT_PUBKEY}' >> /root/.ssh/authorized_keys
|
||||
chmod 600 /root/.ssh/authorized_keys
|
||||
" &>/dev/null
|
||||
}
|
||||
|
||||
push_file() {
|
||||
# Write content to a temp file, push into LXC, remove temp
|
||||
local id=$1 dest=$2; shift 2
|
||||
local tmp; tmp=$(mktemp /tmp/hnf-push-XXXX)
|
||||
cat > "$tmp" # reads stdin
|
||||
pct push "$id" "$tmp" "$dest" 2>/dev/null
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
push_dir() {
|
||||
# tar local dir → push tarball → extract in LXC at parent of dest
|
||||
local id=$1 src=$2 dest=$3
|
||||
local tmp; tmp=$(mktemp /tmp/hnf-dir-XXXX.tar.gz)
|
||||
tar czf "$tmp" -C "$(dirname "$src")" "$(basename "$src")" 2>/dev/null
|
||||
pct push "$id" "$tmp" /tmp/hnf-deploy.tar.gz 2>/dev/null
|
||||
pct exec "$id" -- bash -c "
|
||||
mkdir -p '$(dirname "$dest")'
|
||||
tar xzf /tmp/hnf-deploy.tar.gz -C '$(dirname "$dest")'
|
||||
mv '$(dirname "$dest")/$(basename "$src")' '${dest}' 2>/dev/null || true
|
||||
rm -f /tmp/hnf-deploy.tar.gz
|
||||
" &>/dev/null
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
build_clone_url() {
|
||||
# Inject the Forgejo token into the clone URL so private repos work and the
|
||||
# updater can pull later without extra credentials.
|
||||
local repo=$1
|
||||
local url="${FORGEJO_BASE}/${repo}.git"
|
||||
if [[ -n "${FORGEJO_TOKEN:-}" ]]; then
|
||||
url="${url/https:\/\//https://oauth2:${FORGEJO_TOKEN}@}"
|
||||
url="${url/http:\/\//http://oauth2:${FORGEJO_TOKEN}@}"
|
||||
fi
|
||||
echo "$url"
|
||||
}
|
||||
|
||||
deploy_service() {
|
||||
# Either clone from Forgejo or push from a local repo copy on this host
|
||||
local id=$1 repo_name=$2 local_src=$3 dest=$4
|
||||
if [[ "$USE_FORGEJO" == "true" ]]; then
|
||||
local url; url=$(build_clone_url "$repo_name")
|
||||
pct exec "$id" -- bash -c "
|
||||
if [ -d '${dest}/.git' ]; then cd '${dest}' && git pull -q; \
|
||||
else git clone -q '${url}' '${dest}'; fi
|
||||
" &>/dev/null
|
||||
else
|
||||
push_dir "$id" "$local_src" "$dest"
|
||||
fi
|
||||
}
|
||||
|
||||
wait_healthy() {
|
||||
local id=$1 url=$2 max=${3:-40}
|
||||
local i=0
|
||||
while ! pct exec "$id" -- curl -sf --max-time 2 "$url" &>/dev/null; do
|
||||
sleep 3; ((i++))
|
||||
[[ $i -ge $max ]] && return 1
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
wait_pg() {
|
||||
local max=30 i=0
|
||||
while ! pct exec 100 -- bash -c \
|
||||
"docker exec hnf-postgres pg_isready -U postgres" &>/dev/null; do
|
||||
sleep 3; ((i++))
|
||||
[[ $i -ge $max ]] && { msg_warn "Postgres not ready after 90s"; return 1; }
|
||||
done
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE 1 — POSTGRES LXC 100
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
deploy_postgres() {
|
||||
msg_step "1/6 Postgres (LXC 100 · 10.10.10.100)"
|
||||
|
||||
msg_info "Creating LXC 100"
|
||||
create_lxc 100 "10.10.10.100" "postgres" 512 1
|
||||
msg_ok "LXC 100 created"
|
||||
|
||||
msg_info "Installing Docker"
|
||||
install_docker 100
|
||||
msg_ok "Docker installed"
|
||||
|
||||
msg_info "Deploying postgres"
|
||||
pct exec 100 -- mkdir -p /opt/postgres/init
|
||||
|
||||
# docker-compose.yml
|
||||
push_file 100 /opt/postgres/docker-compose.yml <<'EOF'
|
||||
services:
|
||||
postgres:
|
||||
container_name: hnf-postgres
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=${PG_SUPERPASS}
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
- ./init:/docker-entrypoint-initdb.d:ro
|
||||
ports:
|
||||
- "5432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
retries: 15
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
pg_data:
|
||||
EOF
|
||||
|
||||
# .env
|
||||
push_file 100 /opt/postgres/.env <<EOF
|
||||
PG_SUPERPASS=${PG_SUPERPASS}
|
||||
EOF
|
||||
|
||||
# Init SQL — auth DB
|
||||
push_file 100 /opt/postgres/init/01-auth.sql <<EOF
|
||||
CREATE USER auth WITH PASSWORD '${AUTH_DB_PASS}';
|
||||
CREATE DATABASE auth_db OWNER auth;
|
||||
\c auth_db
|
||||
GRANT ALL ON SCHEMA public TO auth;
|
||||
EOF
|
||||
|
||||
# Init SQL — noticeboard DB
|
||||
push_file 100 /opt/postgres/init/02-noticeboard.sql <<EOF
|
||||
CREATE USER noticeboard WITH PASSWORD '${NOTICES_DB_PASS}';
|
||||
CREATE DATABASE noticeboard_db OWNER noticeboard;
|
||||
\c noticeboard_db
|
||||
GRANT ALL ON SCHEMA public TO noticeboard;
|
||||
EOF
|
||||
|
||||
pct exec 100 -- bash -c "cd /opt/postgres && docker compose up -d" &>/dev/null
|
||||
|
||||
msg_info "Waiting for postgres to be ready"
|
||||
wait_pg && msg_ok "Postgres running at 10.10.10.100:5432" || msg_warn "Postgres may need extra time — check LXC 100"
|
||||
|
||||
install_mgmt_key 100
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE 2 — AUTH SERVICE LXC 101
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
deploy_auth() {
|
||||
msg_step "2/6 Auth service (LXC 101 · 10.10.10.101)"
|
||||
|
||||
msg_info "Creating LXC 101"
|
||||
create_lxc 101 "10.10.10.101" "auth" 512 1
|
||||
msg_ok "LXC 101 created"
|
||||
|
||||
msg_info "Installing Docker"
|
||||
install_docker 101
|
||||
install_mgmt_key 101
|
||||
msg_ok "Docker + SSH ready"
|
||||
|
||||
msg_info "Deploying auth service"
|
||||
deploy_service 101 "auth" "${REPO_ROOT}/auth" /opt/auth
|
||||
|
||||
push_file 101 /opt/auth/.env <<EOF
|
||||
NODE_ENV=production
|
||||
PORT=3001
|
||||
DATABASE_URL=postgresql://auth:${AUTH_DB_PASS}@10.10.10.100:5432/auth_db
|
||||
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
||||
DOMAIN=${DOMAIN}
|
||||
ADMIN_EMAIL=${ADMIN_EMAIL}
|
||||
ADMIN_PASSWORD=${ADMIN_PASS}
|
||||
OFFICE_IP_CHECK=${OFFICE_IP}
|
||||
CORS_ORIGIN=https://${DOMAIN}
|
||||
SESSION_DAYS=30
|
||||
EOF
|
||||
|
||||
pct exec 101 -- bash -c "cd /opt/auth && docker compose up -d --build" &>/dev/null
|
||||
|
||||
msg_info "Waiting for auth service"
|
||||
wait_healthy 101 "http://localhost:3001/health" \
|
||||
&& msg_ok "Auth service running at 10.10.10.101:3001" \
|
||||
|| msg_warn "Auth service may need extra time — check LXC 101"
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE 3 — PORTAL LXC 102
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
deploy_portal() {
|
||||
msg_step "3/6 Portal (LXC 102 · 10.10.10.102)"
|
||||
|
||||
msg_info "Creating LXC 102"
|
||||
create_lxc 102 "10.10.10.102" "portal" 1024 2
|
||||
msg_ok "LXC 102 created"
|
||||
|
||||
msg_info "Installing Docker"
|
||||
install_docker 102
|
||||
install_mgmt_key 102
|
||||
msg_ok "Docker + SSH ready"
|
||||
|
||||
msg_info "Deploying portal"
|
||||
deploy_service 102 "portal" "${REPO_ROOT}/portal" /opt/portal
|
||||
|
||||
push_file 102 /opt/portal/.env <<EOF
|
||||
NODE_ENV=production
|
||||
VITE_API_BASE=
|
||||
EOF
|
||||
|
||||
# Patch nginx.conf with real auth LXC IP (already correct in template but be explicit)
|
||||
pct exec 102 -- bash -c "
|
||||
cd /opt/portal && docker compose up -d --build
|
||||
" &>/dev/null
|
||||
|
||||
msg_info "Waiting for portal"
|
||||
wait_healthy 102 "http://localhost:3000/health" \
|
||||
&& msg_ok "Portal running at 10.10.10.102:3000" \
|
||||
|| msg_warn "Portal may need extra time — check LXC 102"
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE 4 — NPM LXC 103 (dual-homed)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
deploy_npm() {
|
||||
msg_step "4/6 Nginx Proxy Manager (LXC 103 · ${NPM_LAN_IP} / 10.10.10.3)"
|
||||
|
||||
msg_info "Creating NPM LXC 103 (dual-homed)"
|
||||
create_npm_lxc
|
||||
msg_ok "LXC 103 created"
|
||||
|
||||
msg_info "Installing Docker"
|
||||
install_docker 103
|
||||
msg_ok "Docker installed"
|
||||
|
||||
msg_info "Deploying NPM"
|
||||
pct exec 103 -- mkdir -p /opt/npm
|
||||
|
||||
push_file 103 /opt/npm/docker-compose.yml <<'EOF'
|
||||
services:
|
||||
npm:
|
||||
container_name: hnf-npm
|
||||
image: jc21/nginx-proxy-manager:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "81:81"
|
||||
volumes:
|
||||
- npm_data:/data
|
||||
- npm_letsencrypt:/etc/letsencrypt
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
npm_data:
|
||||
npm_letsencrypt:
|
||||
EOF
|
||||
|
||||
pct exec 103 -- bash -c "cd /opt/npm && docker compose up -d" &>/dev/null
|
||||
|
||||
msg_info "Waiting for NPM admin UI"
|
||||
# NPM admin API on port 81 — wait up to 60s
|
||||
local i=0
|
||||
while ! pct exec 103 -- curl -sf --max-time 3 "http://localhost:81/api/" &>/dev/null; do
|
||||
sleep 3; ((i++)); [[ $i -ge 20 ]] && break
|
||||
done
|
||||
msg_ok "NPM running — admin UI at http://${NPM_LAN_IP}:81"
|
||||
msg_warn "NPM default login: admin@example.com / changeme (change immediately!)"
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE 5 — MANAGEMENT LXC 105
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
deploy_management() {
|
||||
msg_step "5/6 Management (LXC 105 · 10.10.10.105)"
|
||||
|
||||
msg_info "Creating LXC 105"
|
||||
create_lxc 105 "10.10.10.105" "management" 512 1
|
||||
msg_ok "LXC 105 created"
|
||||
|
||||
msg_info "Installing Docker"
|
||||
install_docker 105
|
||||
msg_ok "Docker installed"
|
||||
|
||||
# Copy the management SSH private key into management container
|
||||
pct exec 105 -- mkdir -p /root/.ssh
|
||||
pct push 105 /root/.ssh/hnf_management /root/.ssh/hnf_management &>/dev/null
|
||||
pct exec 105 -- chmod 600 /root/.ssh/hnf_management
|
||||
|
||||
msg_info "Deploying management stack"
|
||||
deploy_service 105 "management" "${REPO_ROOT}/management" /opt/management
|
||||
|
||||
push_file 105 /opt/management/.env <<EOF
|
||||
FORGEJO_WEBHOOK_SECRET=${WEBHOOK_SECRET}
|
||||
BACKUP_REMOTE=${BACKUP_REMOTE}
|
||||
BACKUP_PG_HOST=10.10.10.100
|
||||
BACKUP_PG_USER=postgres
|
||||
BACKUP_PG_PASS=${PG_SUPERPASS}
|
||||
UPTIME_KUMA_PORT=3002
|
||||
UPDATER_PORT=9000
|
||||
EOF
|
||||
|
||||
pct exec 105 -- bash -c "cd /opt/management && docker compose up -d --build" &>/dev/null
|
||||
|
||||
msg_info "Waiting for Uptime Kuma"
|
||||
wait_healthy 105 "http://localhost:3002" \
|
||||
&& msg_ok "Management running — Kuma at 10.10.10.105:3002" \
|
||||
|| msg_warn "Management may need extra time — check LXC 105"
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE 6 — NOTICEBOARD LXC 112
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
deploy_noticeboard() {
|
||||
msg_step "6/6 Noticeboard (LXC 112 · 10.10.10.112)"
|
||||
|
||||
msg_info "Creating LXC 112"
|
||||
create_lxc 112 "10.10.10.112" "noticeboard" 512 1
|
||||
msg_ok "LXC 112 created"
|
||||
|
||||
msg_info "Installing Docker"
|
||||
install_docker 112
|
||||
install_mgmt_key 112
|
||||
msg_ok "Docker + SSH ready"
|
||||
|
||||
msg_info "Deploying noticeboard"
|
||||
deploy_service 112 "noticeboard" "${REPO_ROOT}/noticeboard" /opt/noticeboard
|
||||
|
||||
push_file 112 /opt/noticeboard/.env <<EOF
|
||||
DATABASE_URL=postgresql://noticeboard:${NOTICES_DB_PASS}@10.10.10.100:5432/noticeboard_db
|
||||
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
||||
APP_SLUG=noticeboard
|
||||
OFFICE_IP_CHECK=${OFFICE_IP}
|
||||
NODE_ENV=production
|
||||
EOF
|
||||
|
||||
pct exec 112 -- bash -c "cd /opt/noticeboard && docker compose up -d --build" &>/dev/null
|
||||
|
||||
msg_info "Waiting for noticeboard"
|
||||
wait_healthy 112 "http://localhost:3080/notices/health" \
|
||||
&& msg_ok "Noticeboard running at 10.10.10.112:3080" \
|
||||
|| msg_warn "Noticeboard may need extra time — check LXC 112"
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# NPM PROXY HOSTS (via API)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
configure_npm_proxy_hosts() {
|
||||
msg_step "Configuring NPM proxy hosts"
|
||||
msg_info "Waiting for NPM API to be ready"
|
||||
|
||||
local i=0
|
||||
while ! curl -sf --max-time 3 "http://${NPM_LAN_IP}:81/api/" &>/dev/null; do
|
||||
sleep 3; ((i++))
|
||||
[[ $i -ge 30 ]] && { msg_warn "NPM API not responding — configure proxy hosts manually"; return; }
|
||||
done
|
||||
|
||||
# Get token with default credentials
|
||||
local token
|
||||
token=$(curl -sf -X POST "http://${NPM_LAN_IP}:81/api/tokens" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"identity":"admin@example.com","secret":"changeme"}' \
|
||||
2>/dev/null | grep -o '"token":"[^"]*"' | cut -d'"' -f4) || true
|
||||
|
||||
if [[ -z "$token" ]]; then
|
||||
msg_warn "Could not get NPM token — proxy hosts must be created manually (see summary)"
|
||||
return
|
||||
fi
|
||||
|
||||
msg_ok "NPM API authenticated"
|
||||
|
||||
create_proxy_host() {
|
||||
local name=$1 forward_host=$2 forward_port=$3 locations_json=${4:-'[]'}
|
||||
curl -sf -X POST "http://${NPM_LAN_IP}:81/api/proxy-hosts" \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"domain_names\": [\"${DOMAIN}\"],
|
||||
\"forward_scheme\": \"http\",
|
||||
\"forward_host\": \"${forward_host}\",
|
||||
\"forward_port\": ${forward_port},
|
||||
\"ssl_forced\": false,
|
||||
\"locations\": ${locations_json},
|
||||
\"block_exploits\": true,
|
||||
\"allow_websocket_upgrade\": true,
|
||||
\"http2_support\": false,
|
||||
\"advanced_config\": \"proxy_set_header X-Real-IP \$remote_addr;\nproxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;\"
|
||||
}" &>/dev/null && echo "created" || echo "failed"
|
||||
}
|
||||
|
||||
# Single proxy host for the domain routing everything through portal,
|
||||
# with custom locations per app path.
|
||||
# NPM "custom locations" feature handles path-based routing.
|
||||
local locations
|
||||
locations=$(cat <<LOCS
|
||||
[
|
||||
{
|
||||
"path": "/api/auth/",
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "10.10.10.101",
|
||||
"forward_port": 3001,
|
||||
"advanced_config": "proxy_set_header X-Real-IP \$remote_addr;\nproxy_set_header Host \$host;"
|
||||
},
|
||||
{
|
||||
"path": "/notices/",
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "10.10.10.112",
|
||||
"forward_port": 3080,
|
||||
"advanced_config": "proxy_set_header X-Real-IP \$remote_addr;\nproxy_set_header Host \$host;"
|
||||
},
|
||||
{
|
||||
"path": "/monitor/",
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "10.10.10.105",
|
||||
"forward_port": 3002,
|
||||
"advanced_config": "proxy_set_header X-Real-IP \$remote_addr;\nproxy_set_header Host \$host;"
|
||||
}
|
||||
]
|
||||
LOCS
|
||||
)
|
||||
|
||||
local result; result=$(create_proxy_host "${DOMAIN}" "10.10.10.102" 3000 "$locations")
|
||||
if [[ "$result" == "created" ]]; then
|
||||
msg_ok "NPM proxy host created for ${DOMAIN}"
|
||||
msg_warn "SSL certificate: configure in NPM admin UI after DNS is pointed at ${NPM_LAN_IP}"
|
||||
else
|
||||
msg_warn "NPM proxy host creation failed — create manually (see summary)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# SUMMARY
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
print_summary() {
|
||||
printf "\n${GN}"
|
||||
cat <<SUMMARY
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ HNF Manage — Stack Deployed ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
SUMMARY
|
||||
printf "${CL}"
|
||||
|
||||
cat <<SUMMARY
|
||||
|
||||
Site: ${SITE_NAME}
|
||||
Domain: https://${DOMAIN}
|
||||
|
||||
── Services ──────────────────────────────────────────────────
|
||||
LXC Hostname IP Port Status
|
||||
100 hnf-postgres 10.10.10.100 5432 (internal only)
|
||||
101 hnf-auth 10.10.10.101 3001 /api/auth/*
|
||||
102 hnf-portal 10.10.10.102 3000 /
|
||||
103 hnf-npm 10.10.10.3 80/443 entry point
|
||||
(LAN) ${NPM_LAN_IP} 81 NPM admin
|
||||
105 hnf-management 10.10.10.105 3002 Uptime Kuma
|
||||
9000 Forgejo webhooks
|
||||
112 hnf-noticeboard 10.10.10.112 3080 /notices/
|
||||
|
||||
── Credentials ───────────────────────────────────────────────
|
||||
Admin login: ${ADMIN_EMAIL}
|
||||
Credentials: /root/hnf-credentials.txt (chmod 600)
|
||||
|
||||
── Next steps ────────────────────────────────────────────────
|
||||
1. Point DNS: ${DOMAIN} → ${NPM_LAN_IP}
|
||||
|
||||
2. NPM admin UI: http://${NPM_LAN_IP}:81
|
||||
Default: admin@example.com / changeme
|
||||
→ Change password → Add SSL cert for ${DOMAIN}
|
||||
→ Verify proxy host paths are routing correctly
|
||||
|
||||
3. Set up Uptime Kuma monitors:
|
||||
http://10.10.10.105:3002
|
||||
Health endpoints to monitor:
|
||||
http://10.10.10.101:3001/health (auth)
|
||||
http://10.10.10.102:3000/health (portal)
|
||||
http://10.10.10.112:3080/notices/health (noticeboard)
|
||||
|
||||
4. Forgejo webhooks (when repos are pushed):
|
||||
URL: http://10.10.10.105:9000/webhook
|
||||
Secret: ${WEBHOOK_SECRET}
|
||||
Events: Push
|
||||
|
||||
5. To add an app LXC later, on this host run:
|
||||
bash <(curl -fsSL https://git.pterois.co.uk/proxmox-helpers/stack/raw/branch/main/add-app.sh)
|
||||
|
||||
SUMMARY
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# ENTRY POINT
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
check_vmbr1
|
||||
collect_config
|
||||
gen_secrets
|
||||
gen_mgmt_ssh_key
|
||||
|
||||
deploy_postgres
|
||||
deploy_auth
|
||||
deploy_portal
|
||||
deploy_npm
|
||||
deploy_management
|
||||
deploy_noticeboard
|
||||
configure_npm_proxy_hosts
|
||||
print_summary
|
||||
Loading…
Add table
Add a link
Reference in a new issue