#!/usr/bin/env bash # ┌─────────────────────────────────────────────────────────────────────────┐ # │ HNF Manage — Add App LXC │ # │ Provisions a single app container and wires it into the stack. │ # │ │ # │ Usage: bash add-app.sh (or curl-bootstrap, see install/README.md) │ # └─────────────────────────────────────────────────────────────────────────┘ set -euo pipefail 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" "$*"; } [[ $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" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" # Load secrets from credentials file if present CREDS_FILE=/root/hnf-credentials.txt if [[ -f "$CREDS_FILE" ]]; then # shellcheck disable=SC1090 set -a; source <(grep -v '^#' "$CREDS_FILE" | grep '='); set +a fi # ── Collect config ──────────────────────────────────────────────────────────── APP_NAME=$(whiptail --title "Add App LXC" \ --inputbox "App slug (e.g. kitchen, cashup, housekeeping):" 8 52 "" 3>&1 1>&2 2>&3) || exit 0 APP_NAME=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-') LXC_ID=$(whiptail --title "Add App LXC" \ --inputbox "LXC ID to allocate (check Proxmox for free IDs):" 8 52 "" 3>&1 1>&2 2>&3) || exit 0 LAST_OCTET=$(whiptail --title "Add App LXC" \ --inputbox "Internal IP last octet (10.10.10.X):" 8 52 "$LXC_ID" 3>&1 1>&2 2>&3) || exit 0 APP_PORT=$(whiptail --title "Add App LXC" \ --inputbox "Port the app container listens on internally:" 8 52 "3000" 3>&1 1>&2 2>&3) || exit 0 APP_PATH=$(whiptail --title "Add App LXC" \ --inputbox "URL path prefix (e.g. /kitchen/):" 8 52 "/${APP_NAME}/" 3>&1 1>&2 2>&3) || exit 0 APP_DB=$(whiptail --title "Add App LXC" --yesno \ "Create a dedicated postgres database for this app?" 8 52 && echo "yes" || echo "no") if [[ "$APP_DB" == "yes" ]]; then APP_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24) APP_DB_USER="$APP_NAME" APP_DB_NAME="${APP_NAME}_db" fi if whiptail --title "Add App LXC" --yesno \ "Deploy from Forgejo git repo?" 8 52; then USE_FORGEJO=true FORGEJO_REPO=$(whiptail --title "Add App LXC" \ --inputbox "Full Forgejo repo URL (.git):" 8 70 \ "${FORGEJO_BASE:-https://git.pterois.co.uk/jtr}/${APP_NAME}.git" \ 3>&1 1>&2 2>&3) || exit 0 else USE_FORGEJO=false LOCAL_SRC=$(whiptail --title "Add App LXC" \ --inputbox "Local app directory path:" 8 70 \ "${REPO_ROOT}/apps/${APP_NAME}" 3>&1 1>&2 2>&3) || exit 0 fi ENV_EXTRA=$(whiptail --title "Add App LXC" \ --inputbox \ "Additional .env lines (KEY=VALUE, one per line). CENTRAL_AUTH_SECRET and DATABASE_URL are added automatically. Leave blank if none." \ 12 70 "" 3>&1 1>&2 2>&3) || exit 0 # Confirm whiptail --title "Add App LXC — Confirm" --yesno \ "Create LXC for app: ${APP_NAME} LXC ID: ${LXC_ID} IP: 10.10.10.${LAST_OCTET} Port: ${APP_PORT} Path: ${APP_PATH} Database: $( [[ "$APP_DB" == "yes" ]] && echo "${APP_DB_NAME}" || echo "none" ) Proceed?" 16 52 || exit 0 # ── Detect storage ──────────────────────────────────────────────────────────── STORAGE=$(pvesm status 2>/dev/null | awk '{print $1}' | grep -E "^local-lvm$|^local-zfs$" | head -1) STORAGE="${STORAGE:-local}" # ── Get template ────────────────────────────────────────────────────────────── TEMPLATE=$(pveam list local 2>/dev/null | awk '/ubuntu-22\.04/{print "local:vztmpl/"$1; exit}') if [[ -z "$TEMPLATE" ]]; then msg_info "Downloading Ubuntu 22.04 template" pveam update &>/dev/null && pveam download local ubuntu-22.04-standard_22.04-1_amd64.tar.zst &>/dev/null TEMPLATE=$(pveam list local 2>/dev/null | awk '/ubuntu-22\.04/{print "local:vztmpl/"$1; exit}') msg_ok "Template ready" fi # ── Get mgmt public key ─────────────────────────────────────────────────────── MGMT_PUBKEY="" [[ -f /root/.ssh/hnf_management.pub ]] && MGMT_PUBKEY=$(cat /root/.ssh/hnf_management.pub) # ── Create LXC ──────────────────────────────────────────────────────────────── msg_info "Creating LXC ${LXC_ID} (hnf-${APP_NAME} at 10.10.10.${LAST_OCTET})" pct create "$LXC_ID" "$TEMPLATE" \ --hostname "hnf-${APP_NAME}" \ --memory 512 \ --cores 1 \ --rootfs "${STORAGE}:8" \ --net0 "name=eth0,bridge=vmbr1,ip=10.10.10.${LAST_OCTET}/24,gw=10.10.10.1" \ --features nesting=1 \ --unprivileged 0 \ --onboot 1 \ --start 1 &>/dev/null sleep 5 msg_ok "LXC ${LXC_ID} created" # ── Install Docker ──────────────────────────────────────────────────────────── msg_info "Installing Docker" pct exec "$LXC_ID" -- bash -s &>/dev/null <<'DOCKER' export DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y -qq ca-certificates curl gnupg git openssh-server install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg 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 && apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin systemctl enable --now docker ssh DOCKER msg_ok "Docker installed" # ── Install management SSH key ──────────────────────────────────────────────── if [[ -n "$MGMT_PUBKEY" ]]; then pct exec "$LXC_ID" -- bash -c " mkdir -p /root/.ssh && chmod 700 /root/.ssh echo '${MGMT_PUBKEY}' >> /root/.ssh/authorized_keys chmod 600 /root/.ssh/authorized_keys " &>/dev/null msg_ok "Management SSH key installed" fi # ── Create database ─────────────────────────────────────────────────────────── if [[ "$APP_DB" == "yes" ]]; then msg_info "Creating database ${APP_DB_NAME}" # Run SQL via postgres LXC pct exec 100 -- bash -c " docker exec hnf-postgres psql -U postgres -c \ \"CREATE USER ${APP_DB_USER} WITH PASSWORD '${APP_DB_PASS}';\" 2>/dev/null || true docker exec hnf-postgres psql -U postgres -c \ \"CREATE DATABASE ${APP_DB_NAME} OWNER ${APP_DB_USER};\" 2>/dev/null || true docker exec hnf-postgres psql -U postgres -d ${APP_DB_NAME} -c \ \"GRANT ALL ON SCHEMA public TO ${APP_DB_USER};\" 2>/dev/null || true " &>/dev/null msg_ok "Database ${APP_DB_NAME} created" # Append DB creds to credentials file cat >> /root/hnf-credentials.txt </dev/null else tmp=$(mktemp /tmp/hnf-app-XXXX.tar.gz) tar czf "$tmp" -C "$(dirname "$LOCAL_SRC")" "$(basename "$LOCAL_SRC")" 2>/dev/null pct push "$LXC_ID" "$tmp" /tmp/hnf-app.tar.gz 2>/dev/null pct exec "$LXC_ID" -- bash -c " mkdir -p /opt && tar xzf /tmp/hnf-app.tar.gz -C /opt mv /opt/$(basename "$LOCAL_SRC") /opt/${APP_NAME} 2>/dev/null || true rm -f /tmp/hnf-app.tar.gz " &>/dev/null rm -f "$tmp" fi msg_ok "App files deployed to /opt/${APP_NAME}" # ── Write .env ──────────────────────────────────────────────────────────────── msg_info "Writing .env" ENV_TMP=$(mktemp /tmp/hnf-env-XXXX) { echo "NODE_ENV=production" echo "APP_SLUG=${APP_NAME}" echo "CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET:-REPLACE_ME}" echo "OFFICE_IP_CHECK=${OFFICE_IP_CHECK:-disabled}" if [[ "$APP_DB" == "yes" ]]; then echo "DATABASE_URL=postgresql://${APP_DB_USER}:${APP_DB_PASS}@10.10.10.100:5432/${APP_DB_NAME}" fi [[ -n "$ENV_EXTRA" ]] && echo "$ENV_EXTRA" } > "$ENV_TMP" pct push "$LXC_ID" "$ENV_TMP" "/opt/${APP_NAME}/.env" 2>/dev/null rm -f "$ENV_TMP" msg_ok ".env written" # ── Register app in auth service ────────────────────────────────────────────── msg_warn "Remember to register '${APP_NAME}' in the auth service DB:" printf " docker exec hnf-auth-1 node -e \"\n" printf " const db = require('./src/db.js');\n" printf " db.query(\\\"INSERT INTO apps (slug,name,base_path) VALUES ('%s','%s','%s') ON CONFLICT DO NOTHING\\\");\n" \ "$APP_NAME" "$APP_NAME" "$APP_PATH" printf " \"\n\n" # ── NPM route reminder ──────────────────────────────────────────────────────── NPM_LAN="${NPM_LAN_IP:-}" printf "\n${GN}── Summary ──────────────────────────────────────────────────${CL}\n" cat <