#!/usr/bin/env bash set -euo pipefail log() { echo "[pulse-install] $*"; } die() { echo "[pulse-install] ERROR: $*" >&2; exit 1; } if [ "${EUID:-$(id -u)}" -ne 0 ]; then die "Run as root (or with sudo)." fi read -r -p "Reinstall from scratch (will delete /opt/pulse and ALL volumes)? [y/N]: " WIPE WIPE="${WIPE:-N}" PULSE_DIR="/opt/pulse" if [[ "${WIPE}" =~ ^[Yy]$ ]]; then log "Stopping and removing existing Pulse stack (if any)..." if [ -d "${PULSE_DIR}" ]; then cd "${PULSE_DIR}" || true if [ -f docker-compose.su pawse.yml ]; then true fi if [ -f docker-compose-supabase.yml ]; then COMPOSE_FILES="-f docker-compose-supabase.yml" if [ -f docker-compose.override.yml ]; then COMPOSE_FILES="$COMPOSE_FILES -f docker-compose.override.yml" fi if [ -f docker-compose.noise.yml ]; then COMPOSE_FILES="$COMPOSE_FILES -f docker-compose.noise.yml" fi docker compose $COMPOSE_FILES down -v --remove-orphans || true fi fi rm -rf "${PULSE_DIR}" fi read -r -p "Domain for Pulse (e.g. chat.example.com): " DOMAIN if [ -z "${DOMAIN}" ]; then die "Domain is required." fi read -r -p "Enable Caddy HTTPS proxy? [y/N]: " ENABLE_CADDY ENABLE_CADDY="${ENABLE_CADDY:-N}" if [[ "${ENABLE_CADDY}" =~ ^[Yy]$ ]]; then read -r -p "TLS email (for Let's Encrypt): " TLS_EMAIL if [ -z "${TLS_EMAIL}" ]; then die "TLS email is required when enabling Caddy." fi fi read -r -p "Postgres password (leave blank to generate): " POSTGRES_PASSWORD if [ -z "${POSTGRES_PASSWORD}" ]; then POSTGRES_PASSWORD="$(openssl rand -hex 16)" log "Generated Postgres password: ${POSTGRES_PASSWORD}" fi read -r -p "Pulse port [4991]: " PULSE_PORT PULSE_PORT="${PULSE_PORT:-4991}" read -r -p "Public IP (optional; leave blank to auto-detect): " PUBLIC_IP if [ -z "${PUBLIC_IP}" ]; then PUBLIC_IP="$(curl -fsSL https://api.ipify.org || true)" if [ -n "${PUBLIC_IP}" ]; then log "Detected public IP: ${PUBLIC_IP}" else log "Public IP not set." fi fi log "Installing base packages..." apt-get update -y apt-get install -y ca-certificates curl gnupg git ufw openssl if ! command -v docker >/dev/null 2>&1; then log "Installing Docker..." install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg . /etc/os-release echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian ${VERSION_CODENAME} stable" \ | tee /etc/apt/sources.list.d/docker.list > /dev/null apt-get update -y apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin fi if ! command -v bun >/dev/null 2>&1; then log "Installing Bun..." curl -fsSL https://bun.sh/install | bash fi export BUN_INSTALL="${BUN_INSTALL:-/root/.bun}" export PATH="${BUN_INSTALL}/bin:${PATH}" log "Cloning Pulse..." mkdir -p "${PULSE_DIR}" if [ -d "${PULSE_DIR}/.git" ]; then git -C "${PULSE_DIR}" fetch --all --prune git -C "${PULSE_DIR}" reset --hard origin/main else git clone https://github.com/plsechat/pulse-chat.git "${PULSE_DIR}" fi cd "${PULSE_DIR}" log "Generating JWT and Supabase keys..." KEY_OUTPUT="$(bun docker/generate-keys.ts)" JWT_SECRET="$(echo "${KEY_OUTPUT}" | grep -E '^JWT_SECRET=' | head -n1 | cut -d'=' -f2-)" SUPABASE_ANON_KEY="$(echo "${KEY_OUTPUT}" | grep -E '^SUPABASE_ANON_KEY=' | head -n1 | cut -d'=' -f2-)" SUPABASE_SERVICE_ROLE_KEY="$(echo "${KEY_OUTPUT}" | grep -E '^SUPABASE_SERVICE_ROLE_KEY=' | head -n1 | cut -d'=' -f2-)" if [ -z "${JWT_SECRET}" ] || [ -z "${SUPABASE_ANON_KEY}" ] || [ -z "${SUPABASE_SERVICE_ROLE_KEY}" ]; then die "Failed to parse generated keys." fi log "Writing .env..." cp -f .env.supabase.example .env sed -i \ -e "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=${POSTGRES_PASSWORD}|" \ -e "s|^JWT_SECRET=.*|JWT_SECRET=${JWT_SECRET}|" \ -e "s|^SUPABASE_ANON_KEY=.*|SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}|" \ -e "s|^SUPABASE_SERVICE_ROLE_KEY=.*|SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY}|" \ -e "s|^SITE_URL=.*|SITE_URL=https://${DOMAIN}|" \ .env if [ -n "${PUBLIC_IP}" ]; then if grep -q '^PUBLIC_IP=' .env; then sed -i "s|^PUBLIC_IP=.*|PUBLIC_IP=${PUBLIC_IP}|" .env else echo "PUBLIC_IP=${PUBLIC_IP}" >> .env fi fi if grep -q '^PULSE_PORT=' .env; then sed -i "s|^PULSE_PORT=.*|PULSE_PORT=${PULSE_PORT}|" .env else echo "PULSE_PORT=${PULSE_PORT}" >> .env fi if [[ "${ENABLE_CADDY}" =~ ^[Yy]$ ]]; then log "Writing Caddyfile and compose override..." cat > "${PULSE_DIR}/Caddyfile" < "${PULSE_DIR}/docker-compose.override.yml" <<'EOF' services: caddy: image: caddy:2 restart: unless-stopped ports: - "80:80" - "443:443" - "443:443/udp" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config volumes: caddy_data: caddy_config: EOF else rm -f "${PULSE_DIR}/docker-compose.override.yml" "${PULSE_DIR}/Caddyfile" || true fi log "Configuring firewall (ufw)..." ufw allow OpenSSH >/dev/null 2>&1 || true if [[ "${ENABLE_CADDY}" =~ ^[Yy]$ ]]; then ufw allow 80/tcp >/dev/null 2>&1 || true ufw allow 443/tcp >/dev/null 2>&1 || true ufw allow 443/udp >/dev/null 2>&1 || true else ufw allow "${PULSE_PORT}/tcp" >/dev/null 2>&1 || true fi ufw allow 40000:40020/tcp >/dev/null 2>&1 || true ufw allow 40000:40020/udp >/dev/null 2>&1 || true ufw --force enable >/dev/null 2>&1 || true log "Starting Pulse (Supabase stack)..." COMPOSE_FILES="-f docker-compose-supabase.yml" if [ -f docker-compose.override.yml ]; then COMPOSE_FILES="$COMPOSE_FILES -f docker-compose.override.yml" fi docker compose $COMPOSE_FILES up -d --build log "Done." if [[ "${ENABLE_CADDY}" =~ ^[Yy]$ ]]; then log "Pulse: https://${DOMAIN}" else log "Pulse: http://${DOMAIN}:${PULSE_PORT}" fi log "If this is the first run, watch logs to get the security token:" log " docker logs -f pulse | head -n 200"