nginx-site-deploy

SKILL.md 内容

{
  "name": "nginx-site-deploy",
  "content": "---\nname: nginx-site-deploy\ndescription: 新站点全流程部署 — FastAPI + H5 + nginx + SSL(canjinbao.com 系)\ntrigger: \"新建站点/部署新站/配 nginx/申请证书/https\"\n---\n\n# Nginx 站点部署 — 全流程\n\n> **适用**:canjinbao.com 系站点(todaynews/crm/erichou/hermes/didi)\n> **技术栈**:FastAPI + H5 (Python http.server) + nginx + Let's Encrypt\n\n## 部署流程\n\n### Step 1: 后端(FastAPI)\n\n> **⚠️ 本机身份(关键)**:开发机 `VM-4-14-ubuntu`(内网 10.2.4.14)**就是主站本人**(公网 152.136.139.215),见 `~/server-deployment-manual.md`。用户说「部署到 152.136.139.215 / 主站」= **直接本机操作,无需 SSH**(`ssh root@152.136.139.215` 会被拒 publickey,别浪费时间试)。识别:`hostname -I` → 10.2.4.14。\n\n> **⚠️ 用哪个 python 起 uvicorn(本次实测)**:裸 `python3` 在本机解析到 **nano-pdf 的 venv,没有 uvicorn/fastapi**(报 `No module named uvicorn`)。已确认有 fastapi+uvicorn 的是 **`/home/agentuser/.hermes/hermes-agent/venv/bin/python3`**。起服务/写 systemd 一律用**该 venv 的绝对路径**,不要用裸 `python3`(后台进程默认 PATH 还可能解析到别的 venv)。\n\n```bash\n# 假设备份在 /home/agentuser/projects/{project}/backend/\ncd /home/agentuser/projects/{project}/backend/\n# ⚠️ 用指明的 venv(本机可跑 uvicorn 的:hermes-agent venv),不要裸 python3\n/home/agentuser/.hermes/hermes-agent/venv/bin/python3 -m uvicorn main:app --host 127.0.0.1 --port {port}\n```\nsystemd 单(推荐,保证开机自启+自愈):\n```ini\n[Service]\nUser=agentuser\nWorkingDirectory=/home/agentuser/projects/{project}/app\nExecStart=/home/agentuser/.hermes/hermes-agent/venv/bin/python3 -m uvicorn main:app --host 127.0.0.1 --port {port}\nRestart=always\nRestartSec=3\n[Install]\nWantedBy=multi-user.target\n```\n安装:`sudo cp /tmp/{name}.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable {name} && sudo systemctl start {name}`\n\n**⚠️ FastAPI 静态页迭代节奏(本次实测,省心省事)**:若后端用 `FileResponse` 直接服务 `app/static/*.html`,则**改 HTML/CSS 文件立即生效、无需重启 uvicorn**(每次请求重新读盘)——迭代文案/样式时别每次都 `systemctl restart`,改完 `curl` 即可验。**只有新增路由(如 `@app.get(\"/about\")`)时,才需要 `systemctl restart {name}`**;否则新 URL 404。判断依据:改的是\"已有页面内容\"(不重启)还是\"加新 URL 路由\"(重启)。\n\n### Step 2: 前端(H5)\n\n```bash\ncd /home/agentuser/projects/{project}/frontend/\n/home/agentuser/projects/ai-sales-crm/.venv/bin/python -m http.server {port}\n```\n\n### Step 3: nginx 配置\n\n```nginx\nserver {\n    listen 80;\n    server_name {subdomain}.canjinbao.com;\n    return 301 https://$host$request_uri;\n}\n\nserver {\n    listen 443 ssl;\n    listen [::]:443 ssl;\n    server_name {subdomain}.canjinbao.com;\n\n    # ❗ 不要加 http2 关键字(与现有其他站点保持一致避免冲突)\n    # SSL 由 certbot 管理\n    ssl_certificate /etc/letsencrypt/live/{subdomain}.canjinbao.com/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/{subdomain}.canjinbao.com/privkey.pem;\n    include /etc/letsencrypt/options-ssl-nginx.conf;\n    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;\n\n    # 安全头\n    add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains\" always;\n    add_header X-Frame-Options \"SAMEORIGIN\" always;\n    add_header X-Content-Type-Options \"nosniff\" always;\n    add_header Referrer-Policy \"no-referrer\" always;\n    server_tokens off;\n\n    # H5 反代\n    location / {\n        proxy_pass http://127.0.0.1:{frontend_port};\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n\n    # API 反代\n    location /api/ {\n        proxy_pass http://127.0.0.1:{backend_port};\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n        client_max_body_size 10M;\n    }\n}\n```\n\n### Step 4: SSL 证书(certbot)\n\n```bash\n# 1. 先配临时 nginx 让验证通过\n# 2. 用 webroot 模式申请\nsudo certbot certonly --webroot -w /var/www/html -d {subdomain}.canjinbao.com \\\n  --non-interactive --agree-tos --email admin@canjinbao.com\n\n# 3. 写正式配置 + reload\nsudo nginx -t && sudo systemctl restart nginx\n```\n\n## ⚠️ 常见坑\n\n### 坑 1: `limit_req zone=api_limit` 未定义\n- **症状**:nginx 启动失败,旧进程用错证书(浏览器报\"证书不对\")\n- **原因**:`limit_req zone=api_limit` 但 `http{}` 块里没有 `limit_req_zone` 定义\n- **修复**:用现有 zone(如 `zone=hermes_limit`)或先在 nginx.conf 定义\n- **验证**:`sudo nginx -t` 必须通过\n\n### 坑 2: nginx 冲突导致旧进程用错证书\n- **症状**:SSL 握手显示的是另一个站点的证书(如 crm.canjinbao.com)\n- **原因**:nginx reload 失败后旧进程仍在,SNI 匹配到第一个 listen 443 的配置\n- **修复**:`sudo systemctl restart nginx`(完整重启,不是 reload)\n- **验证**:`openssl s_client -servername {domain} -connect {domain}:443 | grep subject`\n- **⚠️ 新高子域场景(本次复现)**:**刚新增一个子域配置**、reload 后 `-servername {新域}` 拿到的是**别的站证书**(本次拿到 ai-service.canjinbao.com),且本地 `curl -H \"Host:{新域}\" https://127.0.0.1/` 返回 301/非 200 —— **必须 `systemctl restart nginx`**(reload 不换 worker),restart 后证书即对。**顺序**:先 `grep -E 'listen|server_name|ssl_certificate' sites-available/{域}` 确认 443 块和证书路径存在+正确 → restart → 再 `openssl s_client` 验证书 → 再 curl 验 200。\n\n### 坑 3: 证书目录不存在\n- **症状**:`BIO_new_file() failed: No such file or directory`\n- **解决**:先申请证书再写配置,或先写临时配置(80 端口只做验证)\n\n### 坑 4: 不需要 `listen 443 ssl http2`\n- 其他站点都不带 `http2`,保持一致避免冲突\n- 用 `listen 443 ssl;` 就够了\n\n### 坑 6: `certbot --nginx` 证书写入 default 而非自定义配置\n\n- **症状**:certbot 报 \"Successfully deployed certificate\",但浏览器 ERR_CERT_COMMON_NAME_INVALID,实际返回另一个域名的证书\n- **根因**:`certbot --nginx` 自动模式优先写 sites-enabled 里的 `default` 文件,而不是你的自定义配置\n- **修复**:\n  ```bash\n  # 1. 删 certbot 写的 default\n  sudo rm -f /etc/nginx/sites-enabled/default\n  \n  # 2. 确认自定义配置已启用\n  sudo ln -sf /etc/nginx/sites-available/{domain} /etc/nginx/sites-enabled/{domain}\n  \n  # 3. 修证书路径\n  sudo sed -i 's|/etc/letsencrypt/live/OLD_DOMAIN/|/etc/letsencrypt/live/{domain}/|g' /etc/nginx/sites-available/{domain}\n  \n  # 4. 测试 + reload\n  sudo nginx -t && sudo nginx -s reload\n  ```\n- **预防**:`certbot --nginx` 后先 `sudo ls -la /etc/nginx/sites-enabled/` 检查证书写到了哪个文件,或者用 `certbot certonly --webroot` 只申请证书不写配置\n\n### 坑 7: 自定义域名配置好但公网访问 000(连接被拒)\n\n- **症状**:`curl https://voice.canjinbao.com/` 返回 HTTP 000(连接被拒),但服务器本地 `curl -H \"Host: voice.canjinbao.com\" http://127.0.0.1/` 返回 301\n- **根因**:新域名 DNS 解析到服务器,但 nginx 配置里证书路径写的是**旧域名**(如 `wagtail.canjinbao.com`)。浏览器看到 SNI 不匹配 → 拒绝连接\n- **排查**:\n  ```bash\n  # 1. 看证书路径\n  grep ssl_certificate /etc/nginx/sites-available/voice.canjinbao.com\n  \n  # 2. 看实际证书\n  echo | openssl s_client -servername voice.canjinbao.com -connect voice.canjinbao.com:443 2>/dev/null | grep subject\n  \n  # 3. 看 certbot 真实证书\n  sudo ls /etc/letsencrypt/live/voice.canjinbao.com/\n  ```\n- **修复**:证书路径指向 `/etc/letsencrypt/live/voice.canjinbao.com/` 而非其他域名\n\n- **症状**:certbot 报 \"Successfully deployed certificate\",但浏览器 ERR_CERT_COMMON_NAME_INVALID,实际返回另一个域名的证书\n- **根因**:`certbot --nginx` 自动模式优先写 sites-enabled 里的 `default` 文件,而不是你的自定义配置\n- **修复**:\n  ```bash\n  # 1. 删 certbot 写的 default\n  sudo rm -f /etc/nginx/sites-enabled/default\n  \n  # 2. 确认自定义配置已启用\n  sudo ln -sf /etc/nginx/sites-available/{domain} /etc/nginx/sites-enabled/{domain}\n  \n  # 3. 修证书路径\n  sudo sed -i 's|/etc/letsencrypt/live/OLD_DOMAIN/|/etc/letsencrypt/live/{domain}/|g' /etc/nginx/sites-available/{domain}\n  \n  # 4. 测试 + reload\n  sudo nginx -t && sudo nginx -s reload\n  ```\n- **预防**:`certbot --nginx` 后先 `sudo ls -la /etc/nginx/sites-enabled/` 检查证书写到了哪个文件,或者用 `certbot certonly --webroot` 只申请证书不写配置\n- **症状**:`/api/ai/chat` 返回 404,但 `curl http://127.0.0.1:3074/api/ai/chat` 通\n- **原因**:nginx 匹配前缀时,`location /api/` 是前缀匹配,所有 `/api/*` 子路径都走它。如果子路径需要走不同后端(如 `/api/ai/` 走 3074 而非 8095),必须把子路径的 `location` 块放在 `location /api/` **之前**\n- **修复**:按优先级从高到低排列:\n  ```nginx\n  # 1. 最具体的子路径(先匹配)\n  location /api/events/ { proxy_pass http://127.0.0.1:3074; proxy_buffering off; ... }\n  location /api/ai/ { proxy_pass http://127.0.0.1:3074; proxy_read_timeout 120s; }\n  \n  # 2. catch-all(最后)\n  location /api/ { proxy_pass http://127.0.0.1:8095/api/; }\n  ```\n  注意:`location /api/events/` 和 `location /api/ai/` 必须在 `location /api/` 之前(nginx 按文件顺序,前缀匹配选最长的,但同长时文件顺序生效)\n- **验证**:`curl -s -m 5 -X POST -H \"Content-Type: application/json\" -d '{\"message\":\"test\"}' https://domain/api/ai/chat | head -c 100`\n\n### 坑 8: 多个站点同时 HTTP 000 — sites-enabled symlink 缺失 / server_name 错配\n\n- **症状**:用户报\"站点都打不开了\",批量 curl 多个域名都返回 HTTP 000,但服务器本地 `nginx -t` 通过、nginx 进程 running、后端进程(node/python)都在跑\n- **根因**(3 种):\n  1. **sites-available 有配置但 sites-enabled 无 symlink**(配置丢失/未启用)— 最常见\n  2. `server_name _` 的 default 配置不匹配任何域名(如 `ai-ip-temp` 临时配置占了 default_server)\n  3. 配置里的 proxy_pass 端口不对(如 hermes 配置指向 9119,但实际端口变了)\n- **排查(按顺序)**:\n  ```bash\n  # 1. 批量测所有域名,快速分类 200/302 vs 000\n  for domain in voice.canjinbao.com ai.canjinbao.com smart-site.canjinbao.com; do\n      code=$(curl -s --max-time 3 -o /dev/null -w \"%{http_code}\" \"https://$domain/\" 2>&1)\n      echo \"$code  $domain\"\n  done\n\n  # 2. 对 000 域名:对比 sites-available 和 sites-enabled\n  ls /etc/nginx/sites-available/ | grep -E \"smart|suite|aikb\"   # 配置存在?\n  ls /etc/nginx/sites-enabled/ | grep -E \"smart|suite|aikb\"     # symlink 存在?\n\n  # 3. 查配置内容(server_name / proxy_pass / listen)\n  sudo cat /etc/nginx/sites-available/{domain} | grep -E \"server_name|proxy_pass|listen\" | head\n\n  # 4. 查后端进程是否真在跑(systemd 服务 + 端口)\n  systemctl status smartsite-frontend | head -5\n  ss -tlnp | grep -E \":3073|:8096\"\n  ```\n- **修复**:\n  ```bash\n  # 1. 补 symlink(sites-available 有但 enabled 无)\n  sudo ln -sf /etc/nginx/sites-available/{domain} /etc/nginx/sites-enabled/{domain}\n\n  # 2. 修 server_name 错配(ai-ip-temp 是 default_server,删掉或改 server_name)\n  # 3. 修 proxy_pass 端口(用 ss -tlnp 确认真实端口)\n  # 4. 测试 + reload\n  sudo nginx -t && sudo nginx -s reload\n  ```\n- **验证**:重新批量 curl 域名,000 → 200/302\n- **预防**:每次 deploy 新站后立即 `ls /etc/nginx/sites-enabled/` 确认 symlink 已建;把 sites-enabled 清单记入 inventory.json 或运维文档,方便比对\"哪些站点应该启用\"\n\n### 坑 9: 静态站新增 HTML 页面 403(复制后权限 600)/ root 指向 /var/www 非本地源\n\n- **症状**:`curl https://<domain>/newpage.html` 返回 **403**,但同目录 `index.html` 200、`assets/xxx.png` 200\n- **根因**:nginx(`www-data`)读不了**权限 600** 的文件。用 `write_file` 生成的源文件权限是 **600**,`sudo cp` 到 `/var/www/<site>/` 还是 600;而 index.html 是 644。nginx 无 read 权限 → 403\n- **修复**:\n  ```bash\n  sudo chmod 644 /var/www/<site>/newpage.html\n  sudo chown www-data:www-data /var/www/<site>/newpage.html\n  ```\n- **关键**:aicnnc.com 类**静态站**的 nginx `root` 是 **`/var/www/<site>`**(不是本地源 `~/projects/<site>`)。部署 = 把本地源**复制/同步**到 `/var/www/<site>` + 修权限。**改本地源不会自动上线上**,必须 cp。\n- **预防**:新增静态页面一律 `chmod 644`;部署后先 `sudo ls -la /var/www/<site>/` 确认目标文件权限与 index.html 一致,再 `curl` 验证 200。\n\n> 注:本流程的静态站(aicnnc/www/cases 等)nginx 是 `root` 直接服务 `/var/www/<site>` + `location /` try_files,非上面的 FastAPI+H5 反代模式。两条线要分清。\n\n## 验证命令\n\n```bash\n# 1. 证书\nopenssl s_client -servername {domain} -connect {domain}:443 | grep \"subject=\"\n\n# 2. 网站\ncurl -skI https://{domain}\n\n# 3. H5 内容\ncurl -sk https://{domain} | head -3\n\n# 4. API\ncurl -sk https://{domain}/api/health\n\n# 5. nginx 生效\nsudo nginx -t && sudo systemctl restart nginx\n```\n\n## 关联\n\n- skill: `data-first-recommendation`(建议前查数据)\n- memory: 手机友好模式 + 数据优先原则\n- LEARNINGS.md: 试错记录",
  "path": "/home/agentuser/.hermes/skills/nginx-site-deploy/SKILL.md",
  "size": 10703
}