Files
the-door/deploy/deploy.sh
Alexander Whitestone a90b659f3a
Some checks failed
Sanity Checks / sanity-test (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Successful in 4s
feat(deploy): add systemd service for hermes-gateway
- Add hermes-gateway.service with restart=always and security hardening
- Integrate service setup into deploy.sh
- Add --service flag for standalone install
- Add make service target

Resolves #2
2026-04-13 02:16:19 -04:00

329 lines
9.9 KiB
Bash

#!/bin/bash
# ================================================================
# The Door — Deploy Script
# ================================================================
# The crisis front door. Deploy to VPS.
#
# Usage:
# bash deploy/deploy.sh # Full deploy (swap + nginx + site + firewall + hermes service)
# bash deploy/deploy.sh --site # Site files only (fast update)
# bash deploy/deploy.sh --ssl # SSL setup only
# bash deploy/deploy.sh --service # Install/restart hermes-gateway systemd service
# bash deploy/deploy.sh --check # Verify deployment health
#
# This script is IDEMPOTENT — safe to run repeatedly.
# Run on VPS as root: bash deploy/deploy.sh
# ================================================================
set -euo pipefail
DOMAIN="alexanderwhitestone.com"
SITE_ROOT="/var/www/the-door"
DEPLOY_DIR="$(cd "$(dirname "$0")/.." && pwd)"
VPS_IP=$(curl -sf --max-time 5 ifconfig.me 2>/dev/null || hostname -I | awk '{print $1}')
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
err() { echo -e "${RED}[-]${NC} $*" >&2; }
# ================================================================
# FUNCTIONS
# ================================================================
setup_swap() {
log "Checking swap..."
if swapon --show 2>/dev/null | grep -q swap; then
log "Swap already configured: $(swapon --show | head -1 | awk '{print $3}')"
return 0
fi
if [ -f /swapfile ]; then
warn "Swapfile exists but not active — activating..."
swapon /swapfile 2>/dev/null && log "Swap activated" || err "Failed to activate swap"
return 0
fi
log "Creating 2GB swap file..."
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
log "Swap configured: $(free -h | awk '/Swap/{print $2}')"
}
install_packages() {
log "Installing packages..."
apt-get update -qq
apt-get install -y -qq nginx certbot python3-certbot-nginx ufw curl
log "Packages installed"
}
deploy_site() {
log "Deploying site files to ${SITE_ROOT}..."
mkdir -p "${SITE_ROOT}"
# Copy static files
for f in index.html manifest.json sw.js about.html testimony.html; do
if [ -f "${DEPLOY_DIR}/${f}" ]; then
cp "${DEPLOY_DIR}/${f}" "${SITE_ROOT}/${f}"
fi
done
# Copy system prompt (reference, not served)
cp "${DEPLOY_DIR}/system-prompt.txt" "${SITE_ROOT}/system-prompt.txt"
chown -R www-data:www-data "${SITE_ROOT}"
chmod -R 755 "${SITE_ROOT}"
log "Site files deployed: $(ls -la ${SITE_ROOT} | wc -l) files"
}
configure_nginx() {
log "Configuring nginx..."
# Deploy site config
cp "${DEPLOY_DIR}/deploy/nginx.conf" /etc/nginx/sites-available/the-door
# Add rate limit zone if not present
if ! grep -q "limit_req_zone.*the_door_api" /etc/nginx/nginx.conf 2>/dev/null; then
sed -i '/http {/a \ limit_req_zone $binary_remote_addr zone=the_door_api:10m rate=10r/m;' /etc/nginx/nginx.conf
fi
# Enable site, disable default
ln -sf /etc/nginx/sites-available/the-door /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
# Test and reload
if nginx -t 2>&1; then
systemctl enable nginx
systemctl reload nginx || systemctl start nginx
log "nginx configured and running"
else
err "nginx config test failed!"
nginx -t
return 1
fi
}
setup_firewall() {
log "Configuring firewall..."
ufw allow 22/tcp comment 'SSH'
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
ufw --force enable
log "Firewall configured: $(ufw status | grep -c ALLOW) rules active"
}
setup_ssl() {
log "Checking SSL certificate..."
if [ -f "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" ]; then
log "SSL certificate already exists"
# Check expiry
EXPIRY=$(openssl x509 -enddate -noout -in "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" 2>/dev/null | cut -d= -f2)
log "Certificate expires: ${EXPIRY}"
return 0
fi
warn "No SSL certificate found."
warn "Ensure DNS is pointed: ${DOMAIN} A record → ${VPS_IP}"
warn ""
warn "Then run manually:"
warn " certbot --nginx -d ${DOMAIN} -d www.${DOMAIN}"
warn ""
# Attempt automated cert if DNS resolves correctly
RESOLVED_IP=$(dig +short "${DOMAIN}" @8.8.8.8 2>/dev/null | head -1)
if [ "${RESOLVED_IP}" = "${VPS_IP}" ]; then
log "DNS resolves correctly — obtaining SSL certificate..."
certbot --nginx -d "${DOMAIN}" -d "www.${DOMAIN}" \
--non-interactive --agree-tos --register-unsafely-without-email \
&& log "SSL certificate obtained!" \
|| warn "certbot failed — run manually"
else
warn "DNS not pointed yet (resolved: ${RESOLVED_IP}, expected: ${VPS_IP})"
fi
}
setup_hermes_service() {
log "Setting up Hermes Gateway systemd service..."
# Create hermes user if it doesn't exist
if ! id -u hermes >/dev/null 2>&1; then
log "Creating hermes user..."
useradd --system --shell /usr/sbin/nologin --home-dir /opt/hermes --create-home hermes
fi
# Create working directory
mkdir -p /opt/hermes
chown hermes:hermes /opt/hermes
# Deploy systemd unit file
cp "${DEPLOY_DIR}/deploy/hermes-gateway.service" /etc/systemd/system/hermes-gateway.service
systemctl daemon-reload
systemctl enable hermes-gateway
# Start or restart the service
if systemctl is-active --quiet hermes-gateway; then
log "Restarting hermes-gateway service..."
systemctl restart hermes-gateway
else
log "Starting hermes-gateway service..."
systemctl start hermes-gateway || warn "Service start failed — ensure hermes binary is installed at /usr/local/bin/hermes"
fi
# Verify
sleep 2
if systemctl is-active --quiet hermes-gateway; then
log "hermes-gateway service is running"
else
warn "hermes-gateway service not running — check: journalctl -u hermes-gateway"
fi
}
check_deployment() {
echo ""
echo "================================"
echo " The Door — Deployment Status"
echo "================================"
echo ""
# Swap
echo -n "Swap: "
if swapon --show 2>/dev/null | grep -q swap; then
echo -e "${GREEN}OK${NC}$(swapon --show | head -1 | awk '{print $3}')"
else
echo -e "${RED}MISSING${NC}"
fi
# nginx
echo -n "nginx: "
if systemctl is-active --quiet nginx 2>/dev/null; then
echo -e "${GREEN}RUNNING${NC}"
else
echo -e "${RED}STOPPED${NC}"
fi
# Site files
echo -n "Site files: "
if [ -f "${SITE_ROOT}/index.html" ]; then
echo -e "${GREEN}OK${NC}$(ls -la ${SITE_ROOT}/index.html | awk '{print $5}') bytes"
else
echo -e "${RED}MISSING${NC}"
fi
# SSL
echo -n "SSL cert: "
if [ -f "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" ]; then
EXPIRY=$(openssl x509 -enddate -noout -in "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" | cut -d= -f2)
echo -e "${GREEN}OK${NC} — expires ${EXPIRY}"
else
echo -e "${YELLOW}NOT SET${NC}"
fi
# Firewall
echo -n "Firewall: "
if ufw status 2>/dev/null | grep -q "Status: active"; then
echo -e "${GREEN}ACTIVE${NC}$(ufw status | grep -c ALLOW) rules"
else
echo -e "${RED}INACTIVE${NC}"
fi
# HTTP test
echo -n "HTTP test: "
if curl -sf --max-time 5 "http://localhost/" -o /dev/null 2>/dev/null; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${YELLOW}N/A${NC}"
fi
# API proxy test
echo -n "API proxy: "
if curl -sf --max-time 5 "http://localhost/health" -o /dev/null 2>/dev/null; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${YELLOW}Hermes not responding${NC} (may be expected)"
fi
# DNS
echo -n "DNS: "
RESOLVED_IP=$(dig +short "${DOMAIN}" @8.8.8.8 2>/dev/null | head -1)
if [ "${RESOLVED_IP}" = "${VPS_IP}" ]; then
echo -e "${GREEN}OK${NC}${DOMAIN}${RESOLVED_IP}"
else
echo -e "${YELLOW}NOT POINTED${NC} (resolved: ${RESOLVED_IP:-nothing}, expected: ${VPS_IP})"
fi
# Hermes gateway service
echo -n "Hermes service: "
if systemctl is-active --quiet hermes-gateway 2>/dev/null; then
echo -e "${GREEN}RUNNING${NC}"
elif systemctl is-enabled --quiet hermes-gateway 2>/dev/null; then
echo -e "${YELLOW}ENABLED but not running${NC}"
else
echo -e "${RED}NOT INSTALLED${NC}"
fi
echo ""
echo "IP: ${VPS_IP}"
echo "Domain: ${DOMAIN}"
echo "Site root: ${SITE_ROOT}"
}
# ================================================================
# MAIN
# ================================================================
echo ""
echo "=== The Door — Deployment ==="
echo "Deploy dir: ${DEPLOY_DIR}"
echo "VPS IP: ${VPS_IP}"
echo ""
case "${1:-full}" in
--site)
deploy_site
configure_nginx
;;
--ssl)
setup_ssl
;;
--service)
setup_hermes_service
;;
--check)
check_deployment
;;
--full|"")
setup_swap
install_packages
deploy_site
configure_nginx
setup_firewall
setup_ssl
setup_hermes_service
check_deployment
;;
*)
echo "Usage: $0 [--site|--ssl|--service|--check|--full]"
exit 1
;;
esac
echo ""
echo "=== Deployment complete ==="
echo ""
echo "Next steps:"
echo " 1. Point DNS: ${DOMAIN} A record → ${VPS_IP}"
echo " 2. If SSL not set: certbot --nginx -d ${DOMAIN} -d www.${DOMAIN}"
echo " 3. Test: curl -I https://${DOMAIN}"
echo " 4. Test API: curl https://${DOMAIN}/api/health"
echo ""
echo "The Door is open."