从零理解 Docker:为什么我的代码在你的电脑上跑不起来?
基于一个最小 Node + Nginx 反向代理的 Demo,把 Docker 的来龙去脉讲清楚。
一、先回答那个灵魂问题
"我的电脑能跑,你的电脑怎么跑?"
这大概是每个开发者都遇到过的尴尬。一份代码在你机器上运行得好好的,到了同事电脑、测试服务器、生产环境,就莫名其妙地报错。原因往往不是代码本身,而是代码之外的那一堆运行环境:
- Node 版本不对(项目要 Node 16,你装的是 Node 22)
- npm 版本不对(要 npm 8,你的是 npm 10)
- Redis 没装、MySQL 版本不一致
- 某些系统库缺失......
举例子:你接手了一个 N 年前的 Vue2 项目,要求 Node 16 + npm 8,可你电脑装的是 Node 22,就是跑不起来。
Docker 做的事,就是把这些"代码 + 依赖 + 环境"打包成一个整体,让你在任何设备上都能原样跑起来。
ini
Agent = LLM + Harness(tool + mcp + rag + skill + ...)
Docker = 应用 + 运行环境
Agent 把大模型和各种工具打包成智能体,Docker 把应用和运行环境打包成容器------同样的思路,隔离 + 打包 + 一次封装到处跑。
二、两个最核心的概念:Image 和 Container
读 Docker 文档,最先绕不过去的两个词:
| 概念 | 类比 | 说明 |
|---|---|---|
| Image(镜像) | 光盘 | 应用 + 环境,一个只读的、隔离好的"母版" |
| Container(容器) | DVD / 正在播放的光盘 | 由镜像"运行起来"之后的可执行实例 |
类比到 Git 就好理解了:
docker pull nginx≈git pull------ 把镜像(母版)拉下来docker run nginx------ 让这个母版真正跑起来,变成一个活的容器
一个镜像可以同时跑出很多个容器,就像一张光盘可以刻很多份、到处播放。
三、一个最小的 Web 应用
先看 index.js,一个最朴素的 Node HTTP 服务:
javascript
// node 早期的 commonjs 规范
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 端口,任何请求进来都回一句 hello world。
这里有几个值得注意的点:
- CommonJS 规范 :
require而不是import,这是 Node 早期最通用的模块写法,很多老项目至今还在用。 - 监听在
0.0.0.0:这很关键。0.0.0.0表示监听所有网卡,而不是只监听localhost。这在容器/代理场景下尤其重要------后面 Nginx 要从另一个"网络环境"访问它,如果只绑在127.0.0.1上就访问不到了。 - 端口号 1314:这是个"内部"端口,用户不会直接知道,后面会被 Nginx 隐藏起来。
四、为什么需要 Nginx:反向代理
现在有了一个跑在 1314 端口的服务。但用户访问一个网站,习惯上只输入一个域名或者 localhost,没人愿意记 :1314 这种端口号。
这里就要引入一点运维知识:
服务器软件把所有 80 端口 产生的请求,代理给 3000 端口(这里我们的例子是 1314)。
80 是 HTTP 的默认端口------你在浏览器里输入 www.juejin.cn,没写端口,其实默认走的就是 :80。
Nginx 就是一个高并发、擅长做代理的服务器。它:
- 监听 80 端口的访问
- 通过配置文件 把请求转发到真正干活的
1314端口
这就叫 反向代理(Reverse Proxy) 。
正向代理 vs 反向代理(考点)
rust
正向代理:用户的 intent -> browser(chrome)(正向代理 http)-> 目标服务器
代理的是"用户",服务器不知道真实用户是谁
反向代理:用户 -> local:80 -> ... -> 真实服务(1314)
代理的是"服务器",用户不知道后端真正的端口/地址
一句话区分:正向代理代理客户端,反向代理代理服务端 。Nginx 在这里做的就是反向代理------用户只知道 localhost:80,根本不知道后端具体跑在 1314 端口上。
五、把 Nginx 放进 Docker:一条命令看懂所有参数
看 nginx.conf:
ini
# nginx.conf
events {}
http {
server {
listen 80;
location / {
proxy_pass http://host.docker.internal:1314;
proxy_set_header Host $host;
}
}
}
含义逐行拆解:
listen 80------ Nginx 在容器内监听 80 端口location /------ 匹配所有路径的请求proxy_pass http://host.docker.internal:1314------ 把请求转发到宿主机(也就是你本机)的 1314 端口proxy_set_header Host $host------ 转发时保留原始的 Host 头信息
host.docker.internal是 Docker 提供的一个特殊域名,专门用来让容器访问宿主机。因为我们的 Node 服务直接跑在宿主机上(而不是容器里),所以 Nginx 容器要用这个域名才能找到它。
接下来就是那条"集大成"的启动命令,readme 里逐段拆解了:
css
docker run --name my-night-demo -p 80:80 \
-v D:\Desktop\workspace\lj-ai\backend\docker\demo\nginx.conf:/etc/nginx/nginx.conf \
-d nginx
| 参数 | 含义 |
|---|---|
docker run |
启动一个镜像,让它成为可运行的容器 |
--name my-night-demo |
给容器起名字 |
-p 80:80 |
端口映射:本机 80 端口 ↔ 容器 80 端口 |
-v 本机路径:容器路径 |
挂载 :把本机的 nginx.conf 映射到容器里的 /etc/nginx/nginx.conf |
-d |
后台运行(daemon) |
nginx |
要启动的镜像名 |
整个请求的完整链路
csharp
用户浏览器
│ http://localhost:80(默认端口,用户无感知)
▼
本机 80 端口 ── docker -p 80:80 ──► 容器 80 端口
│
│ -v 映射进来的 nginx.conf
▼
Nginx(80) 读取配置
│ proxy_pass 反向代理
▼
host.docker.internal:1314
│
▼
Node 服务(1314)
返回 hello world
这就是一条完整的"用户 → Nginx 反向代理 → 后端服务"链路。用户全程只知道 localhost,后端真实端口被 Nginx 完美隐藏。
六、容器起来之后:进容器里看看(以 MySQL 为例)
Docker 的精髓不仅是"跑起来",还能"钻进去"操作。readme 里用 MySQL 做了演示:
ini
# 拉取 8.0 版本的镜像
docker pull mysql:8.0
# 后台运行,端口 3307 映射到容器 3306,并设置 root 密码
docker run -d --name mysql-demo -p 3307:3306 \
-e MYSQL_ROOT_PASSWORD=123456 mysql:8.0
注意两个新东西:
-e MYSQL_ROOT_PASSWORD=123456:通过环境变量传递配置,这是容器化应用非常常见的传参方式3307:3306:本机用 3307,是因为你本机可能已经装了一个 MySQL 占用了 3306 ,用-p错开端口,就实现了"本机 MySQL 和 Docker MySQL 互不干扰、和平共处"。
然后"进容器":
bash
# 进入容器的 Linux 终端
docker exec -it mysql-demo /bin/bash
# 在容器内登录 MySQL
mysql -uroot -p123456
docker exec -it 让你像 SSH 进一台 Linux 服务器一样,进到容器的 shell 里操作------这正好印证了开头那句话:容器就是一个隔离的、完整的运行环境。
七、常用命令速查(运维考点)
makefile
# 拉取任何想要的镜像
docker pull <镜像名>
# 运行任何镜像
docker run <参数> <镜像名>
# 停止所有容器
docker stop $(docker ps -q)
# 删除所有容器
docker rm $(docker ps -aq)
# 删除某个镜像
docker rmi nginx
小技巧:$(docker ps -q) 这种写法,是把"列出所有容器 ID"的结果当作参数传给 stop/rm,一键批量操作。
八、总结:一句话记住 Docker
Docker 是海运的万吨巨轮,容器是标准集装箱。
有了集装箱,货物(应用)不管装的是汽车、家具还是蔬菜,都能用同一套标准装船、运输、卸载------不用再关心"这艘船适不适合运这种货"。
Docker 解决的,正是软件世界里"这堆货怎么从我的码头搬到你的码头"的问题:
- Image(镜像) = 打包好的"母版"(光盘)
- Container(容器) = 跑起来的实例(DVD 播放中)
-p端口映射 = 打通"本机"和"容器"之间的网络-v卷挂载 = 打通"本机文件"和"容器文件"之间的共享- Nginx 反向代理 = 让用户只认 80 端口,后端端口藏起来
搞懂这五点,你就迈过了 Docker 入门的门槛。剩下要做的,就是动手 docker pull、docker run 跑一遍了。
*希望这篇分享对你有帮助。