[{"data":1,"prerenderedAt":1365},["ShallowReactive",2],{"rainink-post-multi-site-deploy-to-https":3,"rainink-content-index":1183},{"id":4,"title":5,"body":6,"category":1166,"cover":1167,"date":1168,"description":1169,"draft":1170,"extension":1171,"featured":1170,"lang":1167,"meta":1172,"navigation":488,"order":1167,"path":1173,"period":1167,"pinned":1170,"repo":1167,"seo":1174,"slug":1167,"stack":1167,"stem":1175,"tags":1176,"thumb":1167,"toc":488,"updated":1167,"__hash__":1182},"content\u002Fposts\u002Fmulti-site-deploy-to-https.md","一台腾讯云轻量跑起多站点：从裸机到 HTTPS 的实际路径",{"type":7,"value":8,"toc":1155},"minimark",[9,16,20,23,85,88,92,95,98,150,157,161,164,213,223,230,262,273,280,284,287,321,324,373,380,403,413,416,450,456,460,467,677,702,705,744,751,785,788,847,861,865,868,925,932,935,952,959,963,966,1027,1034,1079,1082,1085,1088,1127,1133,1136,1139,1151],[10,11,12],"blockquote",{},[13,14,15],"p",{},"这不是一份\"照着抄\"的部署清单，而是一台真实服务器从裸机到 HTTPS 全绿的过程记录。",[17,18,19],"h2",{"id":19},"目标与约束",[13,21,22],{},"要在一台腾讯云轻量（2C4G，Ubuntu 24.04，纯净镜像）上同时跑三类东西：",[24,25,26,42],"table",{},[27,28,29],"thead",{},[30,31,32,36,39],"tr",{},[33,34,35],"th",{},"站点",[33,37,38],{},"形态",[33,40,41],{},"端口",[43,44,45,60,73],"tbody",{},[30,46,47,54,57],{},[48,49,50],"td",{},[51,52,53],"code",{},"admin.rainink.net",[48,55,56],{},"Vue 3 SPA 后台（NestJS 接口）",[48,58,59],{},"443 → 静态目录",[30,61,62,67,70],{},[48,63,64],{},[51,65,66],{},"api.rainink.net",[48,68,69],{},"NestJS 后端",[48,71,72],{},"443 → 127.0.0.1:3000",[30,74,75,80,83],{},[48,76,77],{},[51,78,79],{},"rainink.net",[48,81,82],{},"Nuxt 4 纯静态站",[48,84,59],{},[13,86,87],{},"约束有几个：国内服务器，域名必须备案；同机多站点共用 80\u002F443；静态站不允许在服务器上常驻 Node 进程。",[17,89,91],{"id":90},"施工顺序由备案决定不由技术决定","施工顺序由备案决定，不由技术决定",[13,93,94],{},"一开始按\"技术顺序\"排：装环境 → 部署 → 配 Nginx → 上 HTTPS。跑到 HTTPS 就卡住了，因为 ACME 验证需要域名解析到这台机器，而域名解析又要等备案。",[13,96,97],{},"所以真正的顺序是三段：",[24,99,100,113],{},[27,101,102],{},[30,103,104,107,110],{},[33,105,106],{},"阶段",[33,108,109],{},"依赖备案",[33,111,112],{},"可做的事",[43,114,115,126,136],{},[30,116,117,120,123],{},[48,118,119],{},"备案前",[48,121,122],{},"否",[48,124,125],{},"服务器初始化、装运行时、拉代码、构建、本机端口跑通",[30,127,128,131,133],{},[48,129,130],{},"备案审核中",[48,132,122],{},[48,134,135],{},"Nginx 配置、内网反代、用 IP + 非标端口验收，写完文档",[30,137,138,141,147],{},[48,139,140],{},"备案后",[48,142,143],{},[144,145,146],"strong",{},"是",[48,148,149],{},"域名解析 → 签发证书 → 强制跳转 HTTPS → 公网验收",[13,151,152,153,156],{},"备案审核通常一到两周，这段时间正好把前两段做完。核心原则只有一条：",[144,154,155],{},"审核期间域名不要解析","，否则被管局检测到会因为\"未备案即对外提供服务\"被驳回，重排一次又是两周。",[17,158,160],{"id":159},"第一段服务器初始化","第一段：服务器初始化",[13,162,163],{},"登录统一走密钥，不走密码：",[165,166,171],"pre",{"className":167,"code":168,"language":169,"meta":170,"style":170},"language-bash shiki shiki-themes github-light github-dark","ssh -i ~\u002F.ssh\u002Ftencent_cloud_ssh.pem -o StrictHostKeyChecking=no ubuntu@\u003C服务器IP>\n","bash","",[51,172,173],{"__ignoreMap":170},[174,175,178,182,186,190,193,196,199,203,206,210],"span",{"class":176,"line":177},"line",1,[174,179,181],{"class":180},"sScJk","ssh",[174,183,185],{"class":184},"sj4cs"," -i",[174,187,189],{"class":188},"sZZnC"," ~\u002F.ssh\u002Ftencent_cloud_ssh.pem",[174,191,192],{"class":184}," -o",[174,194,195],{"class":188}," StrictHostKeyChecking=no",[174,197,198],{"class":188}," ubuntu@",[174,200,202],{"class":201},"szBVR","\u003C",[174,204,205],{"class":188},"服务器I",[174,207,209],{"class":208},"sVt8B","P",[174,211,212],{"class":201},">\n",[13,214,215,218,219,222],{},[51,216,217],{},"StrictHostKeyChecking=no"," 只在首次连接时省掉指纹确认，长期运维还是应该把指纹写进 ",[51,220,221],{},"known_hosts","。",[13,224,225,226,229],{},"安装运行时的时候，国内服务器最容易卡在 Node 这一步——",[51,227,228],{},"nvm"," 的安装脚本从 GitHub 拉，大概率超时。改用镜像：",[165,231,233],{"className":167,"code":232,"language":169,"meta":170,"style":170},"export NVM_SOURCE=https:\u002F\u002Fgitee.com\u002Fmirrors\u002Fnvm.git\nexport NODEJS_ORG_MIRROR=https:\u002F\u002Fnpmmirror.com\u002Fmirrors\u002Fnode\u002F\n",[51,234,235,249],{"__ignoreMap":170},[174,236,237,240,243,246],{"class":176,"line":177},[174,238,239],{"class":201},"export",[174,241,242],{"class":208}," NVM_SOURCE",[174,244,245],{"class":201},"=",[174,247,248],{"class":208},"https:\u002F\u002Fgitee.com\u002Fmirrors\u002Fnvm.git\n",[174,250,252,254,257,259],{"class":176,"line":251},2,[174,253,239],{"class":201},[174,255,256],{"class":208}," NODEJS_ORG_MIRROR",[174,258,245],{"class":201},[174,260,261],{"class":208},"https:\u002F\u002Fnpmmirror.com\u002Fmirrors\u002Fnode\u002F\n",[13,263,264,265,268,269,272],{},"装完 Node，把 ",[51,266,267],{},"npm"," 源也指到镜像，否则 ",[51,270,271],{},"pnpm install"," 会在拉包上耗掉半小时。",[13,274,275,276,279],{},"数据库和应用分开装：MySQL 只监听 ",[51,277,278],{},"127.0.0.1","，Redis 同样只给本机用。这是同机部署最容易被忽略的一条——数据库端口一旦对公网开放，等于把整个库交给扫描器。",[17,281,283],{"id":282},"第二段拉代码构建跑通","第二段：拉代码、构建、跑通",[13,285,286],{},"私有仓库用 deploy key，不共用个人 SSH key：",[165,288,290],{"className":167,"code":289,"language":169,"meta":170,"style":170},"ssh-keygen -t ed25519 -C \"deploy@server\" -f ~\u002F.ssh\u002Fdeploy_key\n# 公钥贴到仓库的 Deploy Keys，勾选只读\n",[51,291,292,315],{"__ignoreMap":170},[174,293,294,297,300,303,306,309,312],{"class":176,"line":177},[174,295,296],{"class":180},"ssh-keygen",[174,298,299],{"class":184}," -t",[174,301,302],{"class":188}," ed25519",[174,304,305],{"class":184}," -C",[174,307,308],{"class":188}," \"deploy@server\"",[174,310,311],{"class":184}," -f",[174,313,314],{"class":188}," ~\u002F.ssh\u002Fdeploy_key\n",[174,316,317],{"class":176,"line":251},[174,318,320],{"class":319},"sJ8bj","# 公钥贴到仓库的 Deploy Keys，勾选只读\n",[13,322,323],{},"后端构建与启动：",[165,325,327],{"className":167,"code":326,"language":169,"meta":170,"style":170},"cd \u002Fvar\u002Fwww\u002Fsuper-admin-backend\npnpm install --prod=false\npnpm build\nNODE_ENV=production node dist\u002Fmain.js\n",[51,328,329,337,348,356],{"__ignoreMap":170},[174,330,331,334],{"class":176,"line":177},[174,332,333],{"class":184},"cd",[174,335,336],{"class":188}," \u002Fvar\u002Fwww\u002Fsuper-admin-backend\n",[174,338,339,342,345],{"class":176,"line":251},[174,340,341],{"class":180},"pnpm",[174,343,344],{"class":188}," install",[174,346,347],{"class":184}," --prod=false\n",[174,349,351,353],{"class":176,"line":350},3,[174,352,341],{"class":180},[174,354,355],{"class":188}," build\n",[174,357,359,362,364,367,370],{"class":176,"line":358},4,[174,360,361],{"class":208},"NODE_ENV",[174,363,245],{"class":201},[174,365,366],{"class":188},"production",[174,368,369],{"class":180}," node",[174,371,372],{"class":188}," dist\u002Fmain.js\n",[13,374,375,376,379],{},"先用前台方式跑一次，确认进程能起来、日志没有报错、",[51,377,378],{},"curl 127.0.0.1:3000\u002Fapi"," 有响应，再交给进程守护。",[13,381,382,383,386,387,390,391,394,395,398,399,402],{},"这里有个反复踩的坑：TypeORM 项目部署到空库，",[51,384,385],{},"migration:run"," 报 ",[51,388,389],{},"Entity metadata for Menu#roleMenus was not found","。原因是 ",[51,392,393],{},"data-source.ts"," 只手写注册了部分实体，另外还同时写了 ",[51,396,397],{},"export const"," 和 ",[51,400,401],{},"export default","，导致\"data source 只能有一个导出\"的报错。",[13,404,405,406,408,409,412],{},"处理顺序是：删掉多余的 ",[51,407,401],{},"，实体改成 glob 扫描模块目录，没有迁移文件时先 ",[51,410,411],{},"DB_SYNC=true"," 让同步建表跑通，再补迁移。",[13,414,415],{},"静态站不需要任何运行时：",[165,417,419],{"className":167,"code":418,"language":169,"meta":170,"style":170},"pnpm generate\n# 产物在 .output\u002Fpublic，直接同步到 \u002Fvar\u002Fwww\u002Frainink\nrsync -a --delete .output\u002Fpublic\u002F \u002Fvar\u002Fwww\u002Frainink\u002F\n",[51,420,421,428,433],{"__ignoreMap":170},[174,422,423,425],{"class":176,"line":177},[174,424,341],{"class":180},[174,426,427],{"class":188}," generate\n",[174,429,430],{"class":176,"line":251},[174,431,432],{"class":319},"# 产物在 .output\u002Fpublic，直接同步到 \u002Fvar\u002Fwww\u002Frainink\n",[174,434,435,438,441,444,447],{"class":176,"line":350},[174,436,437],{"class":180},"rsync",[174,439,440],{"class":184}," -a",[174,442,443],{"class":184}," --delete",[174,445,446],{"class":188}," .output\u002Fpublic\u002F",[174,448,449],{"class":188}," \u002Fvar\u002Fwww\u002Frainink\u002F\n",[13,451,452,455],{},[51,453,454],{},"--delete"," 要带上，否则旧的文章页会永远留在服务器上。",[17,457,459],{"id":458},"第三段nginx-server-块","第三段：Nginx server 块",[13,461,462,463,466],{},"静态站的实际配置（",[51,464,465],{},"\u002Fetc\u002Fnginx\u002Fsites-available\u002Frainink.conf","）：",[165,468,472],{"className":469,"code":470,"language":471,"meta":170,"style":170},"language-nginx shiki shiki-themes github-light github-dark","server {\n    server_name rainink.net www.rainink.net;\n\n    root \u002Fvar\u002Fwww\u002Frainink;\n    index index.html;\n\n    charset utf-8;\n\n    gzip on;\n    gzip_vary on;\n    gzip_min_length 1024;\n    gzip_types text\u002Fcss application\u002Fjavascript application\u002Fjson image\u002Fsvg+xml text\u002Fplain application\u002Fxml application\u002Frss+xml;\n\n    access_log \u002Fvar\u002Flog\u002Fnginx\u002Frainink.access.log;\n    error_log  \u002Fvar\u002Flog\u002Fnginx\u002Frainink.error.log;\n\n    location \u002F {\n        try_files $uri\u002Findex.html $uri $uri.html =404;\n    }\n\n    # 构建产物文件名带 hash，可以放心长缓存\n    location \u002F_nuxt\u002F {\n        expires 1y;\n        add_header Cache-Control \"public, immutable\";\n        access_log off;\n    }\n\n    location = \u002Ffavicon.svg {\n        expires 7d;\n    }\n\n    error_page 404 \u002F404.html;\n    location = \u002F404.html {\n        internal;\n    }\n}\n","nginx",[51,473,474,479,484,490,495,501,506,512,517,523,529,535,541,546,552,558,563,569,575,581,586,592,598,604,610,616,621,626,632,638,643,648,654,660,666,671],{"__ignoreMap":170},[174,475,476],{"class":176,"line":177},[174,477,478],{},"server {\n",[174,480,481],{"class":176,"line":251},[174,482,483],{},"    server_name rainink.net www.rainink.net;\n",[174,485,486],{"class":176,"line":350},[174,487,489],{"emptyLinePlaceholder":488},true,"\n",[174,491,492],{"class":176,"line":358},[174,493,494],{},"    root \u002Fvar\u002Fwww\u002Frainink;\n",[174,496,498],{"class":176,"line":497},5,[174,499,500],{},"    index index.html;\n",[174,502,504],{"class":176,"line":503},6,[174,505,489],{"emptyLinePlaceholder":488},[174,507,509],{"class":176,"line":508},7,[174,510,511],{},"    charset utf-8;\n",[174,513,515],{"class":176,"line":514},8,[174,516,489],{"emptyLinePlaceholder":488},[174,518,520],{"class":176,"line":519},9,[174,521,522],{},"    gzip on;\n",[174,524,526],{"class":176,"line":525},10,[174,527,528],{},"    gzip_vary on;\n",[174,530,532],{"class":176,"line":531},11,[174,533,534],{},"    gzip_min_length 1024;\n",[174,536,538],{"class":176,"line":537},12,[174,539,540],{},"    gzip_types text\u002Fcss application\u002Fjavascript application\u002Fjson image\u002Fsvg+xml text\u002Fplain application\u002Fxml application\u002Frss+xml;\n",[174,542,544],{"class":176,"line":543},13,[174,545,489],{"emptyLinePlaceholder":488},[174,547,549],{"class":176,"line":548},14,[174,550,551],{},"    access_log \u002Fvar\u002Flog\u002Fnginx\u002Frainink.access.log;\n",[174,553,555],{"class":176,"line":554},15,[174,556,557],{},"    error_log  \u002Fvar\u002Flog\u002Fnginx\u002Frainink.error.log;\n",[174,559,561],{"class":176,"line":560},16,[174,562,489],{"emptyLinePlaceholder":488},[174,564,566],{"class":176,"line":565},17,[174,567,568],{},"    location \u002F {\n",[174,570,572],{"class":176,"line":571},18,[174,573,574],{},"        try_files $uri\u002Findex.html $uri $uri.html =404;\n",[174,576,578],{"class":176,"line":577},19,[174,579,580],{},"    }\n",[174,582,584],{"class":176,"line":583},20,[174,585,489],{"emptyLinePlaceholder":488},[174,587,589],{"class":176,"line":588},21,[174,590,591],{},"    # 构建产物文件名带 hash，可以放心长缓存\n",[174,593,595],{"class":176,"line":594},22,[174,596,597],{},"    location \u002F_nuxt\u002F {\n",[174,599,601],{"class":176,"line":600},23,[174,602,603],{},"        expires 1y;\n",[174,605,607],{"class":176,"line":606},24,[174,608,609],{},"        add_header Cache-Control \"public, immutable\";\n",[174,611,613],{"class":176,"line":612},25,[174,614,615],{},"        access_log off;\n",[174,617,619],{"class":176,"line":618},26,[174,620,580],{},[174,622,624],{"class":176,"line":623},27,[174,625,489],{"emptyLinePlaceholder":488},[174,627,629],{"class":176,"line":628},28,[174,630,631],{},"    location = \u002Ffavicon.svg {\n",[174,633,635],{"class":176,"line":634},29,[174,636,637],{},"        expires 7d;\n",[174,639,641],{"class":176,"line":640},30,[174,642,580],{},[174,644,646],{"class":176,"line":645},31,[174,647,489],{"emptyLinePlaceholder":488},[174,649,651],{"class":176,"line":650},32,[174,652,653],{},"    error_page 404 \u002F404.html;\n",[174,655,657],{"class":176,"line":656},33,[174,658,659],{},"    location = \u002F404.html {\n",[174,661,663],{"class":176,"line":662},34,[174,664,665],{},"        internal;\n",[174,667,669],{"class":176,"line":668},35,[174,670,580],{},[174,672,674],{"class":176,"line":673},36,[174,675,676],{},"}\n",[13,678,679,682,683,686,687,690,691,686,694,697,698,701],{},[51,680,681],{},"try_files"," 的顺序是关键。纯静态导出会把 ",[51,684,685],{},"\u002Fposts\u002Ffoo"," 生成为 ",[51,688,689],{},"posts\u002Ffoo\u002Findex.html","，把 ",[51,692,693],{},"\u002Fabout",[51,695,696],{},"about.html","（取决于 ",[51,699,700],{},"trailingSlash","）。两个都兜住，才不会出现\"首页能开、文章页 404\"。",[13,703,704],{},"接口站的反代要点：",[165,706,708],{"className":469,"code":707,"language":471,"meta":170,"style":170},"location \u002Fapi\u002F {\n    proxy_pass http:\u002F\u002F127.0.0.1:3000\u002Fapi\u002F;\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",[51,709,710,715,720,725,730,735,740],{"__ignoreMap":170},[174,711,712],{"class":176,"line":177},[174,713,714],{},"location \u002Fapi\u002F {\n",[174,716,717],{"class":176,"line":251},[174,718,719],{},"    proxy_pass http:\u002F\u002F127.0.0.1:3000\u002Fapi\u002F;\n",[174,721,722],{"class":176,"line":350},[174,723,724],{},"    proxy_set_header Host $host;\n",[174,726,727],{"class":176,"line":358},[174,728,729],{},"    proxy_set_header X-Real-IP $remote_addr;\n",[174,731,732],{"class":176,"line":497},[174,733,734],{},"    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n",[174,736,737],{"class":176,"line":503},[174,738,739],{},"    proxy_set_header X-Forwarded-Proto $scheme;\n",[174,741,742],{"class":176,"line":508},[174,743,676],{},[13,745,746,747,750],{},"WebSocket 单独一段，必须转发 ",[51,748,749],{},"Upgrade","：",[165,752,754],{"className":469,"code":753,"language":471,"meta":170,"style":170},"location \u002Fws {\n    proxy_pass http:\u002F\u002F127.0.0.1:3000;\n    proxy_http_version 1.1;\n    proxy_set_header Upgrade $http_upgrade;\n    proxy_set_header Connection \"upgrade\";\n}\n",[51,755,756,761,766,771,776,781],{"__ignoreMap":170},[174,757,758],{"class":176,"line":177},[174,759,760],{},"location \u002Fws {\n",[174,762,763],{"class":176,"line":251},[174,764,765],{},"    proxy_pass http:\u002F\u002F127.0.0.1:3000;\n",[174,767,768],{"class":176,"line":350},[174,769,770],{},"    proxy_http_version 1.1;\n",[174,772,773],{"class":176,"line":358},[174,774,775],{},"    proxy_set_header Upgrade $http_upgrade;\n",[174,777,778],{"class":176,"line":497},[174,779,780],{},"    proxy_set_header Connection \"upgrade\";\n",[174,782,783],{"class":176,"line":503},[174,784,676],{},[13,786,787],{},"每次改配置都先备份再 reload：",[165,789,791],{"className":167,"code":790,"language":169,"meta":170,"style":170},"sudo cp \u002Fetc\u002Fnginx\u002Fsites-available\u002Frainink.conf \\\n        \u002Fetc\u002Fnginx\u002Fsites-available\u002Frainink.conf.bak.$(date +%Y%m%d%H%M%S)\nsudo nginx -t && sudo systemctl reload nginx\n",[51,792,793,807,824],{"__ignoreMap":170},[174,794,795,798,801,804],{"class":176,"line":177},[174,796,797],{"class":180},"sudo",[174,799,800],{"class":188}," cp",[174,802,803],{"class":188}," \u002Fetc\u002Fnginx\u002Fsites-available\u002Frainink.conf",[174,805,806],{"class":184}," \\\n",[174,808,809,812,815,818,821],{"class":176,"line":251},[174,810,811],{"class":188},"        \u002Fetc\u002Fnginx\u002Fsites-available\u002Frainink.conf.bak.",[174,813,814],{"class":208},"$(",[174,816,817],{"class":180},"date",[174,819,820],{"class":188}," +%Y%m%d%H%M%S",[174,822,823],{"class":208},")\n",[174,825,826,828,831,833,836,838,841,844],{"class":176,"line":350},[174,827,797],{"class":180},[174,829,830],{"class":188}," nginx",[174,832,299],{"class":184},[174,834,835],{"class":208}," && ",[174,837,797],{"class":180},[174,839,840],{"class":188}," systemctl",[174,842,843],{"class":188}," reload",[174,845,846],{"class":188}," nginx\n",[13,848,849,852,853,856,857,860],{},[51,850,851],{},"nginx -t"," 必须串在 reload 前面，用 ",[51,854,855],{},"&&"," 而不是 ",[51,858,859],{},";","——配置写错时直接中断，不让坏配置生效。",[17,862,864],{"id":863},"第四段证书与强制-https","第四段：证书与强制 HTTPS",[13,866,867],{},"域名解析生效后，一条命令签发并自动改写 server 块：",[165,869,871],{"className":167,"code":870,"language":169,"meta":170,"style":170},"sudo certbot --nginx -d rainink.net -d www.rainink.net \\\n  --non-interactive --agree-tos -m \u003C你的邮箱> --redirect --keep-until-expiring\n",[51,872,873,896],{"__ignoreMap":170},[174,874,875,877,880,883,886,889,891,894],{"class":176,"line":177},[174,876,797],{"class":180},[174,878,879],{"class":188}," certbot",[174,881,882],{"class":184}," --nginx",[174,884,885],{"class":184}," -d",[174,887,888],{"class":188}," rainink.net",[174,890,885],{"class":184},[174,892,893],{"class":188}," www.rainink.net",[174,895,806],{"class":184},[174,897,898,901,904,907,910,913,916,919,922],{"class":176,"line":251},[174,899,900],{"class":184},"  --non-interactive",[174,902,903],{"class":184}," --agree-tos",[174,905,906],{"class":184}," -m",[174,908,909],{"class":201}," \u003C",[174,911,912],{"class":188},"你的邮",[174,914,915],{"class":208},"箱",[174,917,918],{"class":201},">",[174,920,921],{"class":184}," --redirect",[174,923,924],{"class":184}," --keep-until-expiring\n",[13,926,927,928,931],{},"certbot 会做三件事：把 443 的 ",[51,929,930],{},"listen"," 和证书路径写进 server 块、补一个 80 端口只做 301 跳转的 server 块、注册自动续期定时任务。",[13,933,934],{},"验证续期是否真的可用：",[165,936,938],{"className":167,"code":937,"language":169,"meta":170,"style":170},"sudo certbot renew --dry-run\n",[51,939,940],{"__ignoreMap":170},[174,941,942,944,946,949],{"class":176,"line":177},[174,943,797],{"class":180},[174,945,879],{"class":188},[174,947,948],{"class":188}," renew",[174,950,951],{"class":184}," --dry-run\n",[13,953,954,955,958],{},"输出里应该看到每张证书 ",[51,956,957],{},"success","。这一步不能省——续期失败通常没人发现，直到某天证书过期、所有接口报错。",[17,960,962],{"id":961},"上线验收不只看能不能打开","上线验收：不只看\"能不能打开\"",[13,964,965],{},"验收按五件事依次过：",[967,968,969,979,999,1008,1021],"ol",{},[970,971,972,750,975,978],"li",{},[144,973,974],{},"状态码",[51,976,977],{},"https:\u002F\u002F站点\u002F"," 与关键子路径都要 200；",[970,980,981,750,984,987,988,991,992,995,996,998],{},[144,982,983],{},"跳转",[51,985,986],{},"http:\u002F\u002F"," 必须 301 到 ",[51,989,990],{},"https:\u002F\u002F","，且 ",[51,993,994],{},"www"," 与非 ",[51,997,994],{}," 只有一个规范域；",[970,1000,1001,750,1004,1007],{},[144,1002,1003],{},"证书主体",[51,1005,1006],{},"openssl s_client -connect 127.0.0.1:443 -servername rainink.net"," 看 SAN 是否包含两个域名；",[970,1009,1010,750,1013,1016,1017,1020],{},[144,1011,1012],{},"多站点互不影响",[51,1014,1015],{},"admin","、",[51,1018,1019],{},"api"," 子域各自 200，改一个站的配置不能打断另一个；",[970,1022,1023,1026],{},[144,1024,1025],{},"404 行为","：随机不存在的路径要返回 404，而不是首页内容（否则 SEO 会收录一堆软 404）。",[13,1028,1029,1030,1033],{},"域名还没解析、或者想绕过 DNS 直接验证服务器时，用 ",[51,1031,1032],{},"--resolve"," 把域名指到 IP：",[165,1035,1037],{"className":167,"code":1036,"language":169,"meta":170,"style":170},"curl -sS -o \u002Fdev\u002Fnull -w \"%{http_code}\\n\" \\\n  --resolve rainink.net:443:\u003C服务器IP> https:\u002F\u002Frainink.net\u002F\n",[51,1038,1039,1060],{"__ignoreMap":170},[174,1040,1041,1044,1047,1049,1052,1055,1058],{"class":176,"line":177},[174,1042,1043],{"class":180},"curl",[174,1045,1046],{"class":184}," -sS",[174,1048,192],{"class":184},[174,1050,1051],{"class":188}," \u002Fdev\u002Fnull",[174,1053,1054],{"class":184}," -w",[174,1056,1057],{"class":188}," \"%{http_code}\\n\"",[174,1059,806],{"class":184},[174,1061,1062,1065,1068,1070,1072,1074,1076],{"class":176,"line":251},[174,1063,1064],{"class":184},"  --resolve",[174,1066,1067],{"class":188}," rainink.net:443:",[174,1069,202],{"class":201},[174,1071,205],{"class":188},[174,1073,209],{"class":208},[174,1075,918],{"class":201},[174,1077,1078],{"class":188}," https:\u002F\u002Frainink.net\u002F\n",[13,1080,1081],{},"这个技巧在备案期验收特别有用：不解析域名，也能验证整条链路是通的。",[17,1083,1084],{"id":1084},"回滚",[13,1086,1087],{},"配置回滚靠时间戳备份：",[165,1089,1091],{"className":167,"code":1090,"language":169,"meta":170,"style":170},"sudo cp \u002Fetc\u002Fnginx\u002Fsites-available\u002Frainink.conf.bak.20261022163044 \\\n        \u002Fetc\u002Fnginx\u002Fsites-available\u002Frainink.conf\nsudo nginx -t && sudo systemctl reload nginx\n",[51,1092,1093,1104,1109],{"__ignoreMap":170},[174,1094,1095,1097,1099,1102],{"class":176,"line":177},[174,1096,797],{"class":180},[174,1098,800],{"class":188},[174,1100,1101],{"class":188}," \u002Fetc\u002Fnginx\u002Fsites-available\u002Frainink.conf.bak.20261022163044",[174,1103,806],{"class":184},[174,1105,1106],{"class":176,"line":251},[174,1107,1108],{"class":188},"        \u002Fetc\u002Fnginx\u002Fsites-available\u002Frainink.conf\n",[174,1110,1111,1113,1115,1117,1119,1121,1123,1125],{"class":176,"line":350},[174,1112,797],{"class":180},[174,1114,830],{"class":188},[174,1116,299],{"class":184},[174,1118,835],{"class":208},[174,1120,797],{"class":180},[174,1122,840],{"class":188},[174,1124,843],{"class":188},[174,1126,846],{"class":188},[13,1128,1129,1130,1132],{},"静态站回滚靠 Git：把上一个版本的构建产物重新 ",[51,1131,437],{}," 一次即可。所以每次发布前，产物目录要保留至少一版。",[17,1134,1135],{"id":1135},"小结",[13,1137,1138],{},"同机多站点的复杂度不在技术，而在顺序：备案决定了什么时候能上域名，域名决定了什么时候能签证书。把\"不依赖备案\"的工作全部前置，等域名可用时只剩解析、证书、验收三步，整个上线过程就是可控的。",[13,1140,1141,1142,1144,1145,1147,1148,1150],{},"真正要小心的反而是那些不起眼的细节——",[51,1143,681],{}," 的顺序、",[51,1146,851],{}," 有没有串上、",[51,1149,454],{}," 有没有带、续期演练做没做。这些不出问题时没人看得见，出问题时都是全站级别的。",[1152,1153,1154],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":170,"searchDepth":251,"depth":251,"links":1156},[1157,1158,1159,1160,1161,1162,1163,1164,1165],{"id":19,"depth":251,"text":19},{"id":90,"depth":251,"text":91},{"id":159,"depth":251,"text":160},{"id":282,"depth":251,"text":283},{"id":458,"depth":251,"text":459},{"id":863,"depth":251,"text":864},{"id":961,"depth":251,"text":962},{"id":1084,"depth":251,"text":1084},{"id":1135,"depth":251,"text":1135},"tech",null,"2026-10-21","一台 2C4G 腾讯云轻量同时承载后台、接口与静态站：备案施工顺序、Nginx 多 server 块、certbot 签证书与上线验收。",false,"md",{},"\u002Fposts\u002Fmulti-site-deploy-to-https",{"title":5,"description":1169},"posts\u002Fmulti-site-deploy-to-https",[1177,1178,1179,1180,1181],"部署","Nginx","HTTPS","运维","腾讯云","jY_UUdIdD0uJNmb-AmpBq8Jn5LFuFT9p-WBrWPKt-BQ",{"all":1184,"posts":1300,"essays":1325,"projects":1326,"tags":1333},[1185,1201,1211,1215,1227,1239,1249,1261,1272,1290],{"title":1186,"description":1187,"path":1188,"slug":1189,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1192,"date":1197,"updated":1198,"minutes":497,"pinned":488,"draft":1170,"order":1199,"stack":1200,"period":170,"repo":170,"thumb":170},"用 Nuxt 4 + Nuxt Content 搭一个纯静态个人站","站点骨架的取舍：内容目录怎么定、SSG 怎么预渲染、设计令牌怎么从原型抽成一套 CSS 变量，以及首屏主题闪烁的解法。","\u002Fposts\u002Fnuxt4-static-site","nuxt4-static-site","技术","post",[1193,1194,1195,1196],"Nuxt","Nuxt Content","SSG","前端工程化","2026-10-12","2026-10-18",0,[],{"title":1202,"description":1203,"path":1204,"slug":1205,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1206,"date":1209,"updated":170,"minutes":503,"pinned":1170,"draft":1170,"order":1199,"stack":1210,"period":170,"repo":170,"thumb":170},"国内服务器部署的十个坑：从镜像源到 certbot","从镜像源到 certbot 的十个真实故障：nvm 拉不动、目录权限、空库迁移、Host 头命中错站、证书续期与备案期验收。","\u002Fposts\u002Fcn-server-deploy-pitfalls","cn-server-deploy-pitfalls",[1177,1207,1178,1208,1180],"踩坑","PowerShell","2026-10-22",[],{"title":5,"description":1169,"path":1173,"slug":1212,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1213,"date":1168,"updated":170,"minutes":508,"pinned":1170,"draft":1170,"order":1199,"stack":1214,"period":170,"repo":170,"thumb":170},"multi-site-deploy-to-https",[1177,1178,1179,1180,1181],[],{"title":1216,"description":1217,"path":1218,"slug":1219,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1220,"date":1225,"updated":170,"minutes":358,"pinned":1170,"draft":1170,"order":1199,"stack":1226,"period":170,"repo":170,"thumb":170},"底座能力的边界：什么该沉进数据字典，什么必须单独立表","B 端底座能力的边界判断：哪些配置该沉进数据字典、哪些必须单独立表，以及单级字典不升级成树的理由。","\u002Fposts\u002Fbase-dictionary-boundary","base-dictionary-boundary",[1221,1222,1223,1224],"B端","架构设计","数据字典","super-admin","2026-10-20",[],{"title":1228,"description":1229,"path":1230,"slug":1231,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1232,"date":1225,"updated":170,"minutes":358,"pinned":1170,"draft":1170,"order":1199,"stack":1238,"period":170,"repo":170,"thumb":170},"先画全链路，再切分期：我做 ERP\u002FWMS\u002FMES\u002FQC 需求的方法","ERP\u002FWMS\u002FMES\u002FQC 需求方法：先用编号体系与硬闸点把端到端流程关进文档，再按里程碑切成可交付的分期。","\u002Fposts\u002Ferp-wms-mes-qc-requirement-method","erp-wms-mes-qc-requirement-method",[1233,1234,1235,1236,1237],"MES","ERP","WMS","需求设计","流程设计",[],{"title":1240,"description":1241,"path":1242,"slug":1243,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1244,"date":1247,"updated":170,"minutes":358,"pinned":1170,"draft":1170,"order":1199,"stack":1248,"period":170,"repo":170,"thumb":170},"主数据状态机不是字段，是一份跨模块公共契约","MES 主数据状态机的设计取舍：六态加停用怎么定、四方口径为什么必须交叉核对，以及 BOM 与编号体系为什么会互相污染。","\u002Fposts\u002Fmes-master-data-state-machine","mes-master-data-state-machine",[1233,1245,1236,1246],"领域建模","状态机","2026-10-19",[],{"title":1250,"description":1251,"path":1252,"slug":1253,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1254,"date":1259,"updated":170,"minutes":497,"pinned":1170,"draft":1170,"order":1199,"stack":1260,"period":170,"repo":170,"thumb":170},"B 端后台底座：把权限、菜单、请求收敛成三层","后台系统做多了会发现，真正需要沉淀的不是页面，而是权限模型、动态菜单和请求层这三块底座。","\u002Fposts\u002Fvue3-admin-base","vue3-admin-base",[1255,1256,1257,1258],"Vue","后台","架构","权限","2026-09-24",[],{"title":1262,"description":1263,"path":1264,"slug":1265,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1266,"date":1270,"updated":170,"minutes":497,"pinned":1170,"draft":1170,"order":1199,"stack":1271,"period":170,"repo":170,"thumb":170},"工作流引擎实操笔记：节点、流转与幂等","从状态机到可配置流程：一次工作流引擎落地中关于节点模型、流转条件、幂等与补偿的实操记录。","\u002Fposts\u002Fworkflow-engine-notes","workflow-engine-notes",[1267,1268,1257,1269],"工作流","后端","幂等","2026-08-30",[],{"title":1273,"description":1274,"path":1275,"slug":1224,"category":1276,"categoryLabel":1277,"kind":1276,"tags":1278,"date":1279,"updated":170,"minutes":177,"pinned":1170,"draft":1170,"order":177,"stack":1280,"period":1288,"repo":170,"thumb":1289},"Super Admin 后台底座","一套可复用的 B 端后台前端底座：权限三层收敛、动态菜单、请求层封装与通用表格表单组件，新项目接入只需替换业务页面。","\u002Fprojects\u002Fsuper-admin","project","项目",[1255,1256,1196],"2026-05-20",[1281,1282,1283,1284,1285,1286,1287],"Vue 3","TypeScript","Vite","Pinia","Antdv Next","Node.js","Next.js","2026 年至今","SA",{"title":1291,"description":1292,"path":1293,"slug":1294,"category":1276,"categoryLabel":1277,"kind":1276,"tags":1295,"date":1198,"updated":170,"minutes":177,"pinned":1170,"draft":1170,"order":350,"stack":1296,"period":1298,"repo":170,"thumb":1299},"雨墨 RainInk 本站","你正在看的这个站：Nuxt 4 + Nuxt Content 的纯静态个人站，手写设计令牌、无评论无用户输入、产物可直接静态托管。","\u002Fprojects\u002Frainink-site","rainink-site",[1193,1195,1196],[1297,1281,1194,1282],"Nuxt 4","2026 年 10 月","RI",[1301,1304,1307,1310,1313,1316,1319,1322],{"title":1186,"description":1187,"path":1188,"slug":1189,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1302,"date":1197,"updated":1198,"minutes":497,"pinned":488,"draft":1170,"order":1199,"stack":1303,"period":170,"repo":170,"thumb":170},[1193,1194,1195,1196],[],{"title":1202,"description":1203,"path":1204,"slug":1205,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1305,"date":1209,"updated":170,"minutes":503,"pinned":1170,"draft":1170,"order":1199,"stack":1306,"period":170,"repo":170,"thumb":170},[1177,1207,1178,1208,1180],[],{"title":5,"description":1169,"path":1173,"slug":1212,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1308,"date":1168,"updated":170,"minutes":508,"pinned":1170,"draft":1170,"order":1199,"stack":1309,"period":170,"repo":170,"thumb":170},[1177,1178,1179,1180,1181],[],{"title":1216,"description":1217,"path":1218,"slug":1219,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1311,"date":1225,"updated":170,"minutes":358,"pinned":1170,"draft":1170,"order":1199,"stack":1312,"period":170,"repo":170,"thumb":170},[1221,1222,1223,1224],[],{"title":1228,"description":1229,"path":1230,"slug":1231,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1314,"date":1225,"updated":170,"minutes":358,"pinned":1170,"draft":1170,"order":1199,"stack":1315,"period":170,"repo":170,"thumb":170},[1233,1234,1235,1236,1237],[],{"title":1240,"description":1241,"path":1242,"slug":1243,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1317,"date":1247,"updated":170,"minutes":358,"pinned":1170,"draft":1170,"order":1199,"stack":1318,"period":170,"repo":170,"thumb":170},[1233,1245,1236,1246],[],{"title":1250,"description":1251,"path":1252,"slug":1253,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1320,"date":1259,"updated":170,"minutes":497,"pinned":1170,"draft":1170,"order":1199,"stack":1321,"period":170,"repo":170,"thumb":170},[1255,1256,1257,1258],[],{"title":1262,"description":1263,"path":1264,"slug":1265,"category":1166,"categoryLabel":1190,"kind":1191,"tags":1323,"date":1270,"updated":170,"minutes":497,"pinned":1170,"draft":1170,"order":1199,"stack":1324,"period":170,"repo":170,"thumb":170},[1267,1268,1257,1269],[],[],[1327,1330],{"title":1273,"description":1274,"path":1275,"slug":1224,"category":1276,"categoryLabel":1277,"kind":1276,"tags":1328,"date":1279,"updated":170,"minutes":177,"pinned":1170,"draft":1170,"order":177,"stack":1329,"period":1288,"repo":170,"thumb":1289},[1255,1256,1196],[1281,1282,1283,1284,1285,1286,1287],{"title":1291,"description":1292,"path":1293,"slug":1294,"category":1276,"categoryLabel":1277,"kind":1276,"tags":1331,"date":1198,"updated":170,"minutes":177,"pinned":1170,"draft":1170,"order":350,"stack":1332,"period":1298,"repo":170,"thumb":1299},[1193,1195,1196],[1297,1281,1194,1282],[1334,1336,1337,1338,1339,1340,1341,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364],{"name":1233,"count":251,"weight":1335},1.6,{"name":1178,"count":251,"weight":1335},{"name":1257,"count":251,"weight":1335},{"name":1180,"count":251,"weight":1335},{"name":1177,"count":251,"weight":1335},{"name":1236,"count":251,"weight":1335},{"name":1221,"count":177,"weight":1342},1.25,{"name":1234,"count":177,"weight":1342},{"name":1179,"count":177,"weight":1342},{"name":1193,"count":177,"weight":1342},{"name":1194,"count":177,"weight":1342},{"name":1208,"count":177,"weight":1342},{"name":1195,"count":177,"weight":1342},{"name":1224,"count":177,"weight":1342},{"name":1255,"count":177,"weight":1342},{"name":1235,"count":177,"weight":1342},{"name":1196,"count":177,"weight":1342},{"name":1256,"count":177,"weight":1342},{"name":1268,"count":177,"weight":1342},{"name":1267,"count":177,"weight":1342},{"name":1269,"count":177,"weight":1342},{"name":1223,"count":177,"weight":1342},{"name":1258,"count":177,"weight":1342},{"name":1222,"count":177,"weight":1342},{"name":1237,"count":177,"weight":1342},{"name":1246,"count":177,"weight":1342},{"name":1181,"count":177,"weight":1342},{"name":1207,"count":177,"weight":1342},{"name":1245,"count":177,"weight":1342},1792738754341]