|
| 1 | +#!/bin/bash |
| 2 | +# setup-ssl.sh — 在 VM 上初始化/更新 Nginx + Let's Encrypt SSL |
| 3 | +# 用法: bash scripts/setup-ssl.sh [DOMAIN] |
| 4 | +# 若不传参数, 自动使用 .env 中的 DOMAIN, 或从公网 IP 生成 nip.io 域名 |
| 5 | +set -euo pipefail |
| 6 | + |
| 7 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 8 | +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" |
| 9 | +cd "$PROJECT_DIR" |
| 10 | + |
| 11 | +# ── 1. 确定域名 ─────────────────────────────────────────────────────────────── |
| 12 | +if [ -n "${1:-}" ]; then |
| 13 | + DOMAIN="$1" |
| 14 | +elif grep -q "^DOMAIN=" .env 2>/dev/null; then |
| 15 | + DOMAIN="$(grep '^DOMAIN=' .env | cut -d= -f2 | tr -d ' \r')" |
| 16 | +fi |
| 17 | + |
| 18 | +if [ -z "${DOMAIN:-}" ]; then |
| 19 | + # 自动获取 VM 公网 IP, 生成 nip.io 域名(无需购买域名,DNS 自动生效) |
| 20 | + PUB_IP="$(curl -s --max-time 5 https://api.ipify.org || curl -s --max-time 5 https://ifconfig.me)" |
| 21 | + if [ -z "$PUB_IP" ]; then |
| 22 | + echo "❌ 无法获取公网 IP,请手动设置 DOMAIN 变量" >&2 |
| 23 | + exit 1 |
| 24 | + fi |
| 25 | + DOMAIN="${PUB_IP}.nip.io" |
| 26 | + echo "📡 自动检测到公网 IP: $PUB_IP" |
| 27 | +fi |
| 28 | + |
| 29 | +echo "🌐 使用域名: $DOMAIN" |
| 30 | +echo " - n8n: https://n8n.${DOMAIN}" |
| 31 | +echo " - 采集面板: https://mine.${DOMAIN}" |
| 32 | +echo " - 外展 API: https://outreach.${DOMAIN}" |
| 33 | + |
| 34 | +# ── 2. 更新 .env 中的域名和 n8n URL ────────────────────────────────────────── |
| 35 | +if ! grep -q "^DOMAIN=" .env 2>/dev/null; then |
| 36 | + echo "DOMAIN=${DOMAIN}" >> .env |
| 37 | +else |
| 38 | + sed -i "s|^DOMAIN=.*|DOMAIN=${DOMAIN}|" .env |
| 39 | +fi |
| 40 | + |
| 41 | +# 更新 n8n webhook/editor URL |
| 42 | +sed -i "s|^WEBHOOK_URL=.*|WEBHOOK_URL=https://n8n.${DOMAIN}|" .env |
| 43 | +sed -i "s|^N8N_EDITOR_BASE_URL=.*|N8N_EDITOR_BASE_URL=https://n8n.${DOMAIN}|" .env |
| 44 | + |
| 45 | +# ── 3. 确保 certbot 挂载目录存在 ───────────────────────────────────────────── |
| 46 | +mkdir -p nginx/certbot/www nginx/certbot/conf |
| 47 | + |
| 48 | +# ── 4. 申请/续期 SSL 证书 ──────────────────────────────────────────────────── |
| 49 | +echo "🔐 申请 Let's Encrypt SSL 证书..." |
| 50 | + |
| 51 | +# 先确保 nginx 在 HTTP 模式下运行(用于 ACME challenge) |
| 52 | +docker compose -f docker-compose.prod.yml up -d nginx |
| 53 | +sleep 3 |
| 54 | + |
| 55 | +# 申请包含三个子域名的单张证书 |
| 56 | +docker compose -f docker-compose.prod.yml run --rm certbot certonly \ |
| 57 | + --webroot \ |
| 58 | + --webroot-path /var/www/certbot \ |
| 59 | + --email "admin@${DOMAIN}" \ |
| 60 | + --agree-tos \ |
| 61 | + --no-eff-email \ |
| 62 | + --non-interactive \ |
| 63 | + -d "n8n.${DOMAIN}" \ |
| 64 | + -d "mine.${DOMAIN}" \ |
| 65 | + -d "outreach.${DOMAIN}" \ |
| 66 | + --cert-name "lead-mining" \ |
| 67 | + || { |
| 68 | + echo "⚠️ SSL 申请失败(可能是首次或 nip.io 限速),切换到仅 HTTP 模式" |
| 69 | + SSL_FAILED=1 |
| 70 | + } |
| 71 | + |
| 72 | +# ── 5. 生成完整 nginx 配置 ──────────────────────────────────────────────────── |
| 73 | +echo "📝 生成 nginx 配置..." |
| 74 | + |
| 75 | +if [ "${SSL_FAILED:-0}" = "1" ]; then |
| 76 | + # ── HTTP only fallback ──────────────────────────────────────────────────── |
| 77 | + cat > nginx/conf.d/default.conf <<NGINXEOF |
| 78 | +# HTTP 模式(SSL 证书获取失败或待申请) |
| 79 | +server { |
| 80 | + listen 80; |
| 81 | + server_name n8n.${DOMAIN}; |
| 82 | + location /.well-known/acme-challenge/ { root /var/www/certbot; } |
| 83 | + location / { |
| 84 | + proxy_pass http://n8n:5678; |
| 85 | + proxy_http_version 1.1; |
| 86 | + proxy_set_header Upgrade \$http_upgrade; |
| 87 | + proxy_set_header Connection "upgrade"; |
| 88 | + proxy_set_header Host \$host; |
| 89 | + proxy_set_header X-Forwarded-Proto \$scheme; |
| 90 | + proxy_set_header X-Real-IP \$remote_addr; |
| 91 | + proxy_read_timeout 86400; |
| 92 | + } |
| 93 | +} |
| 94 | +server { |
| 95 | + listen 80; |
| 96 | + server_name mine.${DOMAIN}; |
| 97 | + location /.well-known/acme-challenge/ { root /var/www/certbot; } |
| 98 | + location / { |
| 99 | + proxy_pass http://lead-miner:8000; |
| 100 | + proxy_set_header Host \$host; |
| 101 | + proxy_set_header X-Forwarded-Proto \$scheme; |
| 102 | + proxy_set_header X-Real-IP \$remote_addr; |
| 103 | + } |
| 104 | +} |
| 105 | +server { |
| 106 | + listen 80; |
| 107 | + server_name outreach.${DOMAIN}; |
| 108 | + location /.well-known/acme-challenge/ { root /var/www/certbot; } |
| 109 | + location / { |
| 110 | + proxy_pass http://sales-outreach:8080; |
| 111 | + proxy_set_header Host \$host; |
| 112 | + proxy_set_header X-Forwarded-Proto \$scheme; |
| 113 | + proxy_set_header X-Real-IP \$remote_addr; |
| 114 | + } |
| 115 | +} |
| 116 | +NGINXEOF |
| 117 | + echo "⚠️ 已生成 HTTP 模式配置,之后运行 'bash scripts/setup-ssl.sh' 可补充 SSL" |
| 118 | +else |
| 119 | + # ── HTTP → HTTPS redirect + HTTPS virtual hosts ─────────────────────────── |
| 120 | + cat > nginx/conf.d/default.conf <<NGINXEOF |
| 121 | +# ── HTTP → HTTPS 强制跳转 + ACME challenge ─────────────────────────────────── |
| 122 | +server { |
| 123 | + listen 80; |
| 124 | + server_name n8n.${DOMAIN} mine.${DOMAIN} outreach.${DOMAIN}; |
| 125 | + location /.well-known/acme-challenge/ { root /var/www/certbot; } |
| 126 | + location / { return 301 https://\$host\$request_uri; } |
| 127 | +} |
| 128 | +
|
| 129 | +# SSL 通用参数 |
| 130 | +ssl_protocols TLSv1.2 TLSv1.3; |
| 131 | +ssl_prefer_server_ciphers on; |
| 132 | +ssl_session_cache shared:SSL:10m; |
| 133 | +ssl_session_timeout 10m; |
| 134 | +
|
| 135 | +# ── n8n ─────────────────────────────────────────────────────────────────────── |
| 136 | +server { |
| 137 | + listen 443 ssl; |
| 138 | + server_name n8n.${DOMAIN}; |
| 139 | + ssl_certificate /etc/letsencrypt/live/lead-mining/fullchain.pem; |
| 140 | + ssl_certificate_key /etc/letsencrypt/live/lead-mining/privkey.pem; |
| 141 | +
|
| 142 | + # WebSocket 支持(n8n 编辑器必须) |
| 143 | + location / { |
| 144 | + proxy_pass http://n8n:5678; |
| 145 | + proxy_http_version 1.1; |
| 146 | + proxy_set_header Upgrade \$http_upgrade; |
| 147 | + proxy_set_header Connection "upgrade"; |
| 148 | + proxy_set_header Host \$host; |
| 149 | + proxy_set_header X-Real-IP \$remote_addr; |
| 150 | + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; |
| 151 | + proxy_set_header X-Forwarded-Proto https; |
| 152 | + proxy_read_timeout 86400; |
| 153 | + } |
| 154 | +} |
| 155 | +
|
| 156 | +# ── Lead Mining 控制台 ─────────────────────────────────────────────────────── |
| 157 | +server { |
| 158 | + listen 443 ssl; |
| 159 | + server_name mine.${DOMAIN}; |
| 160 | + ssl_certificate /etc/letsencrypt/live/lead-mining/fullchain.pem; |
| 161 | + ssl_certificate_key /etc/letsencrypt/live/lead-mining/privkey.pem; |
| 162 | +
|
| 163 | + location / { |
| 164 | + proxy_pass http://lead-miner:8000; |
| 165 | + proxy_set_header Host \$host; |
| 166 | + proxy_set_header X-Real-IP \$remote_addr; |
| 167 | + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; |
| 168 | + proxy_set_header X-Forwarded-Proto https; |
| 169 | + proxy_read_timeout 120; |
| 170 | + } |
| 171 | +} |
| 172 | +
|
| 173 | +# ── Sales Outreach API ──────────────────────────────────────────────────────── |
| 174 | +server { |
| 175 | + listen 443 ssl; |
| 176 | + server_name outreach.${DOMAIN}; |
| 177 | + ssl_certificate /etc/letsencrypt/live/lead-mining/fullchain.pem; |
| 178 | + ssl_certificate_key /etc/letsencrypt/live/lead-mining/privkey.pem; |
| 179 | +
|
| 180 | + location / { |
| 181 | + proxy_pass http://sales-outreach:8080; |
| 182 | + proxy_set_header Host \$host; |
| 183 | + proxy_set_header X-Real-IP \$remote_addr; |
| 184 | + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; |
| 185 | + proxy_set_header X-Forwarded-Proto https; |
| 186 | + proxy_read_timeout 120; |
| 187 | + } |
| 188 | +} |
| 189 | +NGINXEOF |
| 190 | + echo "✅ 已生成 HTTPS 配置" |
| 191 | + |
| 192 | + # SSL 成功后更新 n8n 环境变量,启用安全 cookie |
| 193 | + sed -i "s|^N8N_PROTOCOL=.*|N8N_PROTOCOL=https|" .env |
| 194 | + sed -i "s|^N8N_SECURE_COOKIE=.*|N8N_SECURE_COOKIE=true|" .env |
| 195 | + # 重启 n8n 以应用新的 WEBHOOK_URL 和 PROTOCOL |
| 196 | + docker compose -f docker-compose.prod.yml up -d --no-deps n8n |
| 197 | +fi |
| 198 | + |
| 199 | +# ── 6. 重载 nginx ───────────────────────────────────────────────────────────── |
| 200 | +docker compose -f docker-compose.prod.yml exec nginx nginx -s reload 2>/dev/null \ |
| 201 | + || docker compose -f docker-compose.prod.yml restart nginx |
| 202 | + |
| 203 | +# ── 7. 打印访问地址 ─────────────────────────────────────────────────────────── |
| 204 | +echo "" |
| 205 | +echo "╔══════════════════════════════════════════════════════════╗" |
| 206 | +echo "║ ✅ Lead Mining System — 访问地址 ║" |
| 207 | +echo "╠══════════════════════════════════════════════════════════╣" |
| 208 | +if [ "${SSL_FAILED:-0}" = "1" ]; then |
| 209 | +echo "║ 🔄 n8n 工作流 http://n8n.${DOMAIN}" |
| 210 | +echo "║ 🎛️ 采集控制台 http://mine.${DOMAIN}/admin" |
| 211 | +echo "║ 📤 外展 API http://outreach.${DOMAIN}" |
| 212 | +else |
| 213 | +echo "║ 🔄 n8n 工作流 https://n8n.${DOMAIN}" |
| 214 | +echo "║ 🎛️ 采集控制台 https://mine.${DOMAIN}/admin" |
| 215 | +echo "║ 📤 外展 API https://outreach.${DOMAIN}" |
| 216 | +fi |
| 217 | +echo "╚══════════════════════════════════════════════════════════╝" |
0 commit comments