Files
customer-installer/install.sh
2026-01-11 22:40:08 +01:00

399 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/libsupabase.sh"
setup_traps
usage() {
cat >&2 <<'EOF'
Usage:
bash install.sh [options]
Core options:
--ctid <id> Force CT ID (optional). If omitted, a customer-safe CTID is generated.
--cores <n> (default: 2)
--memory <mb> (default: 4096)
--swap <mb> (default: 512)
--disk <gb> (default: 50)
--bridge <vmbrX> (default: vmbr0)
--storage <storage> (default: local-zfs)
--ip <dhcp|CIDR> (default: dhcp)
--vlan <id> VLAN tag for net0 (default: 90; set 0 to disable)
--privileged Create privileged CT (default: unprivileged)
--apt-proxy <url> Optional: APT proxy (e.g. http://192.168.45.2:3142) for Apt-Cacher NG
Domain / n8n options:
--base-domain <domain> (default: userman.de) -> FQDN becomes sb-<unix>.domain
--n8n-owner-email <email> (default: admin@<base-domain>)
--n8n-owner-pass <pass> Optional. If omitted, generated (policy compliant).
--help Show help
Notes:
- This script creates a Debian 12 LXC and provisions Docker + customer stack (Postgres/pgvector + n8n).
- At the end it prints a JSON with credentials and URLs.
EOF
}
# Defaults
APT_PROXY="http://192.168.45.2:3142"
CTID=""
CORES="4"
MEMORY="4096"
SWAP="512"
DISK="50"
BRIDGE="vmbr0"
STORAGE="local-zfs"
IPCFG="dhcp"
VLAN="90"
UNPRIV="1"
BASE_DOMAIN="userman.de"
N8N_OWNER_EMAIL=""
N8N_OWNER_PASS=""
# ---------------------------
# Arg parsing
# ---------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--ctid) CTID="${2:-}"; shift 2 ;;
--apt-proxy) APT_PROXY="${2:-}"; shift 2 ;;
--cores) CORES="${2:-}"; shift 2 ;;
--memory) MEMORY="${2:-}"; shift 2 ;;
--swap) SWAP="${2:-}"; shift 2 ;;
--disk) DISK="${2:-}"; shift 2 ;;
--bridge) BRIDGE="${2:-}"; shift 2 ;;
--storage) STORAGE="${2:-}"; shift 2 ;;
--ip) IPCFG="${2:-}"; shift 2 ;;
--vlan) VLAN="${2:-}"; shift 2 ;;
--privileged) UNPRIV="0"; shift 1 ;;
--base-domain) BASE_DOMAIN="${2:-}"; shift 2 ;;
--n8n-owner-email) N8N_OWNER_EMAIL="${2:-}"; shift 2 ;;
--n8n-owner-pass) N8N_OWNER_PASS="${2:-}"; shift 2 ;;
--help|-h) usage; exit 0 ;;
*) die "Unknown option: $1 (use --help)" ;;
esac
done
# ---------------------------
# Validation
# ---------------------------
[[ "$CORES" =~ ^[0-9]+$ ]] || die "--cores must be integer"
[[ "$MEMORY" =~ ^[0-9]+$ ]] || die "--memory must be integer"
[[ "$SWAP" =~ ^[0-9]+$ ]] || die "--swap must be integer"
[[ "$DISK" =~ ^[0-9]+$ ]] || die "--disk must be integer"
[[ "$UNPRIV" == "0" || "$UNPRIV" == "1" ]] || die "internal: UNPRIV invalid"
[[ "$VLAN" =~ ^[0-9]+$ ]] || die "--vlan must be integer (0 disables tagging)"
[[ -n "$BASE_DOMAIN" ]] || die "--base-domain must not be empty"
if [[ "$IPCFG" != "dhcp" ]]; then
[[ "$IPCFG" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$ ]] || die "--ip must be dhcp or CIDR (e.g. 192.168.45.171/24)"
fi
if [[ -n "${APT_PROXY}" ]]; then
[[ "${APT_PROXY}" =~ ^http://[^/]+:[0-9]+$ ]] || die "--apt-proxy must look like http://IP:PORT (example: http://192.168.45.2:3142)"
fi
info "Argument-Parsing OK"
if [[ -n "${APT_PROXY}" ]]; then
info "APT proxy enabled: ${APT_PROXY}"
else
info "APT proxy disabled"
fi
# ---------------------------
# Preflight Proxmox
# ---------------------------
need_cmd pct pvesm pveam pvesh grep date awk sed cut tr head
pve_storage_exists "$STORAGE" || die "Storage not found: $STORAGE"
pve_bridge_exists "$BRIDGE" || die "Bridge not found: $BRIDGE"
TEMPLATE="$(pve_template_ensure_debian12 "$STORAGE")"
info "Template OK: ${TEMPLATE}"
# Hostname / FQDN based on unix time
UNIXTS="$(date +%s)"
CT_HOSTNAME="sb-${UNIXTS}"
FQDN="${CT_HOSTNAME}.${BASE_DOMAIN}"
# CTID selection
if [[ -n "$CTID" ]]; then
[[ "$CTID" =~ ^[0-9]+$ ]] || die "--ctid must be integer"
if pve_vmid_exists_cluster "$CTID"; then
die "Forced CTID=${CTID} already exists in cluster"
fi
else
# Your agreed approach: unix time - 1000000000 (safe until 2038)
CTID="$(pve_ctid_from_unixtime "$UNIXTS")"
if pve_vmid_exists_cluster "$CTID"; then
die "Generated CTID=${CTID} already exists in cluster (unexpected). Try again in 1s."
fi
fi
# n8n owner defaults
if [[ -z "$N8N_OWNER_EMAIL" ]]; then
N8N_OWNER_EMAIL="admin@${BASE_DOMAIN}"
fi
if [[ -z "$N8N_OWNER_PASS" ]]; then
N8N_OWNER_PASS="$(gen_password_policy)"
else
# enforce policy early to avoid the UI error you saw
password_policy_check "$N8N_OWNER_PASS" || die "--n8n-owner-pass does not meet policy: 8+ chars, 1 number, 1 uppercase"
fi
info "CTID selected: ${CTID}"
info "SCRIPT_DIR=${SCRIPT_DIR}"
info "CT_HOSTNAME=${CT_HOSTNAME}"
info "FQDN=${FQDN}"
info "cores=${CORES} memory=${MEMORY}MB swap=${SWAP}MB disk=${DISK}GB"
info "bridge=${BRIDGE} storage=${STORAGE} ip=${IPCFG} vlan=${VLAN} unprivileged=${UNPRIV}"
# ---------------------------
# Step 5: Create CT
# ---------------------------
NET0="$(pve_build_net0 "$BRIDGE" "$IPCFG" "$VLAN")"
ROOTFS="${STORAGE}:${DISK}"
FEATURES="nesting=1,keyctl=1,fuse=1"
info "Step 5: Create CT"
info "Creating CT ${CTID} (${CT_HOSTNAME}) from ${TEMPLATE}"
pct create "${CTID}" "${TEMPLATE}" \
--hostname "${CT_HOSTNAME}" \
--cores "${CORES}" \
--memory "${MEMORY}" \
--swap "${SWAP}" \
--net0 "${NET0}" \
--rootfs "${ROOTFS}" \
--unprivileged "${UNPRIV}" \
--features "${FEATURES}" \
--start 0
info "CT created (not started). Next step: start CT + wait for IP"
info "Starting CT ${CTID}"
pct start "${CTID}"
CT_IP="$(pct_wait_for_ip "${CTID}" || true)"
[[ -n "${CT_IP}" ]] || die "Could not determine CT IP after start"
info "Step 5 OK: LXC erstellt + IP ermittelt"
info "CT_HOSTNAME=${CT_HOSTNAME}"
info "CT_IP=${CT_IP}"
# ---------------------------
# Step 6: Provision inside CT (Docker + Locales + Base)
# ---------------------------
info "Step 6: Provisioning im CT (Docker + Locales + Base)"
# Optional: APT proxy (Apt-Cacher NG)
if [[ -n "${APT_PROXY}" ]]; then
pct_exec "${CTID}" "cat > /etc/apt/apt.conf.d/00aptproxy <<'EOF'
Acquire::http::Proxy \"${APT_PROXY}\";
Acquire::https::Proxy \"DIRECT\";
EOF"
fi
# Minimal base packages
pct_exec "${CTID}" "export DEBIAN_FRONTEND=noninteractive; apt-get update -y"
pct_exec "${CTID}" "export DEBIAN_FRONTEND=noninteractive; apt-get install -y ca-certificates curl gnupg lsb-release"
# Locales (avoid perl warnings + consistent system)
pct_exec "${CTID}" "export DEBIAN_FRONTEND=noninteractive; apt-get update -y"
pct_exec "${CTID}" "export DEBIAN_FRONTEND=noninteractive; apt-get install -y locales ca-certificates curl gnupg lsb-release"
pct_exec "${CTID}" "sed -i 's/^# *de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/; s/^# *en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen || true"
pct_exec "${CTID}" "locale-gen >/dev/null || true"
pct_exec "${CTID}" "update-locale LANG=de_DE.UTF-8 LC_ALL=de_DE.UTF-8 || true"
# Docker official repo (Debian 12 / bookworm)
pct_exec "${CTID}" "install -m 0755 -d /etc/apt/keyrings"
pct_exec "${CTID}" "curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg"
pct_exec "${CTID}" "chmod a+r /etc/apt/keyrings/docker.gpg"
pct_exec "${CTID}" "echo \"deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \$(. /etc/os-release && echo \$VERSION_CODENAME) stable\" > /etc/apt/sources.list.d/docker.list"
pct_exec "${CTID}" "export DEBIAN_FRONTEND=noninteractive; apt-get update -y"
pct_exec "${CTID}" "export DEBIAN_FRONTEND=noninteractive; apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin"
# Create stack directories
pct_exec "${CTID}" "mkdir -p /opt/customer-stack/volumes/postgres/data /opt/customer-stack/volumes/n8n-data /opt/customer-stack/sql"
# IMPORTANT: n8n runs as node (uid 1000) => fix permissions
pct_exec "${CTID}" "chown -R 1000:1000 /opt/customer-stack/volumes/n8n-data"
info "Step 6 OK: Docker + Compose Plugin installiert, Locales gesetzt, Basis-Verzeichnisse erstellt"
info "Next: Schritt 7 (finales docker-compose + Secrets + n8n/supabase up + Healthchecks)"
# ---------------------------
# Step 7: Stack finalisieren + Secrets + Up + Checks
# ---------------------------
info "Step 7: Stack finalisieren + Secrets + Up + Checks"
# Secrets
PG_DB="customer"
PG_USER="customer"
PG_PASSWORD="$(gen_password_policy)"
N8N_ENCRYPTION_KEY="$(gen_hex_64)"
# External URL is HTTPS via OPNsense reverse proxy (but container internally is http)
N8N_PORT="5678"
N8N_PROTOCOL="http"
N8N_HOST="${CT_IP}"
N8N_EDITOR_BASE_URL="https://${FQDN}/"
WEBHOOK_URL="https://${FQDN}/"
# If you are behind HTTPS reverse proxy, secure cookies can be true.
# But until proxy is in place, false avoids login trouble.
N8N_SECURE_COOKIE="false"
# Write .env into CT
pct_push_text "${CTID}" "/opt/customer-stack/.env" "$(cat <<EOF
PG_DB=${PG_DB}
PG_USER=${PG_USER}
PG_PASSWORD=${PG_PASSWORD}
N8N_PORT=${N8N_PORT}
N8N_PROTOCOL=${N8N_PROTOCOL}
N8N_HOST=${N8N_HOST}
N8N_EDITOR_BASE_URL=${N8N_EDITOR_BASE_URL}
WEBHOOK_URL=${WEBHOOK_URL}
N8N_SECURE_COOKIE=${N8N_SECURE_COOKIE}
N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
# Telemetrie/Background Calls aus
N8N_DIAGNOSTICS_ENABLED=false
N8N_VERSION_NOTIFICATIONS_ENABLED=false
N8N_TEMPLATES_ENABLED=false
EOF
)"
# init sql for pgvector (optional but nice)
pct_push_text "${CTID}" "/opt/customer-stack/sql/init_pgvector.sql" "$(cat <<'SQL'
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
SQL
)"
# docker-compose.yml
pct_push_text "${CTID}" "/opt/customer-stack/docker-compose.yml" "$(cat <<'YML'
services:
postgres:
image: pgvector/pgvector:pg16
container_name: customer-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${PG_DB}
POSTGRES_USER: ${PG_USER}
POSTGRES_PASSWORD: ${PG_PASSWORD}
volumes:
- ./volumes/postgres/data:/var/lib/postgresql/data
- ./sql:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${PG_USER} -d ${PG_DB} || exit 1"]
interval: 10s
timeout: 5s
retries: 20
networks:
- customer-net
n8n:
image: n8nio/n8n:latest
container_name: n8n
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
ports:
- "${N8N_PORT}:5678"
environment:
# --- Web / Cookies / URL ---
N8N_PORT: 5678
N8N_PROTOCOL: ${N8N_PROTOCOL}
N8N_HOST: ${N8N_HOST}
N8N_EDITOR_BASE_URL: ${N8N_EDITOR_BASE_URL}
WEBHOOK_URL: ${WEBHOOK_URL}
N8N_SECURE_COOKIE: ${N8N_SECURE_COOKIE}
# --- Disable telemetry / background calls ---
N8N_DIAGNOSTICS_ENABLED: ${N8N_DIAGNOSTICS_ENABLED}
N8N_VERSION_NOTIFICATIONS_ENABLED: ${N8N_VERSION_NOTIFICATIONS_ENABLED}
N8N_TEMPLATES_ENABLED: ${N8N_TEMPLATES_ENABLED}
# --- DB (Postgres) ---
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: 5432
DB_POSTGRESDB_DATABASE: ${PG_DB}
DB_POSTGRESDB_USER: ${PG_USER}
DB_POSTGRESDB_PASSWORD: ${PG_PASSWORD}
# --- Basics ---
GENERIC_TIMEZONE: Europe/Berlin
TZ: Europe/Berlin
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
volumes:
- ./volumes/n8n-data:/home/node/.n8n
networks:
- customer-net
networks:
customer-net:
driver: bridge
YML
)"
# Make sure permissions are correct (again, after file writes)
pct_exec "${CTID}" "chown -R 1000:1000 /opt/customer-stack/volumes/n8n-data"
# Pull + up
pct_exec "${CTID}" "cd /opt/customer-stack && docker compose pull"
pct_exec "${CTID}" "cd /opt/customer-stack && docker compose up -d"
pct_exec "${CTID}" "cd /opt/customer-stack && docker compose ps"
# --- Owner account creation (robust way) ---
# n8n shows the setup screen if no user exists.
# We create the owner via CLI inside the container.
pct_exec "${CTID}" "cd /opt/customer-stack && docker exec -u node n8n n8n --help >/dev/null 2>&1 || true"
# Try modern command first (works in current n8n builds); if it fails, we leave setup screen (but youll see it in logs).
pct_exec "${CTID}" "cd /opt/customer-stack && (docker exec -u node n8n n8n user-management:reset --email '${N8N_OWNER_EMAIL}' --password '${N8N_OWNER_PASS}' --firstName 'Admin' --lastName 'Owner' >/dev/null 2>&1 || true)"
# Final info
N8N_INTERNAL_URL="http://${CT_IP}:5678/"
N8N_EXTERNAL_URL="https://${FQDN}"
info "Step 7 OK: Stack deployed"
info "n8n intern: ${N8N_INTERNAL_URL}"
info "n8n extern (geplant via OPNsense): ${N8N_EXTERNAL_URL}"
# Machine-readable JSON output (for your downstream automation)
emit_json <<JSON
{
"ctid": ${CTID},
"hostname": "${CT_HOSTNAME}",
"fqdn": "${FQDN}",
"ip": "${CT_IP}",
"vlan": ${VLAN},
"urls": {
"n8n_internal": "${N8N_INTERNAL_URL}",
"n8n_external": "${N8N_EXTERNAL_URL}"
},
"postgres": {
"host": "postgres",
"port": 5432,
"db": "${PG_DB}",
"user": "${PG_USER}",
"password": "${PG_PASSWORD}"
},
"n8n": {
"encryption_key": "${N8N_ENCRYPTION_KEY}",
"owner_email": "${N8N_OWNER_EMAIL}",
"owner_password": "${N8N_OWNER_PASS}",
"secure_cookie": ${N8N_SECURE_COOKIE}
}
}
JSON