不知道你有没有遇到过这种场景:接手一个三年前的老项目,文档上写着"请使用 Node 16 + npm 8 启动",而你本机装的是 Node 22。跑起来报错,降级又怕影响手头其他项目。这时候你心里大概率会冒出一句: "我电脑能跑,你电脑怎么就跑不起来?"
这不是玄学,这是环境依赖在作祟。今天的文章就用一个最小实战,带你用 Docker 和 Nginx 反向代理,把"能跑"这件事从个人电脑上拆下来,变成一份可以分发的配置。
一、Docker 到底解决了什么问题?
先抛开那些厚重的概念,记住一个类比:
Docker = 应用 + 运行环境
就像海运的集装箱,你把货物(应用)和必要的保护措施(运行环境)打包进一个标准箱子,然后这艘"万吨巨轮"可以把这个箱子运到任何港口。无论目的港是深圳、鹿特丹还是洛杉矶,开箱即用。
放到开发里也一样。一个完整的 Web 应用往往不止有代码,还有:
- Node.js 的版本
- npm/yarn 的版本
- Redis、MySQL 等中间件
- 各种系统级依赖
任何一个环节不一致,都可能导致"你电脑能跑,我电脑不能跑"。Docker 做的就是把这些依赖连同代码一起,打成一个镜像 ,然后在你任何设备上以容器的形式运行起来。
如果你关注 AI Agent,会发现一个有趣的类比:
Agent = LLM + Harness(tool + mcp + rag + skill...)
Docker = 应用 + 运行环境
本质上,它们都是在解决"能力 + 环境"的组合问题。
两个必须理解的概念:Image 和 Container
- Image(镜像) :类似于一张光盘。它包含了应用程序和它需要的环境,是只读的模板。
- Container(容器) :类似于光盘放进 DVD 播放器后正在播放的内容。它是镜像运行起来的实例,可读可写。
所以你会发现,Docker 的日常操作,基本就是围绕这两个东西展开:
bash
docker pull nginx # 拉取镜像,相当于下载光盘
docker run nginx # 运行镜像,相当于把光盘放进播放器
docker stop xxx # 停止容器
docker rm xxx # 删除容器
docker rmi nginx # 删除镜像
先记住这两组概念,后面实战中你会反复看到它们。
二、实战:用 Nginx 反向代理一个 Node 服务
假设我们现在有一个最简单的 Node 服务:
javascript
// index.js
const http = require('http')
const server = http.createServer((req, res) => {
res.end('hello world')
})
server.listen(1314, '0.0.0.0', () => {
console.log('node service run on 1314')
})
这个服务监听在 1314 端口,注意这里监听的是 0.0.0.0,而不是 127.0.0.1。为什么要这样?后面你会看到,因为 Nginx 容器需要通过宿主机访问这个服务,如果只监听回环地址,容器可能无法访问。
现在,你在本机启动它:
node index.js
浏览器访问 http://localhost:1314,会看到熟悉的 hello world。
但问题来了:用户会输入 localhost:1314 吗?不会。用户只会输入 http://localhost,甚至是一个域名。而 HTTP 协议默认端口是 80。也就是说,用户永远只会从 80 端口进来,但你的服务跑在 1314 端口。
这就需要一位"前台接待员"------Nginx。
为什么需要 Nginx?
因为在实际生产环境中,你需要一个统一入口来处理请求:
- 用户访问 80 端口
- Nginx 监听 80 端口
- Nginx 根据配置把请求转发到真正运行服务的 1314 端口
这就是反向代理 。说白了就是:用户不知道你的服务在哪个端口,也不该知道,Nginx 帮你挡在前面。
准备 Nginx 配置文件
ini
# nginx.conf
events {}
http {
server {
listen 80;
location / {
proxy_pass http://host.docker.internal:1314;
proxy_set_header Host $host;
}
}
}
这里有一个关键点:host.docker.internal。这是 Docker Desktop 提供的一个特殊 DNS 名称,它指向宿主机。因为我们的 Node 服务直接跑在宿主机上,而 Nginx 跑在容器里,所以 Nginx 需要通过这个地址访问宿主机的 1314 端口。
用 Docker 启动 Nginx
bash
docker run --name my-nginx-demo \
-p 80:80 \
-v /path/to/nginx.conf:/etc/nginx/nginx.conf \
-d nginx
这条命令拆开来看,就是 Docker 的日常操作核心:
--name my-nginx-demo:给容器起个名字,方便后续管理。-p 80:80:端口映射,本机的 80 端口映射到容器的 80 端口。-v /path/to/nginx.conf:/etc/nginx/nginx.conf:把本机的配置文件挂载到容器内 Nginx 的默认配置路径。-d nginx:后台运行nginx镜像。如果不设置,会阻塞运行,需要重开一个进程。
现在,浏览器访问 http://localhost,请求会先到本机 80 端口,然后通过 Docker 端口映射进入 Nginx 容器,再根据配置文件代理到宿主机的 1314 端口,最终返回 hello world。
三、请求链路拆解:一次请求的完整旅程
这个例子虽然简单,但把 Docker 和 Nginx 反向代理的核心链路全串起来了。你可以把下面这条链路在脑子里过一遍:
csharp
浏览器 http://localhost:80
↓
宿主机 80 端口
↓
Docker 端口映射 -p 80:80
↓
Nginx 容器内 80 端口
↓
nginx.conf 反向代理规则
↓
host.docker.internal:1314(宿主机 Node 服务)
↓
返回 hello world
你会发现,整个过程中,用户完全不知道服务真正跑在 1314 端口。对于用户来说,只有一个入口:http://localhost。
这就是反向代理和正向代理的区别:
- 正向代理:代理的是客户端,比如你挂了个代理去访问外网,服务器不知道你是谁。
- 反向代理:代理的是服务端,比如 Nginx 挡在服务前面,用户不知道真正的服务在哪。
反向代理的意义不只是端口转发,它把"服务真正在哪里"这件事藏了起来,对外只暴露一个干净的入口。
四、踩坑点与解决方案
在实际操作中,有几个点需要特别注意,不然很容易卡住。
1. host.docker.internal 的兼容性问题
host.docker.internal 是 Docker Desktop(Windows/Mac)自带的特性。如果你在 Linux 服务器上运行 Docker,这个 DNS 名称可能不存在。
解决方案是添加 --add-host 参数:
lua
docker run --name my-nginx-demo \
-p 80:80 \
-v /path/to/nginx.conf:/etc/nginx/nginx.conf \
--add-host=host.docker.internal:host-gateway \
-d nginx
host-gateway 会让 Docker 自动解析到宿主机 IP,这样 Nginx 就能访问宿主机的 Node 服务了。
2. 端口映射的顺序
-p 80:80 里面,第一个 80 是宿主机端口,第二个 80 是容器端口。这个顺序别搞反了。
如果你本机 80 端口已经被占用(比如本机已经有 Nginx 在跑),可以改成 -p 8080:80,然后访问 http://localhost:8080。
3. 挂载路径的格式
在 Windows 上,挂载路径可能需要写成 E:\workspace...,但更推荐使用绝对路径或者 Docker 的 volume。上面的示例中我用了 /path/to/nginx.conf,你需要替换成自己本机的实际路径。
4. 容器清理
如果折腾了一堆容器,想快速清理环境,可以用以下命令:
makefile
# 停止所有运行中的容器
docker stop $(docker ps -q)
# 删除所有容器
docker rm $(docker ps -aq)
# 删除 nginx 镜像
docker rmi nginx
注意:docker rm 只能删除已停止的容器,如果容器还在运行,需要先执行 docker stop,或者直接用 docker rm -f 强制删除。
五、扩展:用 Docker 跑一个 MySQL
Docker 的便利性在数据库这种强环境依赖的场景下体现得更加明显。比如你本机已经装了一个 MySQL 5.7,但项目要求 MySQL 8.0,你不可能卸载重装。这时候 Docker 就派上用场了。
ini
# 拉取 MySQL 8.0 镜像
docker pull mysql:8.0
# 运行 MySQL 容器,映射到宿主机 3307 端口
docker run -d --name mysql-demo \
-p 3307:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:8.0
这里有两个点需要注意:
-p 3307:3306:MySQL 默认监听 3306 端口,但如果本机已有 MySQL 占用了 3306,就映射到 3307,避免冲突。-e MYSQL_ROOT_PASSWORD=123456:通过环境变量设置 MySQL 的 root 密码,-e就是--env的缩写。
进入容器内部,可以像操作普通 Linux 一样操作:
bash
docker exec -it mysql-demo /bin/bash
mysql -uroot -p123456
docker exec -it 是进入一个正在运行的容器并打开交互式终端,之后你就可以在容器内部执行命令了。
- -i --interactive:保持输入流开启,能接收键盘输入
- -t --tty:分配虚拟终端,显示命令行提示符
- -it 几乎永远成对使用,用来做交互式操作
六、总结:Docker 的核心就三件事
回顾一下这篇文章的内容,你会发现 Docker 的核心其实就是三件事:
- 打包:把应用和运行环境打包成镜像。
- 分发:镜像可以轻松 pull/push,任何设备都能拿到同一份环境。
- 隔离:容器之间互不影响,不会因为版本冲突而崩溃。
而 Nginx 反向代理,则是把"用户入口"和"服务端口"解耦,让你可以灵活地在后面挂任何服务。
环境问题不再是玄学,容器化让你把"能跑"这件事固化下来,变成一份可移植的配置。
下次再遇到"我电脑能跑,你电脑跑不起来"的情况,别急着怀疑人生,先问问自己:这件事,是不是该交给 Docker 了?