add-app.sh had a separate Docker-install block that never got the AppArmor
fix from install-stack.sh's install_docker() (commit 9a45e39) — every app
added individually via add-app.sh since then was exposed to Docker builds
failing with "docker-default profile could not be loaded ... while confined".
Hit this deploying the calendar app to LXC 126 on the dev stack.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
265 lines
13 KiB
Bash
Executable file
265 lines
13 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# ┌─────────────────────────────────────────────────────────────────────────┐
|
|
# │ Hotel 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/hotel-manage-credentials.txt
|
|
if [[ -f "$CREDS_FILE" ]]; then
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
|
[[ -z "${line//[[:space:]]/}" ]] && continue
|
|
[[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]] || continue
|
|
export "${BASH_REMATCH[1]}"="${BASH_REMATCH[2]}"
|
|
done < "$CREDS_FILE"
|
|
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/hotel-manage-stack}/${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/hotel-manage_deploy.pub ]] && MGMT_PUBKEY=$(cat /root/.ssh/hotel-manage_deploy.pub)
|
|
|
|
# ── Resource pool (created by install-stack.sh; reuse if present) ──────────────
|
|
POOL="hotel-manage"
|
|
POOL_OPT=""
|
|
pvesh get "/pools/${POOL}" &>/dev/null || pvesh create /pools --poolid "${POOL}" &>/dev/null || true
|
|
pvesh get "/pools/${POOL}" &>/dev/null && POOL_OPT="--pool ${POOL}"
|
|
|
|
# ── Create LXC ────────────────────────────────────────────────────────────────
|
|
msg_info "Creating LXC ${LXC_ID} (hotel-manage-${APP_NAME} at 10.10.10.${LAST_OCTET})"
|
|
pct create "$LXC_ID" "$TEMPLATE" \
|
|
--hostname "hotel-manage-${APP_NAME}" \
|
|
--memory 1024 \
|
|
--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 \
|
|
--tags "${POOL}" \
|
|
${POOL_OPT} \
|
|
--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
|
|
# Docker-in-LXC: neutralise AppArmor so Docker never tries to load a profile.
|
|
# In a confined LXC that fails ("docker-default ... while confined") for BOTH
|
|
# image builds and container runtime. Removing apparmor_parser makes Docker
|
|
# run everything unconfined — the container itself is the isolation boundary.
|
|
# (Same fix as install-stack.sh's install_docker() — see stack-init commit
|
|
# 9a45e39. Do NOT set lxc.apparmor.profile: unconfined on the LXC instead —
|
|
# that cancels the nesting=1 feature.)
|
|
if [ -e /usr/sbin/apparmor_parser ]; then
|
|
mv -f /usr/sbin/apparmor_parser /usr/sbin/apparmor_parser.disabled
|
|
fi
|
|
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 hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE USER ${APP_DB_USER} WITH PASSWORD '${APP_DB_PASS}';\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE DATABASE ${APP_DB_NAME} OWNER ${APP_DB_USER};\" 2>/dev/null || true
|
|
docker exec hotel-manage-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/hotel-manage-credentials.txt <<EOF
|
|
|
|
# ${APP_NAME} DB
|
|
${APP_NAME^^}_DB_PASS=${APP_DB_PASS}
|
|
EOF
|
|
fi
|
|
|
|
# ── Deploy app files ──────────────────────────────────────────────────────────
|
|
msg_info "Deploying ${APP_NAME}"
|
|
if [[ "$USE_FORGEJO" == "true" ]]; then
|
|
# Embed token for private repos (also lets the updater pull on webhook)
|
|
if [[ -n "${FORGEJO_TOKEN:-}" && "$FORGEJO_REPO" == https://* ]]; then
|
|
FORGEJO_REPO="${FORGEJO_REPO/https:\/\//https://oauth2:${FORGEJO_TOKEN}@}"
|
|
fi
|
|
pct exec "$LXC_ID" -- bash -c "
|
|
git clone -q '${FORGEJO_REPO}' /opt/${APP_NAME}
|
|
" &>/dev/null
|
|
else
|
|
tmp=$(mktemp /tmp/hotel-manage-app-XXXX.tar.gz)
|
|
tar czf "$tmp" -C "$(dirname "$LOCAL_SRC")" "$(basename "$LOCAL_SRC")" 2>/dev/null
|
|
pct push "$LXC_ID" "$tmp" /tmp/hotel-manage-app.tar.gz 2>/dev/null
|
|
pct exec "$LXC_ID" -- bash -c "
|
|
mkdir -p /opt && tar xzf /tmp/hotel-manage-app.tar.gz -C /opt
|
|
mv /opt/$(basename "$LOCAL_SRC") /opt/${APP_NAME} 2>/dev/null || true
|
|
rm -f /tmp/hotel-manage-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/hotel-manage-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 DB ──────────────────────────────────────────
|
|
msg_info "Registering ${APP_NAME} in auth service"
|
|
pct exec 100 -- docker exec hotel-manage-postgres psql -U postgres -d auth_db -c \
|
|
"INSERT INTO apps (slug, name, description, base_path, icon, theme_color, internal_host, internal_port)
|
|
VALUES ('${APP_NAME}', '${APP_NAME^}', '', '${APP_PATH%/}', 'ClipboardList', '#1e3a5f', '10.10.10.${LAST_OCTET}', ${APP_PORT})
|
|
ON CONFLICT (slug) DO UPDATE SET
|
|
internal_host = '10.10.10.${LAST_OCTET}',
|
|
internal_port = ${APP_PORT};" &>/dev/null \
|
|
&& msg_ok "${APP_NAME} registered in auth DB (visible in portal after next login)" \
|
|
|| msg_warn "Auto-register failed — add ${APP_NAME} to the apps table in auth_db manually"
|
|
|
|
# ── NPM route reminder ────────────────────────────────────────────────────────
|
|
NPM_LAN="${NPM_LAN_IP:-<npm-lan-ip>}"
|
|
|
|
printf "\n${GN}── Summary ──────────────────────────────────────────────────${CL}\n"
|
|
cat <<SUMMARY
|
|
App: ${APP_NAME}
|
|
LXC: ${LXC_ID} at 10.10.10.${LAST_OCTET}
|
|
Port: ${APP_PORT}
|
|
Path: ${APP_PATH}
|
|
|
|
Next steps:
|
|
1. cd /opt/${APP_NAME} && docker compose up -d --build (in LXC ${LXC_ID})
|
|
2. Add NPM custom location in admin UI:
|
|
Path: ${APP_PATH}
|
|
Forward: http://10.10.10.${LAST_OCTET}:${APP_PORT}
|
|
3. Add to Uptime Kuma:
|
|
http://10.10.10.${LAST_OCTET}:${APP_PORT}${APP_PATH}health
|
|
4. Add Forgejo webhook:
|
|
http://10.10.10.105:9000/webhook (secret in /root/hotel-manage-credentials.txt)
|
|
5. App is auto-registered in auth DB — management picks it up within 5 minutes
|
|
(or immediately on next Forgejo webhook push)
|
|
|
|
SUMMARY
|