feat: add production deployment artifacts for ghl.cast.ph (Vultr)

- deploy/nginx/ghl.cast.ph.conf: Nginx reverse proxy with SSL (Let's Encrypt)
- deploy/setup-server.sh: one-shot Ubuntu VPS bootstrap (Docker, Nginx, Certbot, UFW)
- deploy/deploy.sh: pull-and-redeploy script using Docker Compose
- docker-compose.yaml: bind bridge to 127.0.0.1 only; add Mongo healthcheck;
  bridge waits for Mongo healthy before starting

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Head of Product & Engineering 2026-04-05 01:46:26 +02:00
parent c2fd0e2f98
commit f99772d8c0
4 changed files with 147 additions and 2 deletions

35
deploy/deploy.sh Normal file
View File

@ -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"

View File

@ -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;
}
}

60
deploy/setup-server.sh Normal file
View File

@ -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"

View File

@ -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: