#!/bin/bash # ============================================================ # NextWks — Production Deploy Script # Idempotent: safe for first-time setup and subsequent updates. # Run: sudo bash deploy.sh # ============================================================ set -euo pipefail RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' info() { echo -e "${BLUE}[+]${NC} $1"; } ok() { echo -e "${GREEN}[✓]${NC} $1"; } warn() { echo -e "${YELLOW}[!]${NC} $1"; } die() { echo -e "${RED}[✗]${NC} $1"; exit 1; } header(){ echo -e "\n${BOLD}${CYAN}── $1 ──${NC}"; } # ============================================================ # CONFIG # ============================================================ INSTALL_DIR="/opt/nextworkspace" SRC_DIR="${INSTALL_DIR}/src/core" CONFIG_DIR="${INSTALL_DIR}/config/nextworkspace" DATA_DIR="${INSTALL_DIR}/data/core" CERTS_DIR="${INSTALL_DIR}/data/certs" LOGS_DIR="${INSTALL_DIR}/logs" STATIC_DIR="${INSTALL_DIR}/static" BIN_PATH="${INSTALL_DIR}/core" CONFIG_PATH="${CONFIG_DIR}/config.yaml" SERVICE_FILE="/etc/systemd/system/nextwks.service" SVC_USER="nextwks" REPO_URL="https://git.lohmar.co.uk/lexton-it/NextWks.git" # Must be root [ "${EUID:-$(id -u)}" -ne 0 ] && die "Run as root: sudo bash deploy.sh" # ============================================================ # PHASE 1: DIRECTORY SCAFFOLD + SERVICE USER # ============================================================ header "Phase 1: Directory Structure" if ! id "$SVC_USER" &>/dev/null; then useradd -r -s /usr/sbin/nologin -d /nonexistent "$SVC_USER" ok "Created service user: $SVC_USER" else info "Service user exists: $SVC_USER" fi mkdir -p "$SRC_DIR" "$CONFIG_DIR/apps.d" "$DATA_DIR" "$CERTS_DIR" "$LOGS_DIR" "$STATIC_DIR" chown -R "$SVC_USER:$SVC_USER" "$DATA_DIR" "$CERTS_DIR" "$LOGS_DIR" "$STATIC_DIR" ok "Directory tree created at $INSTALL_DIR/" # ============================================================ # PHASE 2: FETCH + BUILD # ============================================================ header "Phase 2: Build" if [ -d "$SRC_DIR/.git" ]; then info "Pulling latest from origin..." cd "$SRC_DIR" git fetch origin main --quiet git reset --hard origin/main --quiet ok "Repo updated" else info "Cloning repository..." git clone --depth 1 "$REPO_URL" "$SRC_DIR" --quiet cd "$SRC_DIR" ok "Repo cloned" fi VERSION=$(cat "$SRC_DIR/VERSION" 2>/dev/null || echo "dev") COMMIT_SHA=$(cd "$SRC_DIR" && git rev-parse --short HEAD) BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") if ! command -v go &>/dev/null; then die "Go is not installed. Run: apt install golang-go" fi info "Building v${VERSION} (${COMMIT_SHA})..." cd "$SRC_DIR/src" go build -ldflags="-s -w \ -X git.lohmar.co.uk/lexton-it/NextWks/core/version.Version=${VERSION} \ -X git.lohmar.co.uk/lexton-it/NextWks/core/version.BuildTime=${BUILD_TIME} \ -X git.lohmar.co.uk/lexton-it/NextWks/core/version.CommitSHA=${COMMIT_SHA}" \ -o "$BIN_PATH" . chmod 755 "$BIN_PATH" chown "$SVC_USER:$SVC_USER" "$BIN_PATH" ok "Binary: $BIN_PATH" # ============================================================ # PHASE 3: CONFIG + ASSETS # ============================================================ header "Phase 3: Configuration" if [ ! -f "$CONFIG_PATH" ]; then ADMIN_TOKEN=$(openssl rand -hex 32 2>/dev/null || head -c32 /dev/urandom | xxd -p -c32) SESSION_SECRET=$(openssl rand -hex 32 2>/dev/null || head -c32 /dev/urandom | xxd -p -c32) cat > "$CONFIG_PATH" << CONFIGEOF # NextWks — Production Configuration # Generated: $(date) server: host: "0.0.0.0" port: 80 admin: secret_token: "${ADMIN_TOKEN}" database: type: "sqlite" path: "${DATA_DIR}/nextwks.db" authelia: host: "http://127.0.0.1:9091" config_path: "/opt/authelia/configuration.yml" users_db_path: "/opt/authelia/users_database.yml" oidc: issuer_url: "https://app.nextwks.eu/auth" client_id: "nextwks" client_secret: "" redirect_url: "https://app.nextwks.eu/access" smtp: host: "" port: 587 username: "" password: "" from: "noreply@nextwks.local" session: secret: "${SESSION_SECRET}" expiry_minutes: 60 tls: enabled: true domain: "" email: "" storage_path: "${CERTS_DIR}" staging: false cert_file: "${CERTS_DIR}/cert.pem" key_file: "${CERTS_DIR}/key.pem" CONFIGEOF chmod 600 "$CONFIG_PATH" chown "$SVC_USER:$SVC_USER" "$CONFIG_PATH" ok "Config: $CONFIG_PATH" warn "IMPORTANT: Edit $CONFIG_PATH with your domain, email, and SMTP settings!" else ok "Config exists (not overwritten): $CONFIG_PATH" fi # Generate self-signed TLS certificate if needed if [ ! -f "${CERTS_DIR}/cert.pem" ] || [ ! -f "${CERTS_DIR}/key.pem" ]; then DOMAIN="${TLS_DOMAIN:-test.nextwks.eu}" info "Generating self-signed TLS certificate for ${DOMAIN}..." openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout "${CERTS_DIR}/key.pem" \ -out "${CERTS_DIR}/cert.pem" \ -subj "/CN=${DOMAIN}" 2>/dev/null chmod 600 "${CERTS_DIR}/key.pem" chown -R "$SVC_USER:$SVC_USER" "$CERTS_DIR" ok "Self-signed certificate generated for ${DOMAIN}" fi if [ -d "$SRC_DIR/app/static" ]; then cp -r "$SRC_DIR/app/static/"* "$STATIC_DIR/" 2>/dev/null || true chown -R "$SVC_USER:$SVC_USER" "$STATIC_DIR" ok "Static assets copied" fi # ============================================================ # PHASE 4: SYSTEMD # ============================================================ header "Phase 4: Systemd" cat > "$SERVICE_FILE" << SERVICEEOF [Unit] Description=Next Workspace (NextWks) Core Documentation=https://git.lohmar.co.uk/lexton-it/NextWks After=network-online.target authelia.service Wants=network-online.target authelia.service [Service] Type=simple User=${SVC_USER} Group=${SVC_USER} WorkingDirectory=${INSTALL_DIR} ExecStart=${BIN_PATH} -config ${CONFIG_PATH} Restart=on-failure RestartSec=5 AmbientCapabilities=CAP_NET_BIND_SERVICE NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes ReadWritePaths=${DATA_DIR} ${LOGS_DIR} ${CERTS_DIR} StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target SERVICEEOF systemctl daemon-reload systemctl enable nextwks 2>/dev/null || true systemctl restart nextwks ok "Systemd unit installed and service restarted" # ============================================================ # PHASE 5: HEALTH CHECK # ============================================================ header "Phase 5: Health Check" sleep 2 if curl -s --max-time 5 http://localhost:8080/api/health 2>/dev/null | grep -q '"status":"ok"'; then ok "Health check passed — NextWks v${VERSION} (${COMMIT_SHA}) is running" else warn "Health check failed — check logs: journalctl -u nextwks -n 30" fi echo "" echo -e "${GREEN}════════════════════════════════════════${NC}" echo -e "${GREEN} NextWks v${VERSION} deployed${NC}" echo -e "${GREEN}════════════════════════════════════════${NC}" echo "" info " Binary: ${BIN_PATH}" info " Config: ${CONFIG_PATH}" info " Data: ${DATA_DIR}" info " Logs: journalctl -u nextwks -f" info " Version: ${VERSION} (${COMMIT_SHA})" echo ""