从零理解 Docker:为什么我的代码在你的电脑上跑不起来?

从零理解 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 nginxgit 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

这里有几个值得注意的点:

  1. CommonJS 规范require 而不是 import,这是 Node 早期最通用的模块写法,很多老项目至今还在用。
  2. 监听在 0.0.0.0 :这很关键。0.0.0.0 表示监听所有网卡,而不是只监听 localhost。这在容器/代理场景下尤其重要------后面 Nginx 要从另一个"网络环境"访问它,如果只绑在 127.0.0.1 上就访问不到了。
  3. 端口号 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 解决的,正是软件世界里"这堆货怎么从我的码头搬到你的码头"的问题:

  1. Image(镜像) = 打包好的"母版"(光盘)
  2. Container(容器) = 跑起来的实例(DVD 播放中)
  3. -p 端口映射 = 打通"本机"和"容器"之间的网络
  4. -v 卷挂载 = 打通"本机文件"和"容器文件"之间的共享
  5. Nginx 反向代理 = 让用户只认 80 端口,后端端口藏起来

搞懂这五点,你就迈过了 Docker 入门的门槛。剩下要做的,就是动手 docker pulldocker run 跑一遍了。


*希望这篇分享对你有帮助。

相关推荐
泡海椒1 小时前
轻量级 Java JSON 库推荐:jquick-json 序列化/反序列化 + 变量替换 + 字节码优化全解析
后端
触底反弹1 小时前
🔥 NestJS 从零到实战:一个 Todos CRUD 搞懂企业级后端框架的核心设计
后端·typescript·nestjs
吃饱了得干活1 小时前
从经典的三层架构到DDD:一次对“业务逻辑层”的解剖与重构
java·后端·架构
XuCoder1 小时前
面试官:什么是覆盖索引?
后端
林太白1 小时前
What did Trea work help me with in this optimization process
前端·后端
Asize1 小时前
3 种设计模式 + DI:Nest.js 后端第一课
后端·设计模式·nestjs
程序员cxuan2 小时前
一招教你在 Codex 中开启 1M 上下文
人工智能·后端·程序员
IT_陈寒2 小时前
Vite热更新失效?八成是这个配置在搞鬼
前端·人工智能·后端
qq_22589174662 小时前
基于Flask的空气质量监测与预测分析系统
开发语言·后端·python·信息可视化·django·flask