Skip to content

Commit 418baba

Browse files
kanekane
authored andcommitted
feat: add nginx reverse proxy + Let's Encrypt SSL + free nip.io domain
Automatic domain generation using {VM_IP}.nip.io (no DNS purchase needed). Each service gets its own subdomain with full HTTPS: https://n8n.{VM_IP}.nip.io → n8n workflow editor https://mine.{VM_IP}.nip.io → Lead Mining admin panel (/admin) https://outreach.{VM_IP}.nip.io → Sales Outreach API Changes: - nginx/nginx.conf: base nginx config - nginx/conf.d/default.conf: HTTP-only placeholder (overwritten by setup-ssl.sh) - scripts/setup-ssl.sh: auto-detects VM IP, issues multi-domain cert via certbot HTTP-01 challenge, writes HTTPS nginx vhosts, restarts services - docker-compose.prod.yml: add nginx (80/443) + certbot (auto-renew) services - deploy.yml: new 'Setup domain & SSL' step runs setup-ssl.sh after deploy; supports custom domain via workflow_dispatch input - .env.example: add DOMAIN, N8N_PROTOCOL, update comments
1 parent 09347ae commit 418baba

6 files changed

Lines changed: 334 additions & 2 deletions

File tree

.env.example

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,24 @@ CHROMA_COLLECTION=all_leads
3737
GEMINI_MODEL=gemini-2.0-flash
3838
GEMINI_MAX_CONCURRENT=10
3939

40+
# ── 域名 & Nginx SSL ──────────────────────────────────
41+
# 留空 → 部署时自动生成 {VM_IP}.nip.io 免费域名(无需购买)
42+
# 有自己的域名则填入(需先将 A 记录指向 VM 公网 IP)
43+
DOMAIN=
44+
4045
# ── n8n 2.x ──────────────────────────────────────────
4146
# 首次启动自动创建 Owner 账户,无需手动完成 Setup Wizard
4247
N8N_JWT_SECRET=n8n-super-secret-jwt-key-change-this
4348
N8N_OWNER_EMAIL=admin@leadminer.local
4449
N8N_OWNER_FIRST_NAME=Admin
4550
N8N_OWNER_LAST_NAME=User
4651
N8N_OWNER_PASSWORD=LeadMiner2024!
52+
# 以下两项由 scripts/setup-ssl.sh 自动更新为 https://n8n.{DOMAIN}
4753
WEBHOOK_URL=http://localhost:5678
4854
N8N_EDITOR_BASE_URL=http://localhost:5678
49-
# HTTP 访问时必须设 false;上了 HTTPS 后可删除此行
55+
# HTTPS 反代后 n8n 需感知协议,由 setup-ssl.sh 自动改为 https
56+
N8N_PROTOCOL=http
57+
# HTTP 访问时必须设 false;获取 SSL 证书后 setup-ssl.sh 会自动更新
5058
N8N_SECURE_COOKIE=false
5159

5260
# ── SMTP(邮件发送,sales-outreach-engine 需要)──────────────────────

.github/workflows/deploy.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ on:
44
push:
55
branches: [ main ]
66
workflow_dispatch:
7+
inputs:
8+
domain:
9+
description: '自定义域名(留空则自动生成 nip.io 域名)'
10+
required: false
11+
default: ''
712

813
env:
914
REGISTRY: ghcr.io
@@ -96,9 +101,29 @@ jobs:
96101
sleep 10
97102
docker compose -f docker-compose.prod.yml up -d sales-outreach n8n
98103
104+
# 启动 nginx(HTTP 模式,等待 SSL 脚本处理)
105+
docker compose -f docker-compose.prod.yml up -d nginx certbot
106+
99107
# 清理旧镜像
100108
docker image prune -f
101109
110+
- name: Setup domain & SSL
111+
uses: appleboy/ssh-action@v1.0.3
112+
with:
113+
host: ${{ secrets.VM_HOST }}
114+
username: ${{ secrets.VM_USER }}
115+
key: ${{ secrets.VM_SSH_KEY }}
116+
script: |
117+
cd /opt/lead-mining-system
118+
119+
# 若 workflow_dispatch 传入了自定义域名则使用,否则自动生成 nip.io
120+
CUSTOM_DOMAIN="${{ github.event.inputs.domain }}"
121+
if [ -n "$CUSTOM_DOMAIN" ]; then
122+
bash scripts/setup-ssl.sh "$CUSTOM_DOMAIN"
123+
else
124+
bash scripts/setup-ssl.sh
125+
fi
126+
102127
- name: Import n8n workflows
103128
uses: appleboy/ssh-action@v1.0.3
104129
with:

