#!/usr/bin/env bash set -euo pipefail REPO_URL="https://git.lohmar.co.uk/lexton-it/NextWks.git" BUILD_DIR="/tmp/nextwks-build" TARGET_DIR="/opt/nextworkspace" BACKUP_DIR="/opt/backup" NETWORK_NAME="nextwks-net" HEALTH_CHECK_RETRIES=15 HEALTH_CHECK_INTERVAL=3 usage() { echo "Usage: $0 [--install|--update|--destroy]" echo " --install First-time setup on a bare VM (prompts for config)" echo " --update Smart update: pull, build, copy, bounce containers" echo " --destroy Full greenfield redeploy (uses saved secrets)" exit 1 } [ $# -eq 0 ] && usage MODE="${1#--}" case "$MODE" in install|update|destroy) ;; *) usage ;; esac # Helper: run with sudo if not already root maybe_sudo() { if [ "$(id -u)" -eq 0 ]; then "$@"; else sudo "$@"; fi } # --- Load existing env (if any) --- if [ -f "$BACKUP_DIR/.env" ]; then set -a; source "$BACKUP_DIR/.env"; set +a elif [ -f "$TARGET_DIR/.env" ]; then set -a; source "$TARGET_DIR/.env"; set +a fi DOMAIN="${DOMAIN:-nextwks.eu}" echo "=== NextWorkspace ${MODE} ===" # ============================================================ # 1. INSTALL MODE — first-time setup # ============================================================ if [ "$MODE" = "install" ]; then echo "[*] Installing system dependencies..." maybe_sudo apt-get update -qq maybe_sudo apt-get install -y -qq git build-essential curl podman podman-compose if ! command -v go &>/dev/null; then echo "[*] Installing Go..." GO_VERSION=$(curl -sL https://go.dev/VERSION?m=text) GO_URL="https://go.dev/dl/${GO_VERSION}.linux-amd64.tar.gz" curl -sL "$GO_URL" -o /tmp/go.tar.gz maybe_sudo rm -rf /usr/local/go maybe_sudo tar -C /usr/local -xzf /tmp/go.tar.gz rm /tmp/go.tar.gz maybe_sudo sh -c 'echo "export PATH=\$PATH:/usr/local/go/bin" > /etc/profile.d/go.sh' maybe_sudo chmod +x /etc/profile.d/go.sh export PATH=$PATH:/usr/local/go/bin fi echo "" echo "--- NextWorkspace Configuration ---" read -p "Domain [nextwks.eu]: " input; DOMAIN="${input:-$DOMAIN}" read -p "TLS email (Let's Encrypt): " TLS_EMAIL while [ -z "$TLS_EMAIL" ]; do read -p "TLS email (required): " TLS_EMAIL; done while echo "$TLS_EMAIL" | grep -qv '@'; do read -p "Invalid email: " TLS_EMAIL; done read -p "Admin username: " ADMIN_USERNAME while [ -z "$ADMIN_USERNAME" ]; do read -p "Admin username (required): " ADMIN_USERNAME; done ADMIN_PASSWORD=$(openssl rand -hex 6) echo "" echo "========================================" echo " Domain: $DOMAIN" echo " TLS email: $TLS_EMAIL" echo " Admin username: $ADMIN_USERNAME" echo " Admin password: $ADMIN_PASSWORD" echo " Save this password — it won't be shown again!" echo "========================================" echo "" read -p "SMTP host [smtp.openxchange.eu]: " SMTP_HOST; SMTP_HOST="${SMTP_HOST:-smtp.openxchange.eu}" read -p "SMTP port [587]: " SMTP_PORT; SMTP_PORT="${SMTP_PORT:-587}" read -p "SMTP user [post@nextwks.eu]: " SMTP_USER; SMTP_USER="${SMTP_USER:-post@nextwks.eu}" read -sp "SMTP password: " SMTP_PASS; echo "" [ -z "$SMTP_PASS" ] && echo "ERROR: SMTP password required" && exit 1 read -p "IMAP host [imap.openxchange.eu]: " IMAP_HOST; IMAP_HOST="${IMAP_HOST:-imap.openxchange.eu}" read -p "IMAP port [993]: " IMAP_PORT; IMAP_PORT="${IMAP_PORT:-993}" # Persist config to backup vault maybe_sudo mkdir -p "$BACKUP_DIR" maybe_sudo bash -c "cat > '$BACKUP_DIR/.env' < "$GEN_DIR/Caddyfile" # Authelia config — preserve existing secrets if present JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}" SESSION_SECRET="${SESSION_SECRET:-$(openssl rand -hex 32)}" sed -e "s|{DOMAIN}|$DOMAIN|g" -e "s|{JWT_SECRET}|$JWT_SECRET|g" \ -e "s|{SESSION_SECRET}|$SESSION_SECRET|g" \ -e "s|{SMTP_HOST}|${SMTP_HOST:-smtp.openxchange.eu}|g" \ -e "s|{SMTP_PORT}|${SMTP_PORT:-587}|g" \ -e "s|{SMTP_USER}|${SMTP_USER:-post@nextwks.eu}|g" \ -e "s|{SMTP_PASS}|$SMTP_PASS|g" \ "$BUILD_DIR/config/authelia/configuration.yml" > "$GEN_DIR/configuration.yml" # Users database ADMIN_PASSWORD_HASH="${ADMIN_PASSWORD_HASH:-}" if [ -z "$ADMIN_PASSWORD_HASH" ] && [ -n "${ADMIN_PASSWORD:-}" ]; then ADMIN_PASSWORD_HASH=$(cd "$BUILD_DIR" && go run ./tools/hash-password/ "$ADMIN_PASSWORD" 2>/dev/null || echo "") fi sed -e "s|{ADMIN_PASSWORD_HASH}|$ADMIN_PASSWORD_HASH|g" \ -e "s|{TLS_EMAIL}|${TLS_EMAIL:-admin@$DOMAIN}|g" \ "$BUILD_DIR/config/authelia/users_database.yml" > "$GEN_DIR/users_database.yml" # Copy generated configs to target maybe_sudo cp "$GEN_DIR/Caddyfile" "$TARGET_DIR/config/caddy/Caddyfile" maybe_sudo cp "$GEN_DIR/configuration.yml" "$TARGET_DIR/config/authelia/configuration.yml" maybe_sudo cp "$GEN_DIR/users_database.yml" "$TARGET_DIR/config/authelia/users_database.yml" # Persist generated secrets so --destroy is idempotent if [ -f "$BACKUP_DIR/.env" ]; then maybe_sudo sed -i "/^JWT_SECRET=/d; /^SESSION_SECRET=/d; /^ADMIN_PASSWORD_HASH=/d" "$BACKUP_DIR/.env" 2>/dev/null || true fi maybe_sudo bash -c "echo 'JWT_SECRET=$JWT_SECRET' >> '$BACKUP_DIR/.env'" maybe_sudo bash -c "echo 'SESSION_SECRET=$SESSION_SECRET' >> '$BACKUP_DIR/.env'" [ -n "$ADMIN_PASSWORD_HASH" ] && maybe_sudo bash -c "echo 'ADMIN_PASSWORD_HASH=$ADMIN_PASSWORD_HASH' >> '$BACKUP_DIR/.env'" maybe_sudo chmod 600 "$BACKUP_DIR/.env" fi # ============================================================ # 8. FIX OWNERSHIP — all files in TARGET_DIR/BACKUP_DIR to user # ============================================================ RUN_USER="${SUDO_USER:-${USER}}" echo "[*] Setting file ownership to $RUN_USER..." maybe_sudo chown -R "$RUN_USER:" "$TARGET_DIR" 2>/dev/null || true maybe_sudo chown -R "$RUN_USER:" "$BACKUP_DIR" 2>/dev/null || true # ============================================================ # 9. STOP old containers (all modes, as root) # ============================================================ echo "[*] Stopping any previous containers..." for c in caddy authelia launcher; do maybe_sudo podman stop "$c" 2>/dev/null && echo " stopped $c" || true maybe_sudo podman rm "$c" 2>/dev/null && echo " removed $c" || true done sleep 1 # ============================================================ # 10. PORT CHECK — before deploy # ============================================================ if maybe_sudo ss -tlnp 2>/dev/null | grep -q ':80 '; then echo "==============================================" echo "[WARN] Port 80 is already in use." maybe_sudo ss -tlnp 2>/dev/null | grep ':80 ' echo "" echo " This is usually Caddy from a previous run." echo "==============================================" echo "" read -p "Stop the container on port 80 now? [y/N]: " KILL if [ "$KILL" = "y" ] || [ "$KILL" = "Y" ]; then maybe_sudo podman stop caddy 2>/dev/null || true maybe_sudo podman rm caddy 2>/dev/null || true sleep 2 echo "[OK] Port 80 freed." else echo "[ABORT] Free port 80 first, then re-run." exit 1 fi fi # ============================================================ # 11. DEPLOY stack (as root — needs port 80/443) # ============================================================ echo "[*] Deploying containers on $NETWORK_NAME..." maybe_sudo podman network create "$NETWORK_NAME" 2>/dev/null || true # AUTHELIA_SECRET is SESSION_SECRET (Authelia session.secret) AUTHELIA_SECRET="${SESSION_SECRET:-}" if [ -z "$AUTHELIA_SECRET" ]; then AUTHELIA_SECRET=$(maybe_sudo sed -n '/^session:/,/^[a-z]/p' "$TARGET_DIR/config/authelia/configuration.yml" \ | grep 'secret:' | awk '{print $2}' 2>/dev/null || echo "") fi # Generate compose file with substituted secret sed -e "s|{AUTHELIA_SECRET}|$AUTHELIA_SECRET|g" \ "$BUILD_DIR/compose/stack.yaml" > "$GEN_DIR/stack.yaml" maybe_sudo cp "$GEN_DIR/stack.yaml" "$TARGET_DIR/compose/stack.yaml" maybe_sudo podman-compose -f "$TARGET_DIR/compose/stack.yaml" down 2>/dev/null || true sleep 1 maybe_sudo podman-compose -f "$TARGET_DIR/compose/stack.yaml" up -d 2>&1 || echo "[WARN] Stack deploy had issues" # ============================================================ # 12. HEALTH CHECK # ============================================================ echo "[*] Running health check..." for i in $(seq 1 $HEALTH_CHECK_RETRIES); do HEALTH=$(maybe_sudo podman exec launcher curl -sf http://127.0.0.1:9000/health 2>/dev/null || echo "") if [ "$HEALTH" = "OK" ]; then echo "[OK] NextWorkspace launcher is healthy" echo "[OK] https://$DOMAIN/" exit 0 fi sleep $HEALTH_CHECK_INTERVAL done echo "[FAIL] Health check failed — launcher did not respond" echo "" echo "--- Container status ---" maybe_sudo podman ps -a --filter "name=caddy|authelia|launcher" 2>/dev/null || true echo "" echo "--- Launcher logs (last 20 lines) ---" maybe_sudo podman logs launcher --tail 20 2>/dev/null || echo " (no logs)" exit 1