外观
项目 3:使用 Docker 部署 2048 小游戏与静态网站
约 4011 字大约 13 分钟
DockerNginx端口映射NAT
2026-05-27
项目目标
公司周年庆需要快速上线一个 2048 小游戏活动页面。你的任务是用 Docker + Nginx 在 4 个学时内完成部署,要求浏览器可访问、页面资源可替换(热更新),并能说清楚"浏览器到底是怎么访问到容器内部那个 80 端口的"。
本任务将通过 三个层层递进的实验,带你深入理解 Docker 网络穿透(NAT 端口映射)、数据持久化(Bind Mount 与 Volume)和跨容器数据共享的核心原理。
一、核心底层理论支撑
1.1 Network Namespace —— 容器为什么是一座"网络孤岛"
第一课我们学过,Docker 依赖 Linux Namespace 实现隔离。其中 Network Namespace 是最关键的一种——每个容器启动时,内核都会为它创建一个完全独立的网络栈,包括:
- 独立的网卡(虚拟接口
eth0) - 独立的路由表
- 独立的
iptables规则 - 独立的 IP 地址(默认从
172.17.0.0/16网段分配)
当你在容器里执行 curl localhost:80 时,访问的是容器内部的 80 端口。但宿主机的浏览器运行在另一个完全不同的 Network Namespace 中,它根本不知道 172.17.0.2 是什么东西——这就是"孤岛"的本质。
Docker 如何让流量跨越 Namespace 边界? 答案是 veth pair(虚拟以太网对)+ docker0 网桥。内核创建一对虚拟网线,一头插在容器的 eth0 上,另一头插在宿主机的 docker0 网桥上。容器通过这条虚拟网线可以访问外网,但外网仍然无法主动找到容器——因为它用的是私有 IP。
1.2 NAT 端口映射 —— -p 参数背后的 iptables 魔法
为了让外界能主动连接到容器内部的服务,Docker 借助了 Linux 内核的 iptables 防火墙 来实现 DNAT(目标网络地址转换)。
当你执行 docker run -p 8080:80 nginx 时,Docker 在后台默默地做了这件事:
iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80翻译成人话就是:"只要有人在宿主机的 8080 端口敲 TCP 的门,就把这个请求的目标地址和端口改写成 172.17.0.2:80,然后扔进 Docker 网桥。"
动画说明
下方动画以实验二中 2048 容器的 8088 端口为例进行演示,-p 8088:80 和 -p 8080:80 的底层 NAT 机制完全相同——只是宿主机端口号不同。
👇 点击下方动画中的按钮,亲自追踪一个 HTTP 数据包如何穿越宿主机防火墙,最终抵达容器内部:
🌐 端口映射 (Port Mapping) 原理演示
直观感受外部请求如何通过宿主机的 NAT 防火墙规则“变魔术般”穿透进入隔离的容器内部
🌍 外部网络 / 浏览器
💻
🖥️ 宿主机 (Host OS) [192.168.1.100]
端口 :8088
🔥 iptables (DNAT 规则)
🌉 docker0 虚拟网桥网关: 172.17.0.1
📦 容器隔离网络
容器 IP: 172.17.0.2
监听端口 :80
🟢 Nginx Web Server
三个关键结论:
| 概念 | 含义 | 示例 |
|---|---|---|
| 宿主机端口(Host Port) | 外界浏览器访问的端口 | 8080 |
| 容器端口(Container Port) | 容器内部服务实际监听的端口 | 80 |
-p 8080:80 | -p 宿主机端口:容器端口 | 把外界的 8080 转发到容器内的 80 |
验证端口映射:容器启动后,用 docker port <容器名> 可以查看当前生效的映射规则。
1.3 容器的"阅后即焚"与数据持久化之道
第 2 课我们学过,容器的文件系统由"只读镜像层 + 可读写层"组成。容器运行期间的任何文件修改都保存在最顶部的可读写层中——一旦容器被 docker rm 删除,这个读写层就永远消失了。
这就是容器著名的"无状态(Stateless)"特性:容器本身不保存任何业务数据。如果我们在容器里辛辛苦苦写好了 HTML 代码,哪天容器崩溃了或者需要升级镜像版本,代码就全丢了。
为了打破这个限制,Docker 提供了两种主流的数据持久化方案:
📁 数据持久化:Bind Mount vs Docker Volume
对比不同挂载模式下,容器销毁对数据生命周期的影响
【启动容器】直接启动 MySQL 容器,不进行任何挂载。
🖥️ 宿主机存储区
📂 用户自定义目录
/Users/bob/data空
🔒 Docker 托管区
/var/lib/docker/volumes/📦 MySQL 容器
读写层 (可丢弃)
空
| 维度 | Bind Mount(绑定挂载) | Docker Volume(数据卷) |
|---|---|---|
| 数据位置 | 宿主机上的任意绝对路径 | Docker 托管目录(/var/lib/docker/volumes/) |
| 管理方式 | 用户自己管理 | Docker 守护进程全权管理 |
| 典型场景 | 前端开发热更新、配置文件注入 | 数据库持久化、跨容器数据共享 |
| 权限控制 | 继承宿主机文件权限 | Docker 施加额外的安全隔离 |
| 创建方式 | -v /host/path:/container/path | docker volume create vol-name + -v vol-name:/container/path |
本课的核心实践逻辑:实验一验证网络穿透 → 实验二用 Bind Mount 实现热更新 → 实验三用 Volume 实现跨容器数据共享。
二、项目全景图
环境说明
本课程所有操作均在 Windows Docker Desktop 中完成(第 1 课已安装配置好)。命令在 WSL 终端中执行,浏览器在 Windows 中访问,Docker Desktop 会自动将容器端口映射到 localhost。
🔬 进阶观察:下文涉及的
iptables、docker0网桥、/var/lib/docker/volumes/等概念属于 Linux 内核/引擎层面的实现细节,在 Windows Docker Desktop 中由内置的轻量虚拟机承载。你不需要修改它们,只需通过docker命令间接观察即可。
2.1 我们要构建什么
2.2 项目目录结构
project03-2048-docker/
├── 2048/ # 2048 源码(从 GitHub 拉取)
│ ├── index.html
│ ├── css/
│ ├── js/
│ └── images/
└── screenshots/ # 存放浏览器验证截图
├── nginx-default.png
├── 2048-game.png
└── 2048-hotupdate.png三、实验一:Nginx 默认页面部署与端口映射验证
现在进入第一个动手实验。目标:启动一个 Nginx 容器,让宿主机的浏览器能访问到它。
3.1 创建项目目录
mkdir -p project03-2048-docker/screenshots
cd project03-2048-docker3.2 启动 Nginx 并发布端口
Nginx 镜像内置的 Web 服务默认监听 80 端口(容器内部)。我们通过 -p 8080:80 把这个端口"发布"到宿主机上。
在下方输入这款全球最流行的 Web 服务器名称,获取精确的启动命令:
问题:本任务使用的官方 Web 服务器镜像名称是什么?(全小写,5 个字母)
3.3 验证端口映射
容器启动后,关注 docker ps 输出的 PORTS 列——你应该能看到 0.0.0.0:8080->80/tcp。这意味着 iptables DNAT 规则已经生效。
用 docker port 确认映射关系:
docker port nginx-demo
# 输出:80/tcp -> 0.0.0.0:8080现在打开浏览器,访问 http://localhost:8080。
如果你看到硕大的 "Welcome to nginx!" 页面,恭喜——你的第一个 NAT 穿透实验成功了!
📸 截图 1:截取浏览器中显示的 Nginx 欢迎页,保存为
screenshots/nginx-default.png。
四、实验二:Bind Mount 部署 2048 游戏与热更新
实验一证明了网络能通,但页面是 Nginx 默认的。现在我们要把 2048 游戏部署上去,并且要求"改了宿主机上的 HTML 文件,浏览器刷新就能看到变化"。
4.1 获取 2048 静态资源
2048 小游戏的源码托管在 GitHub 上。优先用 wget 直接拉取:
wget -O 2048.zip https://github.com/gabrielecirulli/2048/archive/refs/heads/master.zip
unzip 2048.zip
mv 2048-master 2048
rm 2048.zip如果网络不通,也可以使用教师提供的 2048 资源包解压到当前目录。
确认目录结构如下:
project03-2048-docker/
├── 2048/
│ ├── index.html
│ ├── css/
│ ├── js/
│ └── images/
└── screenshots/# 确认关键文件存在
test -f 2048/index.html && echo "✅ 资源包就绪" || echo "❌ 请确认 2048/ 目录存在且包含 index.html"4.2 使用 Bind Mount 部署 2048
Bind Mount 的核心思路:把宿主机的 2048/ 目录"映射"到容器内 Nginx 读取网页的位置(/usr/share/nginx/html)。这样 Nginx 读到的文件实际上是宿主机上的文件。
在下方输入代表这种挂载方式的英文单词(提示:它的名字就是"绑定"的意思),获取完整的部署命令:
问题:把宿主机目录直接绑定挂载到容器目录的方式常称为什么 mount?(全小写,4 个字母)
4.3 验证 2048 页面和挂载信息
浏览器访问:
http://localhost:8088你应该能立刻玩到 2048 小游戏。接下来用 docker inspect 查看挂载的底层细节:
docker inspect game2048 --format '{{ json .Mounts }}' | python3 -m json.tool关注输出中的 "Source"(宿主机路径)和 "Destination"(容器内路径),它们应该精准对应你的 Bind Mount 参数。
📸 截图 2:截取浏览器中 2048 游戏页面,保存为
screenshots/2048-game.png。
4.4 体验热更新
用 Windows 上的文本编辑器(VS Code 或记事本)打开 2048/index.html。找到 <title> 标签:
<title>2048</title>将其修改为:
<title>公司周年庆专属 2048 活动</title>保存文件。不需要重启容器,不需要任何额外操作。 直接刷新浏览器里的 2048 页面,你会发现标签页的标题已经变了。
这就是 Bind Mount 赋予的极速开发体验:宿主机上改文件,容器里立刻生效。
📸 截图 3:截取修改标题后的浏览器页面(注意标签栏上的新标题),保存为
screenshots/2048-hotupdate.png。
五、实验三:Docker Volume 跨容器数据共享
Bind Mount 很适合本地开发,但在生产环境中,Docker Volume 才是更安全、更规范的选择。Docker Volume 由 Docker 守护进程全权管理,支持跨容器共享。
5.1 创建命名 Volume
docker volume create game-data
docker volume ls --filter name=game-data5.2 将同一个 Volume 挂载到两个容器
在下方输入这种由 Docker 完全托管的数据存储区域名称,获取完整的跨容器数据共享命令:
问题:由 Docker 完全托管的数据卷,英文名叫什么?(全小写,6 个字母)
5.3 验证跨容器数据共享
执行完命令后,你应该在终端里看到 Hello from A 的输出。这说明尽管两个容器在网络和进程上是绝对隔离的,但通过同一个 Volume,它们在底层文件系统上实现了数据互通。
用 docker volume inspect 可以查看 Volume 在宿主机上的实际存储位置:
docker volume inspect game-data
# 关注 "Mountpoint" 字段5.4 Bind Mount vs Volume 适用场景总结
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 本地前端开发(需要热更新) | Bind Mount | 宿主机改文件,容器立刻生效 |
| 配置文件注入(如 nginx.conf) | Bind Mount | 直接映射单个配置文件 |
| 数据库持久化(MySQL、PostgreSQL) | Docker Volume | 权限安全,Docker 托管,支持跨容器 |
| 微服务共享临时数据 | Docker Volume | 多个容器同时读写,互不干扰 |
| 日志收集 | Bind Mount 或 Volume | 两者皆可,Volume 更规范 |
六、提交物清单
将以下文件打包压缩后提交:
| 文件 | 说明 | 验证方法 |
|---|---|---|
screenshots/nginx-default.png | 实验一:Nginx 欢迎页截图 | 含地址栏,显示 localhost:8080 |
screenshots/2048-game.png | 实验二:2048 游戏页面截图 | 含地址栏,显示 localhost:8088 |
screenshots/2048-hotupdate.png | 实验二:修改标题后的截图 | 标签栏显示新标题 |
docker ps 输出截图 | 显示两个容器正在运行 | 含 nginx-demo 和 game2048 |
docker inspect game2048 Mounts 输出 | 证明 Bind Mount 挂载关系 | Source 和 Destination 字段正确 |
| Volume 共享验证截图 | 终端中容器 B 读出 Hello from A | 证明跨容器数据共享成功 |
随堂提问
实验二中,你修改宿主机上的 index.html 后刷新浏览器就能看到变化。请回答:
- 宿主机上的
2048/目录被挂载到了容器内的哪个路径? - 实验三用的是 Docker Volume 而不是 Bind Mount。如果用 Volume 来部署 2048,你还能在宿主机上找到 2048 的文件并直接编辑吗?为什么?
七、故障排查工具箱
| 现象 | 根因 | 排查方法 |
|---|---|---|
浏览器访问 localhost:8080 无响应 | 端口映射缺失或容器未运行 | docker ps 确认容器状态,检查 -p 参数 |
页面显示 403 Forbidden | 挂载目录缺少 index.html | ls -la 2048/index.html 确认文件存在 |
| 页面仍显示 Nginx 默认欢迎页 | Bind Mount 路径错误或未生效 | docker inspect game2048 检查 Mounts → Source |
port is already allocated | 宿主机端口已被占用 | docker ps -a 检查旧容器,docker rm -f 清理 |
修改 index.html 后页面没变化 | 浏览器缓存或修改了错误的文件 | 按 Ctrl+Shift+R 强制刷新;确认你编辑的是 2048/index.html |
docker: permission denied | 当前用户不在 docker 组 | 在 WSL 中执行 sudo usermod -aG docker $USER 后重新登录 |
八、进阶验收标准
| 验收维度 | 达标要求 |
|---|---|
| 网络穿透 | 能用自己的话说清楚 -p 8080:80 背后的 iptables DNAT 过程;能区分宿主机端口与容器端口 |
| 存储挂载 | 能画出 Bind Mount 和 Docker Volume 的目录归属图;能说出各自的两个典型适用场景 |
| Web 部署 | 能独立完成 docker run -p -v 的组合命令,将任意静态网页部署到 Nginx 容器中 |
| 热更新验证 | 能演示修改 index.html → 浏览器刷新 → 页面变化的全过程,并能用 docker inspect 证明挂载关系 |
| 跨容器共享 | 能独立创建命名 Volume 并挂载到两个容器,验证写入-读取的数据共享效果 |
九、深度探索任务
以下任务不强制提交,但完成后会让你真正理解本课的设计思想:
探索任务 1:用 iptables 验证 DNAT 规则
⚠️ 适用环境:本任务仅适用于 Linux Docker Engine 原生安装,或你能够进入 Docker Desktop 内部 VM 的场景。在 Windows Docker Desktop + WSL2 的默认配置下,
iptables规则位于 Docker Desktop 的轻量虚拟机内部,WSL 终端通常无法直接查看——看不到输出是正常的,不代表实验失败。
如果你所处的环境支持,在启动 nginx-demo 容器后执行:
sudo iptables -t nat -L DOCKER -n | grep 8080观察输出,找到那条由 Docker 自动创建的 DNAT 规则。
主线验证方式(适用于所有环境):实验一中你已经用了 docker port、docker ps、浏览器访问 http://localhost:8080——这三条命令已经完整验证了 NAT 端口映射是否生效,不必强求 iptables 输出。
❓ 提问:
docker port nginx-demo的输出是什么?它和-p 8080:80中的 8080、80 分别是什么关系?如果你能看到 iptables 输出,容器 IP 和规则中的目标地址一致吗?(如果看不到 iptables,只需回答前半部分即可)
探索任务 2:对比容器内外的文件修改
在 game2048 容器中执行:
docker exec game2048 sh -c "echo '<h1>容器内修改</h1>' > /usr/share/nginx/html/test.html"然后检查宿主机上的 2048/ 目录——你会发现 test.html 也出现在那里。反过来,在宿主机上新建文件,容器里也会立刻出现。这个双向同步证明 Bind Mount 本质上就是同一个目录的两个入口。
❓ 提问:在宿主机
2048/目录中执行touch hello-world.txt,然后执行docker exec game2048 ls /usr/share/nginx/html/hello-world.txt。容器内能看到这个文件吗?如果删掉容器(docker rm -f game2048),宿主机上的hello-world.txt还在吗?
探索任务 3:模拟 Volume 数据丢失场景
故意删除 container-a 和 container-b(docker rm -f),然后检查 Volume 是否仍然存在(docker volume ls)。再启动一个新容器挂载同一个 Volume,确认数据仍然可以读出。
这证明了 Docker Volume 的生命周期独立于任何单个容器——这正是生产环境中数据库持久化依赖 Volume 的根本原因。
❓ 提问:执行
docker volume rm game-data删除卷,然后重新执行docker volume create game-data创建同名卷。挂载到新容器后,还能读到之前容器 A 写入的Hello from A吗?为什么?