docker-compose.prod.yml

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ services:
8989
DB_POSTGRESDB_SCHEMA: n8n
9090
LEAD_MINER_URL: http://lead-miner:8000
9191
GEMINI_API_KEY: ${GEMINI_API_KEY}
92-
# Cookie 安全(HTTP 访问时必须设为 false;如果配了 HTTPS 可删除此行)
92+
# 通过 nginx HTTPS 代理时,设置协议为 https 以修复 cookie
93+
N8N_PROTOCOL: ${N8N_PROTOCOL:-http}
94+
# Cookie 安全(HTTP 访问时必须设 false;如果配了 HTTPS 可删除此行)
9395
N8N_SECURE_COOKIE: "false"
9496
N8N_LOG_LEVEL: info
9597
depends_on:
@@ -101,10 +103,37 @@ services:
101103
ports:
102104
- "5678:5678"
103105

106+
# ── Nginx 反代理 + SSL 入口 ──────────────────────────────────
107+
nginx:
108+
image: nginx:1.25-alpine
109+
restart: unless-stopped
110+
ports:
111+
- "80:80"
112+
- "443:443"
113+
volumes:
114+
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
115+
- ./nginx/conf.d:/etc/nginx/conf.d
116+
- certbot_www:/var/www/certbot:ro
117+
- certbot_certs:/etc/letsencrypt:ro
118+
depends_on:
119+
- n8n
120+
- lead-miner
121+
- sales-outreach
122+
123+
# ── Certbot 自动 SSL 证书 ──────────────────────────────────────
124+
certbot:
125+
image: certbot/certbot:latest
126+
volumes:
127+
- certbot_www:/var/www/certbot
128+
- certbot_certs:/etc/letsencrypt
129+
entrypoint: ["/bin/sh", "-c", "trap exit TERM; while :; do certbot renew --quiet; sleep 12h & wait $${!}; done"]
130+
104131
volumes:
105132
postgres_data:
106133
chroma_data:
107134
n8n_data:
135+
certbot_www:
136+
certbot_certs:
108137

109138
networks:
110139
default:

nginx/conf.d/default.conf

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# 临时 HTTP 配置 — 仅用于 Certbot ACME 验证,SSL 初始化前使用
2+
# 生产完整配置由 setup-ssl.sh 生成并覆盖此文件
3+
4+
server {
5+
listen 80;
6+
server_name _;
7+
8+
# Certbot ACME 验证目录
9+
location /.well-known/acme-challenge/ {
10+
root /var/www/certbot;
11+
}
12+
13+
# 其余请求临时反代到 n8n(HTTP)
14+
location / {
15+
proxy_pass http://n8n:5678;
16+
proxy_http_version 1.1;
17+
proxy_set_header Upgrade $http_upgrade;
18+
proxy_set_header Connection "upgrade";
19+
proxy_set_header Host $host;
20+
proxy_set_header X-Real-IP $remote_addr;
21+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
22+
proxy_set_header X-Forwarded-Proto $scheme;
23+
proxy_read_timeout 86400;
24+
}
25+
}

nginx/nginx.conf

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
user nginx;
2+
worker_processes auto;
3+
error_log /var/log/nginx/error.log warn;
4+
pid /var/run/nginx.pid;
5+
6+
events {
7+
worker_connections 1024;
8+
}
9+
10+
http {
11+
include /etc/nginx/mime.types;
12+
default_type application/octet-stream;
13+
14+
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
15+
'$status $body_bytes_sent "$http_referer" '
16+
'"$http_user_agent"';
17+
access_log /var/log/nginx/access.log main;
18+
19+
sendfile on;
20+
keepalive_timeout 65;
21+
client_max_body_size 50m;
22+
23+
# Gzip
24+
gzip on;
25+
gzip_types text/plain text/css application/json application/javascript;
26+
27+
include /etc/nginx/conf.d/*.conf;
28+
}

scripts/setup-ssl.sh

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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

Comments
 (0)