#!/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 only for commands that need it 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 iptables-persistent 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 # Enable user lingering — containers stay alive after logout maybe_sudo loginctl enable-linger "$USER" 2>/dev/null || true # Clean up any old rootful containers from a previous deploy echo "[*] Cleaning up old rootful containers (if any)..." maybe_sudo podman stop caddy authelia launcher 2>/dev/null || true maybe_sudo podman rm caddy authelia launcher 2>/dev/null || true maybe_sudo podman network rm "$NETWORK_NAME" 2>/dev/null || true # Set up iptables: redirect 80→8080, 443→8443 for rootless Caddy echo "[*] Setting up iptables port redirects (80→8080, 443→8443)..." maybe_sudo iptables -t nat -C PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080 2>/dev/null || \ maybe_sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080 maybe_sudo iptables -t nat -C PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443 2>/dev/null || \ maybe_sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443 maybe_sudo iptables -t nat -C OUTPUT -p tcp --dport 80 -j REDIRECT --to-port 8080 2>/dev/null || \ maybe_sudo iptables -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to-port 8080 maybe_sudo iptables -t nat -C OUTPUT -p tcp --dport 443 -j REDIRECT --to-port 8443 2>/dev/null || \ maybe_sudo iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to-port 8443 # Persist across reboots if command -v netfilter-persistent &>/dev/null; then maybe_sudo netfilter-persistent save 2>/dev/null || true else maybe_sudo mkdir -p /etc/iptables maybe_sudo sh -c 'iptables-save > /etc/iptables/rules.v4' 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' </dev/null || true podman rm caddy authelia launcher 2>/dev/null || true podman network rm "$NETWORK_NAME" 2>/dev/null || true # Wipe target maybe_sudo rm -rf "$TARGET_DIR" maybe_sudo mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \ "$TARGET_DIR/data/caddy" "$TARGET_DIR/data/authelia" \ "$TARGET_DIR/compose" "$TARGET_DIR/www" \ "$TARGET_DIR/config/nextworkspace" "$TARGET_DIR/logs" fi # ============================================================ # 6. COPY artifacts to target (as root) # ============================================================ echo "[*] Copying artifacts..." maybe_sudo cp nextworkspace "$TARGET_DIR/nextworkspace" maybe_sudo cp "$BUILD_DIR/VERSION" "$TARGET_DIR/VERSION" if [ -d "$BUILD_DIR/config/www" ]; then maybe_sudo cp -r "$BUILD_DIR/config/www"/* "$TARGET_DIR/www/" fi if [ -d "$BUILD_DIR/lng" ]; then maybe_sudo rm -rf "$TARGET_DIR/lng" maybe_sudo cp -r "$BUILD_DIR/lng" "$TARGET_DIR/lng" fi if [ -d "$BUILD_DIR/config/nextworkspace" ]; then maybe_sudo cp -r "$BUILD_DIR/config/nextworkspace"/* "$TARGET_DIR/config/nextworkspace/" fi # Restore .env from backup if [ -f "$BACKUP_DIR/.env" ]; then maybe_sudo cp "$BACKUP_DIR/.env" "$TARGET_DIR/.env" maybe_sudo chmod 644 "$TARGET_DIR/.env" fi # ============================================================ # 7. GENERATE config files with placeholder substitution # Write to /tmp first, then sudo cp to target # ============================================================ GEN_DIR=$(mktemp -d) trap "rm -rf '$GEN_DIR'" EXIT if [ "$MODE" != "update" ]; then echo "[*] Generating config files..." # Caddyfile sed -e "s|{DOMAIN}|$DOMAIN|g" -e "s|{TLS_EMAIL}|${TLS_EMAIL:-admin@$DOMAIN}|g" \ "$BUILD_DIR/config/caddy/Caddyfile" > "$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. DEPLOY stack (rootless podman — no sudo!) # ============================================================ echo "[*] Deploying containers on $NETWORK_NAME..." 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=$(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" cp "$GEN_DIR/stack.yaml" "$TARGET_DIR/compose/stack.yaml" podman-compose -f "$TARGET_DIR/compose/stack.yaml" down 2>/dev/null || true sleep 1 podman-compose -f "$TARGET_DIR/compose/stack.yaml" up -d 2>&1 || echo "[WARN] Stack deploy had issues" # ============================================================ # 10. HEALTH CHECK # ============================================================ echo "[*] Running health check..." for i in $(seq 1 $HEALTH_CHECK_RETRIES); do HEALTH=$(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 ---" podman ps -a --filter "name=caddy|authelia|launcher" 2>/dev/null || true echo "" echo "--- Launcher logs (last 20 lines) ---" podman logs launcher --tail 20 2>/dev/null || echo " (no logs)" exit 1