速读#
Docker 缓解的是“我机器上能跑”背后的环境不一致,本质上是把 venv 的隔离思路扩到应用、运行时和系统库。它不会把内核、CPU 架构、外部数据和网络一起冻住,所以可复现仍有边界;端口绑在哪里,也直接决定服务暴露到哪一层网络。
问题从哪来#
装服务装到后面,系统里 Python、Java、各种库混在一起。图省事把包都 pip install 到全局,两个项目依赖同一库的不同版本时直接打架——升级一个弄坏另一个;更坑的是坏的那个当时不报错,过几天重跑才发现。
根因一句话:全局环境谁都能往里扔,出事难追是谁破坏的。
Docker 把应用、语言运行时和系统库打进镜像,在兼容的容器运行时、CPU 架构和 Linux 内核上减少环境差异。它不能保证任何机器上结果都完全一样:浮动标签会更新,amd64 与 arm64 可能走不同镜像,挂载数据和外部服务也仍在镜像之外。它解决的是很大一部分环境问题,不是给部署盖一枚“绝对可复现”的章。
venv 的放大版#
| venv | Docker | |
|---|---|---|
| 隔离什么 | Python 依赖 | 应用、系统库、运行时与进程空间 |
| 治什么 | 给 A 项目装依赖,弄坏了 B 项目 | 我机器能跑、你机器不行 |
逻辑同一条:会在多台机器 / 多个项目间流动的东西,第一件事是隔离。
从 venv 到 Docker,粒度从 Python 依赖扩大到用户空间运行环境。venv 管不了 libpq、OpenSSL 等系统库,也管不了宿主发行版差异;Linux 容器把自己的用户空间放进镜像,但仍共享宿主内核。密码、域名和连接串这类运行时配置不该烘进镜像,而应在启动时注入。环境不会自己保持干净——不主动隔离,混乱是默认结局;把秘密也一并打包,则只是把另一种混乱封存起来。
镜像和容器:用类与对象理解#
| 概念 | 类比 | 说明 |
|---|---|---|
| 镜像 Image | 类 / 模板 | 由不可变层组成的打包结果,含应用、依赖与系统库 |
| 容器 Container | 对象 / 实例 | 镜像加一层可写层后运行出的隔离进程 |
一个镜像可以跑出多个容器,就像一个类 new 出多个对象。把熟悉的 OOP 概念迁过来,Docker 就不玄了——这也是「底层是通的」的一次落地。
顺着这个类比可以记住:镜像改了要重建并替换容器。在容器里手改文件,只改了这个容器的可写层,删除后就消失;应用与依赖变更应进入 Dockerfile 重新构建,业务数据则挂到卷或外部存储。类与对象只是入门记忆法,容器还有网络、挂载、namespace 和 cgroup,不能继续拿 OOP 类比推导安全边界。
容器不是轻量虚拟机#
我最初把 Docker 理解成「启动快一点的虚拟机」,这个心智模型会误导判断:
| 虚拟机 | 容器 | |
|---|---|---|
| 隔离层级 | 完整客机 OS + 虚拟硬件 | 进程级,共享宿主内核 |
| 启动 | 通常较慢 | 通常更快 |
| 开销 | 每台 VM 有独立客机内核与用户空间 | 共享宿主内核,额外开销通常更小 |
容器本质上是一组受 namespace、cgroup 等机制约束的进程,不是缩小的机器。 容器的主进程退出后,容器生命周期就结束。实践上我按“一个可独立部署和替换的职责一个容器”来拆,而不是机械要求只能存在一个 Unix 进程;应用派生 worker、使用轻量 init 回收僵尸进程都很正常,把 Nginx、数据库和业务应用绑成一个不可分割单元才是问题。
共享内核同时也是代价:容器隔离通常弱于虚拟机,内核漏洞、过大的 Linux capabilities、特权容器或挂载 Docker socket 都可能把影响扩大到宿主机。我的场景跑的是自己的服务,配合非 root 用户、最小权限和及时更新后可以接受;隔离互不信任的代码时,我会选择独立虚拟机或 gVisor、Kata 这类更强边界,而不是把普通 Docker 容器当沙箱。快和轻不是白来的,知道换走了什么才用得踏实。
端口绑哪,直接决定攻击面#
docker run -d -p 8080:80 --name web nginx-p 8080:80 把宿主机 8080 映射到容器 80。
容易忽略的一点:不写宿主地址时,Docker 默认把发布端口绑定到所有宿主地址;只要云防火墙、宿主路由和上游网络允许,它就可能从公网到达。后面部署时我改成 127.0.0.1:8080:80,只让宿主机上的 Nginx 访问容器端口,这不是语法偏好,而是在入口层收缩暴露面。
| 绑定方式 | 效果 |
|---|---|
-p 8080:80 | 发布到所有宿主地址,是否公网可达还取决于外层网络策略 |
-p 127.0.0.1:8080:80 | 仅 IPv4 回环,适合宿主机反代 |
更隐蔽的一层是 Docker 会按所用后端管理 iptables 或 nftables 规则,发布流量与 UFW 的处理顺序并不等同于普通宿主进程。很多默认组合下,UFW 的 deny incoming 不能按直觉拦住 Docker 已发布的端口;版本、后端和自定义规则又会改变结果。因此我不猜防火墙是否兜底,而是先绑定回环,再用 docker port web、sudo ss -ltnp 检查监听地址,并从另一台机器实际探测。需要对外发布时,再在云防火墙和 Docker 推荐的过滤链上显式收来源。
端口绑哪,直接决定攻击面。 命令会了只是起点;绑法选错,等于自己把门开大。
这篇留下的判断就两条:环境差异要主动收敛,别等依赖打架才处理;发布端口前先明确谁需要访问,有宿主机反代时优先绑定回环,再用实际网络探测验证暴露面。
