diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100644 index 0000000..dbd9d50 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# deploy.sh — Pull latest code and redeploy via Docker Compose +# Run from /opt/cast-ghl-plugin after initial server setup. +# Usage: bash deploy/deploy.sh + +set -euo pipefail + +APP_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$APP_DIR" + +echo "==> Pulling latest code" +git pull --ff-only + +echo "==> Building and restarting services" +docker compose pull mongo # pull latest Mongo image if updated +docker compose build --no-cache bridge +docker compose up -d --remove-orphans + +echo "==> Waiting for health check" +sleep 5 +STATUS=$(docker compose ps --format json | python3 -c " +import sys, json +for line in sys.stdin: + s = json.loads(line) + if s.get('Service') == 'bridge': + print(s.get('Health', s.get('State', 'unknown'))) +" 2>/dev/null || echo "unknown") +echo "Bridge container status: $STATUS" + +echo "==> Tailing last 20 log lines" +docker compose logs --tail=20 bridge + +echo "" +echo "=== Deploy complete ===" +echo "Health endpoint: https://ghl.cast.ph/health" diff --git a/deploy/nginx/ghl.cast.ph.conf b/deploy/nginx/ghl.cast.ph.conf new file mode 100644 index 0000000..b11e50f --- /dev/null +++ b/deploy/nginx/ghl.cast.ph.conf @@ -0,0 +1,42 @@ +server { + listen 80; + server_name ghl.cast.ph; + + # Let's Encrypt ACME challenge + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + server_name ghl.cast.ph; + + ssl_certificate /etc/letsencrypt/live/ghl.cast.ph/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/ghl.cast.ph/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # Security headers + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header Referrer-Policy no-referrer; + + location / { + proxy_pass http://127.0.0.1:3002; + proxy_http_version 1.1; + 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; + + # Timeouts — GHL expects quick 200 for webhook; 30s is generous + proxy_connect_timeout 10s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + } +} diff --git a/deploy/setup-server.sh b/deploy/setup-server.sh new file mode 100644 index 0000000..ce61108 --- /dev/null +++ b/deploy/setup-server.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# setup-server.sh — Bootstrap a fresh Ubuntu 22.04/24.04 LTS Vultr VPS +# Run once as root (or with sudo) after provisioning. +# Usage: bash setup-server.sh + +set -euo pipefail + +DOMAIN="ghl.cast.ph" +APP_DIR="/opt/cast-ghl-plugin" +REPO_URL="https://github.com/CAST-ph/cast-ghl-plugin.git" # adjust if needed + +echo "==> Updating system packages" +apt-get update -q && apt-get upgrade -y -q + +echo "==> Installing dependencies" +apt-get install -y -q \ + ca-certificates curl gnupg ufw \ + nginx certbot python3-certbot-nginx \ + git + +echo "==> Installing Docker" +install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ + | gpg --dearmor -o /etc/apt/keyrings/docker.gpg +chmod a+r /etc/apt/keyrings/docker.gpg +echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ +https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ + | tee /etc/apt/sources.list.d/docker.list > /dev/null +apt-get update -q +apt-get install -y -q docker-ce docker-ce-cli containerd.io docker-compose-plugin +systemctl enable --now docker + +echo "==> Configuring firewall" +ufw default deny incoming +ufw default allow outgoing +ufw allow ssh +ufw allow 'Nginx Full' +ufw --force enable + +echo "==> Cloning application" +mkdir -p "$APP_DIR" +if [ -d "$APP_DIR/.git" ]; then + git -C "$APP_DIR" pull +else + git clone "$REPO_URL" "$APP_DIR" +fi + +echo "==> Installing Nginx config" +cp "$APP_DIR/deploy/nginx/ghl.cast.ph.conf" /etc/nginx/sites-available/"$DOMAIN" +ln -sf /etc/nginx/sites-available/"$DOMAIN" /etc/nginx/sites-enabled/"$DOMAIN" +rm -f /etc/nginx/sites-enabled/default +nginx -t && systemctl reload nginx + +echo "==> Obtaining Let's Encrypt certificate" +certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos -m ops@cast.ph +systemctl reload nginx + +echo "" +echo "=== Setup complete ===" +echo "Next: copy .env to $APP_DIR/.env then run deploy/deploy.sh" diff --git a/docker-compose.yaml b/docker-compose.yaml index c5b673c..b6cd41b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,12 +1,13 @@ services: bridge: build: . + # Bind to localhost only — Nginx proxies from outside ports: - - "${PORT:-3002}:${PORT:-3002}" + - "127.0.0.1:${PORT:-3002}:${PORT:-3002}" env_file: .env depends_on: mongo: - condition: service_started + condition: service_healthy restart: unless-stopped logging: driver: json-file @@ -16,9 +17,16 @@ services: mongo: image: mongo:7 + # No ports exposed — only reachable by bridge on the internal network volumes: - mongo-data:/data/db restart: unless-stopped + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 20s volumes: mongo-data: