Files
customer-installer/install.sh
2026-01-09 18:54:01 +01:00

265 lines
8.8 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=libsupabase.sh
source "${SCRIPT_DIR}/libsupabase.sh"
setup_traps
# -------------------------
# Defaults
# -------------------------
CTID=""
CORES="2"
MEMORY="4096"
SWAP="512"
DISK="50"
BRIDGE="vmbr0"
STORAGE="local-zfs"
IPCFG="dhcp"
UNPRIV="1"
TG_TOKEN=""
TG_CHAT=""
DOMAIN="userman.de"
CT_HOSTNAME="sb-$(date +%s)"
usage() {
cat <<'USAGE'
Usage:
bash install.sh [options]
Core options:
--ctid <id> Force CT ID (optional)
--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)
--privileged Create privileged CT (default: unprivileged)
--domain <domain> FQDN base (default: userman.de)
Telegram options:
--tg-token <token> Telegram bot token (optional)
--tg-chat <id> Telegram chat id (optional)
--help Show help
Examples:
bash install.sh
bash install.sh --storage local-zfs --bridge vmbr0 --ip dhcp
bash install.sh --ip 192.168.45.171/24 --bridge vmbr90 --domain userman.de
USAGE
}
# -------------------------
# Args
# -------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--ctid) CTID="${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 ;;
--privileged) UNPRIV="0"; shift 1 ;;
--domain) DOMAIN="${2:-}"; shift 2 ;;
--tg-token) TG_TOKEN="${2:-}"; shift 2 ;;
--tg-chat) TG_CHAT="${2:-}"; shift 2 ;;
--help|-h) usage; exit 0 ;;
*) die "Unknown arg: $1 (use --help)" ;;
esac
done
# -------------------------
# Validate basics
# -------------------------
need_cmd pct pvesm pveam pvesh python3 awk grep date
[[ "$CORES" =~ ^[0-9]+$ ]] || die "--cores must be integer"
[[ "$MEMORY" =~ ^[0-9]+$ ]] || die "--memory must be integer (MB)"
[[ "$SWAP" =~ ^[0-9]+$ ]] || die "--swap must be integer (MB)"
[[ "$DISK" =~ ^[0-9]+$ ]] || die "--disk must be integer (GB)"
[[ "$UNPRIV" == "0" || "$UNPRIV" == "1" ]] || die "Internal: UNPRIV must be 0/1"
[[ -n "$DOMAIN" ]] || die "--domain empty"
if [[ "$IPCFG" != "dhcp" ]]; then
[[ "$IPCFG" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]] || die "--ip must be 'dhcp' or CIDR like 192.168.45.171/24"
fi
info "Argument-Parsing OK"
# -------------------------
# Step 4: Preflight Proxmox
# -------------------------
pve_storage_exists "$STORAGE" || die "Storage not found: ${STORAGE}"
pve_bridge_exists "$BRIDGE" || die "Bridge not found or not a Linux bridge: ${BRIDGE}"
TEMPLATE="$(pve_template_ensure_debian12 "$STORAGE")"
[[ -n "$TEMPLATE" ]] || die "Template selection failed (empty)"
info "Template OK: ${TEMPLATE}"
if [[ -n "$CTID" ]]; then
[[ "$CTID" =~ ^[0-9]+$ ]] || die "--ctid must be integer"
else
CTID="$(pve_next_free_ctid 2000 9999 || true)"
[[ -n "$CTID" ]] || die "CTID selection failed (empty)"
fi
info "CTID selected: ${CTID}"
info "SCRIPT_DIR=${SCRIPT_DIR}"
info "CT_HOSTNAME=${CT_HOSTNAME}"
info "cores=${CORES} memory=${MEMORY}MB swap=${SWAP}MB disk=${DISK}GB"
info "bridge=${BRIDGE} storage=${STORAGE} ip=${IPCFG} unprivileged=${UNPRIV}"
if [[ -n "$TG_TOKEN" && -n "$TG_CHAT" ]]; then
info "Telegram enabled (token/chat provided)"
else
info "Telegram disabled"
fi
# -------------------------
# Step 5: Create CT
# -------------------------
info "Step 5: Create CT"
pve_create_ct "$CTID" "$TEMPLATE" "$CT_HOSTNAME" "$CORES" "$MEMORY" "$SWAP" "$STORAGE" "$DISK" "$BRIDGE" "$IPCFG" "$UNPRIV"
info "CT created (not started). Next step: start CT + wait for IP"
pve_start_ct "$CTID"
CT_IP="$(pve_wait_ct_ip "$CTID" 180 || true)"
[[ -n "$CT_IP" ]] || die "CT IP not obtained (timeout)"
info "Step 5 OK: LXC erstellt + IP ermittelt"
info "CT_HOSTNAME=${CT_HOSTNAME}"
info "CT_IP=${CT_IP}"
# -------------------------
# Step 6: Provision inside CT
# -------------------------
info "Step 6: Provisioning im CT (Docker + Stack)"
# 6.1 Base packages + Docker (Debian 12: docker.io + compose plugin)
ct_exec "$CTID" "export DEBIAN_FRONTEND=noninteractive; apt-get update -y"
ct_exec "$CTID" "export DEBIAN_FRONTEND=noninteractive; apt-get install -y ca-certificates curl gnupg lsb-release apt-transport-https"
ct_exec "$CTID" "export DEBIAN_FRONTEND=noninteractive; apt-get install -y docker.io docker-compose-plugin"
# enable/start docker
ct_exec "$CTID" "systemctl enable --now docker"
# 6.2 customer-stack dirs
ct_exec "$CTID" "mkdir -p /opt/customer-stack/volumes/{n8n-data,n8n-db,pgvector-db} /opt/customer-stack/sql"
ct_exec "$CTID" "chmod 700 /opt/customer-stack/volumes/n8n-db /opt/customer-stack/volumes/pgvector-db || true"
# 6.3 Write init sql for pgvector (copy from host sql/init_pgvector.sql if exists)
if [[ -f "${SCRIPT_DIR}/sql/init_pgvector.sql" ]]; then
ct_push "$CTID" "${SCRIPT_DIR}/sql/init_pgvector.sql" "/opt/customer-stack/sql/init_pgvector.sql"
else
# minimal fallback
cat > /tmp/init_pgvector.sql <<'SQL'
CREATE EXTENSION IF NOT EXISTS vector;
SQL
ct_push "$CTID" "/tmp/init_pgvector.sql" "/opt/customer-stack/sql/init_pgvector.sql"
rm -f /tmp/init_pgvector.sql
fi
# 6.4 Generate docker compose (real stack)
# Notes:
# - n8n runs behind reverse proxy later -> for now disable secure cookie to avoid the browser warning until TLS is in place.
# - WEBHOOK_URL is set to https://<ct_hostname>.<domain>/ (works once proxy/TLS is configured)
# - For local testing without TLS you can set WEBHOOK_URL=http://<ct_ip>:5678/ and N8N_SECURE_COOKIE=false (already false here)
N8N_BASIC_USER="$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 10)"
N8N_BASIC_PASS="$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 20)"
N8N_ENC_KEY="$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 32)"
N8N_DB_PASS="$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 24)"
PGVECTOR_PASS="$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 24)"
# compose content -> temp file -> push
cat > /tmp/docker-compose.yml <<YML
services:
n8n-db:
image: postgres:15-alpine
container_name: n8n-db
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=${N8N_DB_PASS}
- POSTGRES_DB=n8n
volumes:
- /opt/customer-stack/volumes/n8n-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
pgvector:
image: pgvector/pgvector:pg15
container_name: pgvector
environment:
- POSTGRES_USER=vector
- POSTGRES_PASSWORD=${PGVECTOR_PASS}
- POSTGRES_DB=vector
volumes:
- /opt/customer-stack/volumes/pgvector-db:/var/lib/postgresql/data
- /opt/customer-stack/sql/init_pgvector.sql:/docker-entrypoint-initdb.d/init_pgvector.sql:ro
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U vector -d vector"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
n8n:
image: docker.n8n.io/n8nio/n8n:latest
container_name: n8n
depends_on:
n8n-db:
condition: service_healthy
ports:
- "5678:5678"
environment:
- NODE_ENV=production
- GENERIC_TIMEZONE=Europe/Berlin
- N8N_HOST=${CT_HOSTNAME}
- N8N_PORT=5678
- N8N_PROTOCOL=https
- N8N_SECURE_COOKIE=false
- WEBHOOK_URL=https://${CT_HOSTNAME}.${DOMAIN}/
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=${N8N_BASIC_USER}
- N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_PASS}
- N8N_ENCRYPTION_KEY=${N8N_ENC_KEY}
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=n8n-db
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=${N8N_DB_PASS}
volumes:
- /opt/customer-stack/volumes/n8n-data:/home/node/.n8n
restart: unless-stopped
YML
ct_push "$CTID" "/tmp/docker-compose.yml" "/opt/customer-stack/docker-compose.yml"
rm -f /tmp/docker-compose.yml
# Ensure correct permissions for n8n volume (node user inside container is 1000)
ct_exec "$CTID" "chown -R 1000:1000 /opt/customer-stack/volumes/n8n-data"
# 6.5 Start stack
ct_exec "$CTID" "cd /opt/customer-stack && docker compose up -d"
ct_exec "$CTID" "cd /opt/customer-stack && docker compose ps"
info "Step 6 OK: Stack läuft im CT"
info "n8n URL (ohne Proxy/TLS): http://${CT_IP}:5678"
info "n8n URL (mit Proxy/TLS später): https://${CT_HOSTNAME}.${DOMAIN}/"
info "n8n BasicAuth User: ${N8N_BASIC_USER}"
info "n8n BasicAuth Pass: ${N8N_BASIC_PASS}"
info "pgvector DSN: host=${CT_IP} port=5432 db=vector user=vector pass=${PGVECTOR_PASS}"