#!/usr/bin/env bash # ============================================================================= # ETC PRS — Initial Deployment Script # Targets: Ubuntu 24.04 LTS on a fresh Digital Ocean droplet # Usage: sudo bash deploy.sh # ============================================================================= set -euo pipefail # ── Colours ─────────────────────────────────────────────────────────────────── RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m' CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' info() { echo -e "${CYAN}▸ $*${RESET}"; } success() { echo -e "${GREEN}✓ $*${RESET}"; } warn() { echo -e "${YELLOW}⚠ $*${RESET}"; } error() { echo -e "${RED}✗ $*${RESET}"; exit 1; } header() { echo -e "\n${BOLD}${CYAN}══ $* ══${RESET}\n"; } divider() { echo -e "${CYAN}─────────────────────────────────────────────────${RESET}"; } # ── Root check ──────────────────────────────────────────────────────────────── if [[ $EUID -ne 0 ]]; then error "This script must be run as root (sudo bash deploy.sh)" fi # ── Banner ──────────────────────────────────────────────────────────────────── clear echo -e "${BOLD}${CYAN}" echo " ███████╗████████╗ ██████╗ ██████╗ ██████╗ ███████╗" echo " ██╔════╝╚══██╔══╝██╔════╝ ██╔══██╗██╔══██╗██╔════╝" echo " █████╗ ██║ ██║ ██████╔╝██████╔╝███████╗" echo " ██╔══╝ ██║ ██║ ██╔═══╝ ██╔══██╗╚════██║" echo " ███████╗ ██║ ╚██████╗ ██║ ██║ ██║███████║" echo " ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝" echo -e "${RESET}" echo -e " ${BOLD}Personality Editor — Initial Deployment${RESET}" echo -e " ${CYAN}Targeting Ubuntu 24.04 · Node 20 · PM2 · Nginx · Certbot${RESET}" divider echo "" # ── Interactive prompts ─────────────────────────────────────────────────────── header "Configuration" # Domain while true; do read -rp "$(echo -e "${BOLD}Domain name${RESET} (e.g. etcprs.app): ")" DOMAIN DOMAIN="${DOMAIN#www.}" # strip leading www. if provided DOMAIN="${DOMAIN%/}" # strip trailing slash [[ -n "$DOMAIN" ]] && break warn "Domain cannot be empty." done # Gitea repo echo "" read -rp "$(echo -e "${BOLD}Gitea repo path${RESET} [default: raine/etc-prs-ui]: ")" REPO_PATH REPO_PATH="${REPO_PATH:-raine/etc-prs-ui}" REPO_URL="https://git.etcprs.app/${REPO_PATH}.git" echo -e " ${CYAN}→ ${REPO_URL}${RESET}" # Gitea credentials (for private repos) echo "" echo -e "${YELLOW}If your Gitea repo is private, enter credentials below." echo -e "Leave blank if the repo is public.${RESET}" read -rp "$(echo -e "${BOLD}Gitea username${RESET} (blank = public): ")" GITEA_USER if [[ -n "$GITEA_USER" ]]; then read -rsp "$(echo -e "${BOLD}Gitea password / token${RESET}: ")" GITEA_PASS echo "" REPO_URL="https://${GITEA_USER}:${GITEA_PASS}@git.etcprs.app/${REPO_PATH}.git" fi # Admin account echo "" echo -e "${BOLD}Create the first admin account:${RESET}" while true; do read -rp " Admin username (2–32 chars): " ADMIN_USER [[ ${#ADMIN_USER} -ge 2 && ${#ADMIN_USER} -le 32 ]] && break warn "Username must be 2–32 characters." done while true; do read -rsp " Admin password (min 8 chars): " ADMIN_PASS echo "" [[ ${#ADMIN_PASS} -ge 8 ]] && break warn "Password must be at least 8 characters." done read -rsp " Confirm password: " ADMIN_PASS_CONFIRM echo "" [[ "$ADMIN_PASS" == "$ADMIN_PASS_CONFIRM" ]] || error "Passwords do not match." # Rate limits echo "" echo -e "${BOLD}Rate limits${RESET} (press Enter to accept defaults):" read -rp " Publish rate limit per IP/hour [default: 5]: " RATE_PUBLISH RATE_PUBLISH="${RATE_PUBLISH:-5}" read -rp " Read rate limit per IP/hour [default: 100]: " RATE_READ RATE_READ="${RATE_READ:-100}" # SSL email echo "" read -rp "$(echo -e "${BOLD}Email for SSL certificate${RESET} (Let's Encrypt notices): ")" SSL_EMAIL [[ -n "$SSL_EMAIL" ]] || error "SSL email is required." # ── Confirmation ────────────────────────────────────────────────────────────── echo "" divider echo -e "${BOLD} Deployment Summary${RESET}" divider echo -e " Domain: ${CYAN}${DOMAIN}${RESET} (and www.${DOMAIN})" echo -e " Repo: ${CYAN}${REPO_URL//:*@/:***@}${RESET}" echo -e " Admin user: ${CYAN}${ADMIN_USER}${RESET}" echo -e " Rate limits: publish=${RATE_PUBLISH}/hr read=${RATE_READ}/hr" echo -e " SSL email: ${CYAN}${SSL_EMAIL}${RESET}" divider echo "" read -rp "$(echo -e "${BOLD}Proceed with deployment? [y/N]: ${RESET}")" CONFIRM [[ "${CONFIRM,,}" == "y" ]] || { echo "Aborted."; exit 0; } # ── Constants ───────────────────────────────────────────────────────────────── APP_USER="prs" APP_DIR="/opt/etc-prs/app" DATA_DIR="/var/lib/etc-prs" LOG_DIR="/var/log/etc-prs" BACKUP_DIR="/var/backups/etc-prs" BACKUP_SCRIPT="/opt/etc-prs/backup.sh" NGINX_CONF="/etc/nginx/sites-available/etc-prs" # ── Step 1: System packages ─────────────────────────────────────────────────── header "Step 1 / 9 — System Packages" info "Updating package lists…" apt-get update -qq info "Upgrading installed packages…" DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -qq info "Installing build tools, Nginx, Certbot, SQLite…" DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ build-essential python3 git nginx certbot python3-certbot-nginx sqlite3 ufw curl info "Installing Node.js 20 LTS via NodeSource…" curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1 DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nodejs NODE_VER=$(node --version) NPM_VER=$(npm --version) success "Node ${NODE_VER} / npm ${NPM_VER} installed" info "Installing PM2 globally…" npm install -g pm2 --quiet success "PM2 $(pm2 --version) installed" # ── Step 2: Users & directories ─────────────────────────────────────────────── header "Step 2 / 9 — Users & Directories" if ! id "$APP_USER" &>/dev/null; then info "Creating system user '${APP_USER}'…" useradd --system --shell /bin/false --home /opt/etc-prs "$APP_USER" success "User '${APP_USER}' created" else success "User '${APP_USER}' already exists" fi for DIR in /opt/etc-prs "$APP_DIR" "$DATA_DIR" "$LOG_DIR" "$BACKUP_DIR"; do mkdir -p "$DIR" done chown -R "${APP_USER}:${APP_USER}" /opt/etc-prs "$DATA_DIR" "$LOG_DIR" "$BACKUP_DIR" success "Directories created and ownership set" # ── Step 3: Clone from Gitea ────────────────────────────────────────────────── header "Step 3 / 9 — Clone Repository" info "Cloning from ${REPO_URL//:*@/:***@}…" if [[ -d "${APP_DIR}/.git" ]]; then warn "App directory already contains a git repo — pulling latest instead." git -C "$APP_DIR" pull else git clone "$REPO_URL" "$APP_DIR" 2>&1 | \ sed 's/'"${GITEA_PASS:-NOPASS}"'/***REDACTED***/g' || \ error "Git clone failed. Check your repo URL and credentials." fi chown -R "${APP_USER}:${APP_USER}" "$APP_DIR" success "Repository cloned to ${APP_DIR}" # ── Step 4: Environment file ────────────────────────────────────────────────── header "Step 4 / 9 — Environment Configuration" ENV_FILE="${APP_DIR}/.env" cat > "$ENV_FILE" < "$ECOSYSTEM" < "$NGINX_CONF" < "$BACKUP_SCRIPT" <<'EOF' #!/usr/bin/env bash DATE=$(date +%Y-%m-%d_%H%M) BACKUP_DIR=/var/backups/etc-prs DB_PATH=/var/lib/etc-prs/personalities.db mkdir -p "$BACKUP_DIR" sqlite3 "$DB_PATH" ".backup ${BACKUP_DIR}/personalities-${DATE}.db" gzip "${BACKUP_DIR}/personalities-${DATE}.db" # Keep only the last 30 days find "$BACKUP_DIR" -name "personalities-*.db.gz" -mtime +30 -delete echo "$(date '+%Y-%m-%d %H:%M:%S') Backup OK: personalities-${DATE}.db.gz" EOF chmod +x "$BACKUP_SCRIPT" # Add cron job (2am daily) — idempotent CRON_LINE="0 2 * * * ${BACKUP_SCRIPT} >> ${LOG_DIR}/backup.log 2>&1" ( crontab -l 2>/dev/null | grep -v "$BACKUP_SCRIPT"; echo "$CRON_LINE" ) | crontab - success "Backup cron job scheduled (daily at 2am)" # Firewall info "Configuring UFW firewall…" ufw allow OpenSSH > /dev/null ufw allow 'Nginx Full' > /dev/null ufw --force enable > /dev/null success "Firewall enabled (SSH + HTTP + HTTPS)" # Admin account info "Creating admin account '${ADMIN_USER}'…" cd "$APP_DIR" node scripts/create-admin.js "$ADMIN_USER" "$ADMIN_PASS" || \ warn "Admin creation failed — run manually: cd ${APP_DIR} && node scripts/create-admin.js " success "Admin account '${ADMIN_USER}' created" # ── Done ────────────────────────────────────────────────────────────────────── echo "" divider echo -e "${BOLD}${GREEN} Deployment complete!${RESET}" divider echo "" echo -e " ${BOLD}Site:${RESET} https://${DOMAIN}" echo -e " ${BOLD}Admin panel:${RESET} https://${DOMAIN}/admin" echo -e " ${BOLD}App logs:${RESET} pm2 logs etc-prs" echo -e " ${BOLD}DB path:${RESET} ${DATA_DIR}/personalities.db" echo -e " ${BOLD}Backups:${RESET} ${BACKUP_DIR}" echo "" echo -e " ${BOLD}Redeploy after a push:${RESET}" echo -e " ${CYAN} cd ${APP_DIR} && git pull && \\" echo -e " npm install && npm run build && \\" echo -e " chown -R ${APP_USER}:${APP_USER} ${APP_DIR} && \\" echo -e " pm2 reload etc-prs${RESET}" echo "" divider echo "" # ── Verify app is responding ────────────────────────────────────────────────── info "Checking app is responding on port 3000…" sleep 2 if curl -sf http://127.0.0.1:3000 > /dev/null; then success "App is up and responding ✓" else warn "App did not respond on port 3000. Check logs:" warn " sudo -u ${APP_USER} pm2 logs etc-prs --lines 30" fi echo ""