#!/usr/bin/env bash set -euo pipefail # setup-project.sh — One-time VPS setup for a new project. # # Run as root on the VPS: # bash setup-project.sh [port] # # Example: # bash setup-project.sh my-app app.spektr.design 3000 # # What it does: # 1. Creates service user (no login shell, no home dir) # 2. Creates /opt/ directory # 3. Installs deploy script to /usr/local/bin/ # 4. Adds sudoers rule for Woodpecker agent # 5. Creates systemd service # 6. Creates nginx config (location blocks for Webuzo) # # After running, you still need to: # - Create .env file at /opt//.env # - Set up SSL (certbot or Webuzo panel) # - Push code to Gitea to trigger first deploy # --------------------------------------------------------------------------- # Args # --------------------------------------------------------------------------- PROJECT="${1:?Usage: setup-project.sh [port]}" DOMAIN="${2:?Usage: setup-project.sh [port]}" PORT="${3:-3000}" INSTALL_DIR="/opt/${PROJECT}" SERVICE_USER="${PROJECT}" DEPLOY_SCRIPT="/usr/local/bin/deploy-${PROJECT}" # Detect Woodpecker agent user AGENT_USER=$(ps -o user= -p "$(pgrep woodpecker-agent)" 2>/dev/null || echo "") if [[ -z "${AGENT_USER}" ]]; then echo "Warning: Woodpecker agent not running. Defaulting to 'git'." AGENT_USER="git" fi echo "=== Setting up project: ${PROJECT} ===" echo " Domain: ${DOMAIN}" echo " Port: ${PORT}" echo " Install dir: ${INSTALL_DIR}" echo " User: ${SERVICE_USER}" echo " Agent user: ${AGENT_USER}" echo "" # --------------------------------------------------------------------------- # Checks # --------------------------------------------------------------------------- if [[ $EUID -ne 0 ]]; then echo "Error: run as root (sudo)"; exit 1 fi if [[ -d "${INSTALL_DIR}" ]]; then echo "Warning: ${INSTALL_DIR} already exists. Continuing..." fi # --------------------------------------------------------------------------- # 1. Service user # --------------------------------------------------------------------------- echo "[1/6] Creating service user..." if id "${SERVICE_USER}" &>/dev/null; then echo " User ${SERVICE_USER} already exists" else useradd -r -s /sbin/nologin "${SERVICE_USER}" echo " Created user ${SERVICE_USER}" fi # --------------------------------------------------------------------------- # 2. Project directory # --------------------------------------------------------------------------- echo "[2/6] Creating project directory..." mkdir -p "${INSTALL_DIR}" chown "${SERVICE_USER}:${SERVICE_USER}" "${INSTALL_DIR}" echo " ${INSTALL_DIR} ready" # --------------------------------------------------------------------------- # 3. Deploy script # --------------------------------------------------------------------------- echo "[3/6] Installing deploy script..." cat > "${DEPLOY_SCRIPT}" << DEPLOY #!/usr/bin/env bash set -euo pipefail INSTALL_DIR="${INSTALL_DIR}" SERVICE_USER="${SERVICE_USER}" SERVICE_NAME="${PROJECT}" HEALTH_URL="http://localhost:${PORT}/api/health" SOURCE_DIR="\${1:-.}" echo "=== Deploy ${PROJECT} ===" if [[ \$EUID -ne 0 ]]; then echo "Error: must be root"; exit 1 fi if [[ ! -d "\${INSTALL_DIR}" ]]; then echo "Error: \${INSTALL_DIR} not found"; exit 1 fi echo "[1/3] Syncing files..." rsync -a --delete --exclude='node_modules' --exclude='.env' --exclude='.git' --exclude='.idea' "\${SOURCE_DIR}/" "\${INSTALL_DIR}/" chown -R "\${SERVICE_USER}:\${SERVICE_USER}" "\${INSTALL_DIR}" echo "[2/3] Installing dependencies..." sudo -u "\${SERVICE_USER}" HOME="\${INSTALL_DIR}" bash -c "cd \${INSTALL_DIR} && npm ci --omit=dev" echo "[3/3] Restarting service..." systemctl daemon-reload systemctl restart "\${SERVICE_NAME}" sleep 2 if systemctl is-active --quiet "\${SERVICE_NAME}"; then echo "Service running" else echo "Error: service failed"; exit 1 fi curl -sf "\${HEALTH_URL}" || { echo "FAIL: health check"; exit 1; } echo "=== Deploy complete ===" DEPLOY chmod 755 "${DEPLOY_SCRIPT}" # Strip trailing whitespace (heredoc can add it) sed -i 's/[[:space:]]*$//' "${DEPLOY_SCRIPT}" echo " ${DEPLOY_SCRIPT} installed" # --------------------------------------------------------------------------- # 4. Sudoers # --------------------------------------------------------------------------- echo "[4/6] Configuring sudoers..." SUDOERS_FILE="/etc/sudoers.d/woodpecker-${PROJECT}" echo "${AGENT_USER} ALL=(root) NOPASSWD: ${DEPLOY_SCRIPT} *" > "${SUDOERS_FILE}" chmod 0440 "${SUDOERS_FILE}" if visudo -cf "${SUDOERS_FILE}" &>/dev/null; then echo " ${SUDOERS_FILE} — parsed OK" else echo " ERROR: sudoers syntax check failed!" rm -f "${SUDOERS_FILE}" exit 1 fi # --------------------------------------------------------------------------- # 5. Systemd service # --------------------------------------------------------------------------- echo "[5/6] Creating systemd service..." cat > "/etc/systemd/system/${PROJECT}.service" << SERVICE [Unit] Description=${PROJECT} After=network-online.target Wants=network-online.target [Service] Type=simple User=${SERVICE_USER} Group=${SERVICE_USER} WorkingDirectory=${INSTALL_DIR} ExecStart=/usr/bin/node ${INSTALL_DIR}/src/index.js Restart=on-failure RestartSec=5 StartLimitBurst=5 StartLimitIntervalSec=60 EnvironmentFile=-${INSTALL_DIR}/.env NoNewPrivileges=true PrivateTmp=true ProtectHome=true ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true LimitNOFILE=65536 MemoryMax=256M StandardOutput=journal StandardError=journal SyslogIdentifier=${PROJECT} [Install] WantedBy=multi-user.target SERVICE systemctl daemon-reload echo " ${PROJECT}.service created" # --------------------------------------------------------------------------- # 6. Nginx config # --------------------------------------------------------------------------- echo "[6/6] Creating nginx config..." NGINX_DIR="/var/webuzo-data/nginx/custom/domains" if [[ -d "${NGINX_DIR}" ]]; then NGINX_CONF="${NGINX_DIR}/${DOMAIN}.conf" else NGINX_DIR="/etc/nginx/conf.d" NGINX_CONF="${NGINX_DIR}/${PROJECT}.conf" fi cat > "${NGINX_CONF}" << 'NGINX' # Security headers — at server block level so they are inherited by all # location blocks (including Webuzo's own regex location). # Do NOT move into location blocks: on Webuzo the regex location # location ~ (\.php|shtml|/)$ takes priority and blocks inheritance. add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "no-referrer" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; # Static files location / { root __INSTALL_DIR__/public; index index.html; try_files $uri $uri/ /index.html; } # API proxy location ^~ /api/ { proxy_pass http://127.0.0.1:__PORT__; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 10s; proxy_send_timeout 30s; proxy_read_timeout 30s; } # WebSocket proxy (if needed) location ^~ /ws/ { proxy_pass http://127.0.0.1:__PORT__; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_pass_header Upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 10s; proxy_send_timeout 86400s; proxy_read_timeout 86400s; proxy_buffering off; } NGINX sed -i "s|__INSTALL_DIR__|${INSTALL_DIR}|g" "${NGINX_CONF}" sed -i "s|__PORT__|${PORT}|g" "${NGINX_CONF}" NGINX_BIN="/usr/local/apps/nginx/sbin/nginx" if [[ ! -x "${NGINX_BIN}" ]]; then NGINX_BIN="nginx" fi if "${NGINX_BIN}" -t 2>/dev/null; then "${NGINX_BIN}" -s reload echo " ${NGINX_CONF} — nginx reloaded" else echo " Warning: nginx config test failed. Check ${NGINX_CONF} manually." fi # --------------------------------------------------------------------------- # Done # --------------------------------------------------------------------------- echo "" echo "=== Setup complete ===" echo "" echo "Next steps:" echo " 1. Create .env: nano ${INSTALL_DIR}/.env" echo " 2. Push code to Gitea — Woodpecker will deploy automatically" echo " 3. Check: curl -s http://localhost:${PORT}/api/health" echo " 4. Uncomment deploy step in .woodpecker.yml:" echo " sudo /usr/local/bin/deploy-${PROJECT} \"\${CI_WORKSPACE:-.}\""