最终架构#
互联网
│
│ https://example.com/my-app/*
▼
服务器 Nginx
│
│ http://127.0.0.1:3001/my-app/*
▼
docs 容器 Nginx
├─ /my-app/*
│ └─ /usr/share/nginx/html/my-app/*
│
└─ /my-app/api/*
└─ http://web:3000/api/*
│
▼
web 容器服务器 Nginx 是整台机器的入口,前端容器 Nginx 是当前应用的入口。后端只存在于 Docker 内部网络。
第一步:确认项目配置#
前端运行镜像使用 Nginx:
FROM nginx:alpine AS runner
COPY nginx/frontend.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/apps/docs/dist /usr/share/nginx/html/my-appCompose 只发布前端容器:
services:
docs:
ports:
- "127.0.0.1:3001:80"
web:
# 不配置 ports绑定 127.0.0.1 表示只有服务器本机可以直接连接 3001。公网入口仍然是服务器 Nginx 的 80/443 端口。
前端和后端共享内部网络:
networks:
backend:
internal: trueinternal: true 用于限制该网络的外部访问能力,但不能代替应用鉴权、防火墙和密钥管理。
第二步:构建并启动 Compose#
安装并启动 Docker 后,在项目根目录执行:
docker compose up -d --build查看状态:
docker compose ps预期:
web先启动并通过健康检查;docs随后启动;- 两个服务最终都是
healthy; docs显示宿主机127.0.0.1:3001到容器 80 的映射;web没有宿主机端口映射。
当前电脑尚未安装 Docker,因此上述容器级结果属于待验证清单,不能只根据代码构建成功就假设已经通过。
第三步:验证前端容器 Nginx#
先检查配置语法:
docker compose exec docs nginx -t成功时应看到类似:
syntax is ok
test is successful查看 Nginx 实际加载的完整配置:
docker compose exec docs nginx -T直接从服务器本机验证前端页面:
curl -I http://127.0.0.1:3001/my-app/验证静态 HTML:
curl http://127.0.0.1:3001/my-app/验证 API 代理:
curl http://127.0.0.1:3001/my-app/api/message这一步绕过服务器 Nginx,可以单独证明前端容器的静态文件和内部 API 转发是否正常。
第四步:配置服务器 Nginx#
项目提供的核心配置:
server {
listen 80;
server_name example.com;
location = /my-app {
return 301 /my-app/;
}
location /my-app/ {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}需要将 example.com 改成真实域名,并把配置放进服务器 Nginx 实际加载的目录。不同 Linux 发行版可能使用:
/etc/nginx/conf.d/*.conf或:
/etc/nginx/sites-available/
/etc/nginx/sites-enabled/修改后先测试:
sudo nginx -t只有语法检查通过后再重新加载:
sudo systemctl reload nginx重新加载通常不会像停止再启动那样直接中断已有连接。
第五步:验证服务器入口#
确认 DNS 已经指向服务器 IP后访问:
curl -I http://example.com/my-app/
curl http://example.com/my-app/api/message也可以在 DNS 生效前临时指定请求目标和 Host:
curl -H "Host: example.com" http://服务器IP/my-app/如果服务器上存在多个 server,正确的 Host 对验证 server_name 匹配很重要。
页面请求的完整处理过程#
请求:
GET /my-app/处理:
1. DNS 将 example.com 解析到服务器 IP
2. 服务器 Nginx 根据 server_name 选择网站
3. location /my-app/ 匹配请求
4. 请求原样转发到 127.0.0.1:3001/my-app/
5. 前端容器 Nginx 匹配静态规则
6. try_files 找到 /usr/share/nginx/html/my-app/index.html
7. HTML 经两层 Nginx 返回浏览器
8. 浏览器继续请求 /my-app/assets/*API 请求的完整处理过程#
请求:
GET /my-app/api/message处理:
1. 服务器 Nginx 保留完整 URI
2. 前端容器收到 /my-app/api/message
3. location ^~ /my-app/api/ 接管请求
4. proxy_pass 将前缀替换为 /api/
5. Docker DNS 把 web 解析为后端容器地址
6. 后端收到 GET /api/message
7. 后端生成 JSON
8. JSON 经前端 Nginx、服务器 Nginx 返回浏览器按层排障,而不是一次猜完整链路#
遇到问题时从内向外逐层验证:
后端是否正常
↓
前端容器能否访问后端
↓
前端容器 Nginx 是否正常
↓
宿主机能否访问 127.0.0.1:3001
↓
服务器 Nginx 是否正确转发
↓
DNS、HTTPS 和浏览器是否正常这样可以快速确定错误发生在哪一层。
排查 404 Not Found#
404 表示某一层成功处理了请求,但没有找到对应资源或路由。
先记录原始请求:
/my-app/api/message再确认每一层 URI:
服务器 Nginx 上游 URI:/my-app/api/message
前端 Nginx 上游 URI:/api/message
Next.js 路由:/api/message常见原因:
- 服务器
proxy_pass多写了结尾/,提前删除/my-app; - Vite
base仍然是/; - 静态文件没有复制到
html/my-app; - API 请求没有匹配专用
location; - 后端真实路由与代理后的路径不一致;
- 请求落到了错误的
server_name。
排查 502 Bad Gateway#
502 通常说明 Nginx 选择了代理规则,但连接上游失败。
先看容器状态和日志:
docker compose ps
docker compose logs --tail 100 docs
docker compose logs --tail 100 web从前端容器直接请求后端:
docker compose exec docs wget -qO- http://web:3000/api/message检查名称解析:
docker compose exec docs ping -c 3 web如果前端容器访问后端正常,但服务器 Nginx 仍然 502,再检查服务器能否访问:
curl http://127.0.0.1:3001/my-app/排查静态资源 404#
浏览器开发者工具中观察失败的地址:
错误:/assets/index-xxxx.js
正确:/my-app/assets/index-xxxx.js检查构建结果:
pnpm --filter docs build打开 apps/docs/dist/index.html,确认资源 URL 以 /my-app/ 开头。
进入容器检查文件:
docker compose exec docs find /usr/share/nginx/html/my-app -maxdepth 2 -type f如果 URL 正确且文件存在,再检查服务器 Nginx 是否保留了 /my-app。
排查重定向循环#
当前前端容器将 / 重定向到 /my-app/:
location = / {
return 302 /my-app/;
}如果服务器 Nginx 把 /my-app/ 删除后转发为 /:
浏览器 /my-app/
→ 服务器删除 /my-app/
→ 前端容器收到 /
→ 前端重定向到 /my-app/
→ 再次被服务器删除这会形成循环。检查服务器代理是否错误写成:
proxy_pass http://127.0.0.1:3001/;当前设计应当是:
proxy_pass http://127.0.0.1:3001;排查配置修改没有生效#
服务器 Nginx:
sudo nginx -t
sudo nginx -T
sudo systemctl reload nginx容器 Nginx 配置已经构建进镜像,修改后需要重建并重新创建容器:
docker compose up -d --build --force-recreate docs如果只重启旧容器,镜像中的旧配置不会自动更新。
常用观察命令#
# Compose 状态
docker compose ps
# 两个服务的日志
docker compose logs --tail 100 docs
docker compose logs --tail 100 web
# 容器 Nginx 语法与完整配置
docker compose exec docs nginx -t
docker compose exec docs nginx -T
# 宿主机直连前端容器
curl -I http://127.0.0.1:3001/my-app/
# 通过前端 Nginx 访问 API
curl http://127.0.0.1:3001/my-app/api/message
# 绕过前端 Nginx,验证容器间通信
docker compose exec docs wget -qO- http://web:3000/api/message当前方案的边界#
这套配置适合学习和单机部署思路,但不是完整生产模板:
- 服务器示例只有 HTTP,没有实际 TLS 证书配置;
- 没有缓存策略和静态资源长期缓存头;
- 没有限流、认证和安全响应头;
- 没有多实例负载均衡;
- 没有集中日志、指标和告警;
- 没有自动发布、回滚和镜像版本管理。
先理解本次请求链路,再逐项扩展,比一开始复制一份复杂生产配置更容易判断每条指令的作用。
本篇速记#
服务器 Nginx:
域名、HTTPS、/my-app 项目入口
前端容器 Nginx:
静态文件、SPA 回退、API 分流
后端容器:
只在 Docker backend 网络监听 web:3000
排障顺序:
后端 → 容器网络 → 前端 Nginx → 宿主机端口 → 服务器 Nginx → DNS/HTTPS
404:通常是路径或资源匹配问题
502:通常是上游连接问题自测问题#
- 为什么
docs绑定127.0.0.1:3001而不是直接绑定所有公网地址? - 如何绕过服务器 Nginx,单独验证前端容器?
- 如何绕过前端 Nginx,单独验证后端容器?
- 静态资源 404 时应该检查哪三类路径?
- 为什么修改容器 Nginx 配置后只执行
docker compose restart可能没有效果?
