Docker + Nginx + Node.js:从"我的电脑能跑,你的电脑跑不了"到一键部署
读完这篇,你会彻底搞懂容器隔离、端口映射、反向代理这三个概念,并能亲手搭建一个可用的 Web 服务栈。
一、一个古老而扎心的故事
每个程序员都遇到过这样的场景:
"在我的电脑上明明跑得好好的,怎么到你电脑上就崩了?"
原因往往不是代码写错了,而是运行环境不一致------你装的是 Node 22,项目要求 Node 16;你用的 Redis 6,生产环境是 Redis 5;甚至连操作系统底层库都不同。
传统解决办法是:写一份长长的部署文档,让运维手动安装各种依赖,版本对不上就出 Bug。这种"人肉运维"既低效又容易出错。
Docker 就是来解决这个问题的。
Docker 是一个应用容器化工具 ,它的口号很直白:"Build, Ship, Run" (构建、运送、运行)。
就像海运集装箱一样,不管里面装的是电子产品还是水果,一旦装入标准集装箱,就能被任何一艘巨轮运走。
类比一下:
- Docker = 应用 + 运行环境(打包成一个整体)
- 集装箱 = 你的代码 + 它需要的所有系统库、运行时、依赖
只要这个"集装箱"被打包好,就可以在任何安装了 Docker 的机器上直接运行,无需再关心环境差异。
二、隔离是什么?为什么它这么重要?
Docker 的核心能力是隔离。但这到底隔离了什么?
我们用一个真实的例子说明:
你刚入职一家公司,接手一个五年前用 Vue 2 写的项目,它依赖 Node 16 和 npm 8。
可你电脑上装的是 Node 22------直接运行
npm install就报错,因为某些老旧的依赖包在 Node 22 下无法编译。
传统的解决方法:用 nvm 切换 Node 版本,或者干脆装一个虚拟机。但这些方法要么侵入性太强,要么太重。
Docker 的隔离 ,并不是虚拟出一整台电脑,而是在操作系统层面为你的应用划定一个"独立小房间"。这个房间里有:
- 自己独立的文件系统(包含 Node 16、npm 8、项目代码)
- 自己独立的进程空间(看不到宿主机上的其他进程)
- 自己独立的网络栈(有自己的 IP 和端口,与宿主机隔离)
- 受限制的资源配额(CPU、内存上限)
这样,你的 Vue 2 项目就可以安安稳稳地住在这个"房间"里,哪怕宿主机上装了 Node 22,也完全不会冲突。
三、Docker 两大基本概念:Image 和 Container
-
Image(镜像) ------ 相当于一张光盘 。它是一个只读的模板,里面包含了应用程序 + 运行环境的所有文件。你可以把镜像推送到 Docker Hub 或私有仓库,像
git pull一样拉取。 -
Container(容器) ------ 相当于把这张光盘放入 DVD 驱动器后运行起来的实例。容器是可写的、可运行的,你可以启动、停止、删除它。
类比:
Image 是类(Class),Container 是对象(Instance)。
一个 Image 可以同时运行出多个 Container,彼此完全隔离。
四、我们要搭建什么?
现在我们准备搭建一个最简单的 Web 服务栈:
- 一个 Node.js 应用 ,监听
1314端口,返回"hello world"。 - 一个 Nginx 反向代理 ,监听
80端口,把所有请求转发给 Node 应用的1314端口。
用户只需访问 http://localhost(即默认的 80 端口),就能看到 hello world。
这样做的目的是:
- 统一入口:用户不需要知道后端具体跑在哪个端口,只需访问标准的 80 端口。
- 方便扩展:以后如果后端换成多个实例,Nginx 可以轻松做负载均衡。
- 安全加固:Nginx 可以处理 SSL 终止、限流、过滤恶意请求等。
五、编写一个极简的 Node.js HTTP 服务器
我们用 Node.js 内置的 http 模块创建一个服务器,监听 1314 端口,并在所有请求上返回 hello world。
javascript
// index.js ------ 使用 Node.js 原生 http 模块(CommonJS 风格)
const http = require('http');
// 创建 HTTP 服务器,收到任何请求都返回 "hello world"
const server = http.createServer((req, res) => {
res.end("hello world");
});
// 监听 1314 端口,并绑定到 0.0.0.0(所有网络接口)
server.listen(1314, '0.0.0.0', () => {
console.log(`Node server running on port 1314`);
});
关键点解析:
0.0.0.0表示监听所有网络接口,这样容器内外 都能访问到这个端口。如果只写localhost或127.0.0.1,则只有容器内部能访问,外部(包括 Nginx 容器)无法连通。- 这个文件很简单,但足以代表任意一个后端服务(Java、Python、Go 同理)。
六、配置 Nginx 作为反向代理
Nginx 的配置文件 nginx.conf 如下:
nginx
# nginx.conf ------ 极简反向代理配置
events {} # 事件模型配置,这里留空使用默认值
http {
server {
listen 80; # 监听容器内的 80 端口
location / {
# 将请求代理到后端地址
proxy_pass http://host.docker.internal:1314;
# 传递原始 Host 头,避免后端收到 localhost 而困惑
proxy_set_header Host $host;
}
}
}
逐行解读(底层原理向):
events {}:这是 Nginx 处理并发连接的核心配置块。留空时使用默认的epoll(Linux)或kqueue(BSD)模型,足够应对大多数场景。listen 80;:让 Nginx 监听容器内部的 80 端口。注意:这个端口是容器内的端口,与宿主机无关。location /:匹配所有请求路径。proxy_pass:这是反向代理的核心指令。它告诉 Nginx:"把收到的 HTTP 请求转发给这个地址" 。- 这里的
http://host.docker.internal:1314是一个特殊的域名,仅在 Docker for Windows / Mac 中有效,它指向宿主机(即你的笔记本电脑)。因为我们的 Node.js 应用如果运行在宿主机上(而不是容器里),Nginx 容器需要能访问到宿主机的 1314 端口,就需要这个特殊 DNS。 - 如果 Node 也运行在另一个容器中,这里应该写成
http://node-service-name:1314(使用 Docker Compose 的服务名)。
- 这里的
proxy_set_header Host $host;:默认情况下,Nginx 转发请求时会把Host头改成localhost,这可能导致后端应用(尤其是根据域名做路由的)无法正确识别。$host是 Nginx 内置变量,代表用户请求中的原始 Host 值,这样后端就能拿到真实的域名。
为什么不直接把 proxy_pass 写成
http://127.0.0.1:1314?因为 Nginx 运行在容器里,容器内的
127.0.0.1指向容器自身 ,而不是宿主机。所以必须用host.docker.internal来指代宿主机 IP。
七、用 Docker 启动 Nginx 容器
现在我们要把 Nginx 跑起来,命令如下(来自你的笔记):
bash
docker run \
--name my-nginx-demo \ # 给容器起个名字
-p 80:80 \ # 宿主机 80 映射到容器 80
-v C:\Users\Dai\OneDrive\桌面\workspace\djz-ai\backend\docker\demo\nginx.conf:/etc/nginx/nginx.conf \
-d nginx # 后台运行官方 nginx 镜像
参数深度解析:
-p 80:80:端口映射。格式是宿主机端口:容器端口。- 用户访问
http://localhost:80时,流量会被 Docker 转发到容器的 80 端口。 - 如果容器内的服务监听的是 1314,但你想让用户通过 8080 访问,就可以写
-p 8080:80,然后访问localhost:8080。
- 用户访问
-v ...:挂载(Volume) ,把宿主机上的nginx.conf文件"覆盖"到容器内的/etc/nginx/nginx.conf。这样我们无需重新构建镜像,只需修改本地配置文件,重启容器就能生效。-d nginx:-d表示后台运行(detach),这样终端不会被阻塞。nginx是官方镜像的名字,如果本地没有会自动从 Docker Hub 拉取。
八、整体架构与请求流程
整个系统由两个部分组成(假设 Node 运行在宿主机):
用户只需知道 http://localhost,后续的路由、转发全部由 Nginx 处理。后端端口变化时,只需修改 Nginx 配置,用户无感知。
九、进阶思考:如果 Node 也放进容器呢?
在实际生产环境中,Node 也会被打包成镜像运行在容器里。此时 nginx.conf 里的 proxy_pass 就不能再写 host.docker.internal,而应该改成:
nginx
proxy_pass http://node-app:1314;
并且使用 Docker Compose 将 Nginx 和 Node 放在同一个自定义网络中,让它们通过服务名互相访问。
一个简单的 docker-compose.yml 示例:
yaml
version: '3'
services:
node:
build: . # 使用当前目录的 Dockerfile 构建 Node 镜像
ports:
- "1314:1314" # 可选,如果只想容器间通信,可以不用暴露到宿主机
nginx:
image: nginx
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- node # 确保 node 先启动
此时 Nginx 容器内可以通过 node 这个主机名访问到 Node 容器。
十、隔离的本质到底是什么?
通过这次实践,我们可以更加具体地理解 Docker 所谓的"隔离":
- 文件系统隔离 :Nginx 容器里有自己的
/etc/nginx,Node 容器有自己的/usr/local/lib/node_modules,彼此互不干扰。 - 网络隔离:每个容器有自己的虚拟网卡和 IP,端口空间独立------你可以在两个容器里都监听 80 端口而不冲突。
- 进程隔离:容器内的进程 PID 从 1 开始,看不到宿主机或其他容器的进程,增强了安全性。
- 资源限制 (未演示,但可用
--memory、--cpus设置):避免某个容器耗尽宿主机资源。
十一、总结
我们从"环境不一致"这个痛点出发,一步步完成了:
- 理解 Docker 的"集装箱"思想。
- 编写一个简单的 Node.js 服务。
- 配置 Nginx 作为反向代理。
- 使用
docker run启动容器并挂载配置。 - 剖析端口映射、
host.docker.internal等底层细节。
你现在应该能回答以下问题了:
- 为什么我的 Vue2 项目需要 Node 16,而 Docker 能轻松搞定?
- Nginx 的
proxy_pass和proxy_set_header分别做了什么? -p 80:80中的两个 80 分别代表什么?- 为什么容器内不能用
127.0.0.1访问宿主机服务?
如果觉得有帮助,不妨自己动手敲一遍命令,体会一下"一次构建,到处运行"的爽快感。也欢迎在评论区留下你的疑问或踩坑经历,我们一起交流!