前端build成静态产物、Nginx反向代理API、Let's Encrypt免费证书------从"localhost能访问"到"全世界能访问"。
🎯 本篇产出:前端生产Dockerfile(多阶段构建)+ nginx.conf + compose调整 + HTTPS上线流程。含代码约100行。
📌 太长不看版(给想快速上手的你)
| 项目信息 | 一句话说明 |
|---|---|
| 本篇目标 | 把早报站正式发布到公网 |
| 代码行数 | ~100行(Dockerfile + nginx.conf + compose) |
| 依赖 | nginx:1.30-alpine + certbot |
| 核心功能 | 多阶段构建 + 反向代理 + SPA兜底 + HTTPS |
| 跑起来的命令 | 域名访问 → 浏览器锁形图标 |
| 核心知识点 | 反向代理、try_files、proxy_pass、Let's Encrypt |
| 做完你能得到 | 一个全世界可访问、带HTTPS的早报站 |
一、概念课:Nginx是什么,反向代理在代理什么
Nginx :高性能Web服务器 + 反向代理。两件事都干:静态文件直接发 (比uvicorn快、省心)、动态请求转给别人(后端、API)。
🔄 正向代理 vs 反向代理
| 类型 | 代理谁 | 例子 |
|---|---|---|
| 正向代理 | 代"你"出去访问别人 | 翻墙软件 |
| 反向代理 | 代"服务器"接待所有进来的请求 | 一个前台接待:所有请求先到Nginx,它按规则分发给对应的服务 |
🎯 为什么上线离不开它
text
浏览器 → https://daily.example.com
↓ 443
Nginx(证书终结 + 静态文件 + 路由)
├─ / → dist/ 静态产物(前端)
└─ /api/ → 转发 backend:8000(FastAPI)
└─ /api/images/ → 走文件流(FileResponse已实现)
| # | 理由 | 说明 |
|---|---|---|
| ① | 统一入口 | 浏览器只需记一个域名 |
| ② | HTTPS只在一处配置 | 证书放Nginx即可 |
| ③ | 前后端同源 | 反代后/api和页面同域名------CORS问题自动消失 |
二、第0步:前端生产构建------多阶段Dockerfile
之前的前端是dev容器(npm run dev),上线要的是build后静态产物 。frontend/Dockerfile升级:
dockerfile
# 阶段一:构建(装依赖 → 打包)
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build # 产出dist/
# 阶段二:运行(只要产物 + Nginx)
FROM nginx:1.30-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
📌 多阶段构建的设计
🎯 构建工具(node、npm、几千个依赖)只存在于阶段一,最终镜像只带Nginx + dist产物------生产镜像瘦身20倍以上,攻击面也小得多。
🆕 镜像版本选择
| 镜像 | 当前状态 | 选型理由 |
|---|---|---|
node:24-alpine |
Active LTS(Krypton) | Node 20已EOL,Node 26是Current但2026年10月28日才进入Active LTS,生产环境用24最稳 |
nginx:1.30-alpine |
当前稳定分支(1.30.6) | 2026年4月替代1.28成为生产推荐分支,alpine变体约27MB |
💡 生产环境建议固定小版本标签(如
nginx:1.30.6-alpine),避免意外升级。
三、第1步:nginx.conf------静态 + 反代 + SPA兜底
frontend/nginx.conf:
nginx
server {
listen 80;
server_name daily.example.com; # 换成你的域名
root /usr/share/nginx/html;
index index.html;
# 前端静态文件 + SPA路由兜底
location / {
try_files $uri $uri/ /index.html; # ① 刷新任意路由不404
}
# API反向代理
location /api/ {
proxy_pass http://backend:8000; # ② 容器网络内转发(服务名)
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
📌 两个细节决定成败
| # | 细节 | 说明 |
|---|---|---|
| ① | try_files兜底 |
Vue Router用history模式,直接刷新/favorites时Nginx找不到文件会404------try_files ... /index.html把未知路径全部交给Vue路由接管。这就是"本地好好的、上线刷新就404"的幕后黑手 |
| ② | proxy_pass不带路径 |
location /api/ + proxy_pass http://backend:8000(结尾无斜杠)→ 请求/api/articles原样转发为backend的/api/articles------否则路径会被截断(第八节②) |
四、第2步:compose调整------只有80对外
compose.yaml 改动两处:
yaml
backend:
...
expose: ["8000"] # ① 不再对外映射,只有内部网络可达
# ports: ["8000:8000"] ← 删掉这行(生产环境)
frontend:
build: # ② 自动用新的生产Dockerfile
context: ./daily-front
dockerfile: ../frontend/Dockerfile
ports: ["80:80"] # ③ 唯一对外入口
depends_on: [backend]
🚨 生产环境只暴露80/443------后端端口对外不可见,从网络层就少一个被攻击的面("最小暴露")。
五、第3步:HTTPS------Let's Encrypt免费证书
前提 :域名解析(DNS A记录 → 服务器公网IP,dig daily.example.com查一下)。
📜 签发证书(三条命令)
bash
# 服务器上(先起好Nginx反代,80可访问)
apt install certbot python3-certbot-nginx
certbot --nginx -d daily.example.com -d www.daily.example.com # 自动改Nginx并签证书
certbot renew --dry-run # 验证自动续期
📌 Let's Encrypt三件事
| # | 说明 |
|---|---|
| ① | 证书90天有效 |
| ② | certbot自动续期(systemd timer) |
| ③ | 续期后自动reload Nginx------基本零维护 |
🎯 金句回扣
反代之后,前端和API同域名同端口(
https://daily.example.com与https://daily.example.com/api)------浏览器的"同源"判定满足,CORS配置从"必须"变成"不再需要"(本地开发5173→8000跨端口仍然要)。
💡 这就是架构改善一劳永逸的例子:搞明白原理,以后再见面就是老朋友。
🛡️ 进阶:TLS加固(可选,但推荐)
Certbot生成的配置已经够用,想更安全可以加几条:
nginx
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
server_tokens off;
六、第4步:上线前安全检查
text
□ SECRET_KEY换成强随机(openssl rand -hex 32,走secrets)
□ 上传目录图片可访问(/api/images/走反代)
□ 生产环境debug=False(默认已是)
□ PG数据定时备份(pg_dump cron,或先手动做一次)
□ 服务器防火墙只开22/80/443
七、验收清单
bash
1. 浏览器输入域名 → 首页打开(HTTPS锁形图标)
2. 刷新/favorites等前端路由 → 不404(SPA兜底生效)
3. 登录流程全通:API走https://同域名/api/
4. http://访问 → 自动跳https(练习③后)
5. 上传一张图片 → https下正常显示(第16篇的UGC上线)
6. 服务器上docker compose ps:frontend(80) / backend(内部) / db / redis 全Running
7. 全程无报错后提交Git
bash
git add .
git commit -m "生产部署:Nginx反代 + HTTPS + 多阶段构建"
八、常见报错:这6个,上线日的标配(重点!)
① 刷新前端路由404(首页正常)
🔍 原因 :try_files兜底没配------Nginx找/favorites文件找不到就404。
✅ 解法 :location /里加try_files $uri $uri/ /index.html;(第3节写法)。
② API全部502 Bad Gateway
🔍 原因 :proxy_pass目标不对------写成了localhost:8000(Nginx容器里没有backend服务)或服务名拼错。
✅ 解法 :容器网络内用服务名backend:8000;先docker compose ps确认backend在跑。
③ 上传的图片打不开(其他功能正常)
🔍 原因 :图片URL是/api/images/xxx------没走/api/的转发?或location顺序/路径配置有误。
✅ 解法 :图片URL本身带/api/前缀,会命中/api/ location转给后端(第16篇的FileResponse兜底);检查proxy_pass是否截断了路径。
④ certbot报"域名无法解析"
🔍 原因:DNS A记录没生效、IP填错、或还没等DNS传播。
✅ 解法 :dig daily.example.com确认返回服务器IP;改DNS后等几分钟再试。
⑤ HTTPS证书无效 / 浏览器报警
🔍 原因:证书域名不匹配(www和非www只签了一个)、续期失败。
✅ 解法 :certbot --nginx -d daily.example.com -d www.daily.example.com两个域名都签;certbot renew --dry-run验证续期。
⑥ Nginx起不来 / 容器内解析不到backend
🔍 原因:frontend容器和backend不在同一compose网络,或backend服务名写错。
✅ 解法 :compose默认同一网络(服务名互访);确认/api/的proxy_pass与compose.yaml里服务名逐字一致。
九、课后练习
| # | 练习 | 难度 | 提示 |
|---|---|---|---|
| 1 | 上传目录安全 :给Nginx加一条location /api/images/ { }禁执行策略 |
⭐⭐ | 第16篇的"存储目录不执行脚本"落地 |
| 2 | gzip/brotli压缩:静态资源开启压缩 | ⭐⭐ | Nginx一行配置,JS/CSS体积降60%+ |
| 3 | 80→443强制跳转 :certbot生成的配置里加return 301 https://$host$request_uri; |
⭐ | 自动配置一般已包含,检查确认即可 |
| 4(选做) | 自定义404页 :error_page 404 /index.html |
⭐⭐ | SPA项目里"错误页也是前端的事" |
📦 配套代码
生产Dockerfile、nginx.conf与compose调整已上传Git(python_daily/):【gitee仓库地址】