前言
作为一名做了10年前端的开发者,我一直渴望突破"纯前端"的边界。这次,我决定用 Docker + Nginx + Node.js 搭建一个完整的全栈项目:前端静态页面通过 Nginx 托管,后端 API 通过 Nginx 反向代理,三个后端容器做负载均衡。本以为半天搞定,结果在 502 和 404 的泥潭里挣扎了两天......这篇文章记录了从"一脸懵"到"curl 200 OK"的全过程,希望能帮到同样在转型路上的你。
技术栈
- 前端:纯 HTML/CSS/JS(单页应用)
- 后端:Node.js + Express(返回当前时间)
- 网关:Nginx(反向代理 + 负载均衡)
- 容器化:Docker(自定义网络、端口映射、卷挂载)
项目结构
perl
纯文本
my-nginx-config/
├── default.conf # Nginx 反向代理配置
├── html/
│ └── index.html # 前端静态页面
└── my-backend/
├── dockerfile # Node.js 后端镜像构建文件
├── package.json # Node.js 项目描述
└── server.js # Express 后端服务(返回当前时间)
第一步:搭建后端服务
创建 server.js:
javascript
javascript
const express = require('express');
const app = express();
const port = 3001;
// ✅ 添加 CORS 支持
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type');
next();
});
app.get('/time', (req, res) => {
const now = new Date();
res.send(now.toLocaleString());
});
app.listen(port, () => {
console.log(`Backend listening at http://localhost:${port}`);
});
构建并启动后端容器(这里假设你已经有 Node.js 镜像,或者用 node:18-alpine):
perl
bash
# 构建镜像
docker build -t my-backend ./my-backend
# 启动三个后端实例(分别映射不同宿主机端口)
docker run -d --name backend1 --network fullstack-net -p 3001:3001 my-backend
docker run -d --name backend2 --network fullstack-net -p 3002:3001 my-backend
docker run -d --name backend3 --network fullstack-net -p 3003:3001 my-backend
注意:这里后端容器内部监听 3001 端口,宿主机映射到 3001/3002/3003。
第二步:编写 Nginx 配置文件
创建 default.conf:
bash
nginx
# 定义后端服务器组(负载均衡)
upstream backend_servers {
server backend1:3001;
server backend2:3001;
server backend3:3001;
}
server {
listen 80;
server_name localhost;
# 静态文件目录
root /usr/share/nginx/html;
index index.html;
# 前端 SPA 路由兼容(防止刷新 404)
location / {
try_files $uri $uri/ /index.html;
}
# API 反向代理
location /api {
# 显式去掉 /api 前缀,避免双斜杠问题
rewrite ^/api(/.*)$ $1 break;
proxy_pass http://backend_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
关键点 :
rewrite指令比proxy_pass末尾斜杠更可靠,它明确地将/api/time转换为/time,避免了我在实践中遇到的"双斜杠"bug。
第三步:启动 Nginx 容器
javascript
bash
docker run -d --name my-app-router \
--network fullstack-net \
-p 80:80 \
-v 文件路径/my-nginx-config/default.conf:/etc/nginx/conf.d/default.conf \
-v 文件路径/my-nginx-config/html:/usr/share/nginx/html \
nginx:alpine
第四步:前端页面
创建 index.html:
xml
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Fullstack Demo</title>
<style>
body { font-family: Arial; text-align: center; margin-top: 100px; }
h1 { color: #333; }
#time { font-size: 24px; color: #007bff; margin-top: 20px; }
</style>
</head>
<body>
<h1>Hello Fullstack!</h1>
<div id="time">Loading...</div>
<script>
fetch('/api/time')
.then(res => res.text())
.then(data => document.getElementById('time').innerText = data)
.catch(err => document.getElementById('time').innerText = 'Error: ' + err);
</script>
</body>
</html>
踩坑实录(精华部分)
坑1:容器启动后立即退出(Exited 1)
现象 :docker ps -a 显示 my-app-router 状态为 Exited (1)。
原因 :Nginx 配置文件语法错误,比如 upstream 里写了不存在的容器名或端口。
解决 :用 docker logs my-app-router 查看错误日志,修正配置文件后重新运行容器。
坑2:502 Bad Gateway
现象 :浏览器访问 localhost 显示 502,后端容器正常运行。
原因:Nginx 无法连接到后端容器。常见原因:
- 端口不匹配(后端监听 3001,但 Nginx 配置写成了 80)
- 容器不在同一网络
排查方法:
perl
bash
# 1. 进入 Nginx 容器
docker exec -it my-app-router sh
# 2. 测试网络连通性
ping backend1
# 3. 测试后端服务
curl http://backend1:3001/time
坑3:404 Not Found(双斜杠陷阱)
现象 :Nginx 日志显示请求转发到了 //time,后端返回 404。
原因 :proxy_pass http://backend1:3001/; 末尾斜杠在某些情况下会产生双斜杠。如default.conf这样配置

解决 :改用 rewrite 显式处理路径,如上文所示。
坑4:前端跨域(CORS)
现象 :直接访问 localhost:3001/time 正常,但通过 Nginx 代理后报跨域错误。
原因 :前端代码直接请求了 http://localhost:3001/time,而不是通过 /api/time。
解决 :修改前端 fetch 地址为相对路径 /api/time,让 Nginx 代理转发。
最终验证
- 确保所有容器正常运行:
docker ps - 在浏览器访问
http://localhost - 看到 "Hello Fullstack!" 和当前时间,控制台无报错

收获与总结
全栈调试三板斧
- 后端自测 :
curl http://localhost:3001/time(确认后端活着) - 容器互测 :
docker exec -it my-app-router sh→curl http://backend1:3001/time(确认网络通) - 模拟请求 :
curl http://localhost/api/time(确认 Nginx 转发逻辑)

关键认知
- Docker 网络让容器之间通过名字通信,宿主机通过端口映射访问。
- Nginx 的
proxy_pass末尾斜杠行为容易产生歧义,推荐用rewrite显式控制路径。 - 日志是最好的老师:
docker logs和nginx -t能解决大部分问题。
下一步计划
- 接入 MySQL/MongoDB,实现数据持久化
- 用 Docker Compose 一键编排所有服务
- 部署到云服务器,让全世界都能访问
完整代码仓库
GitHub:github.com/qishuixian/...