速读#
Docker 治的是“我机器上能跑”背后的环境不一致,本质上是把 venv 的隔离思路放大到整个运行环境。会在多台机器、多个项目间流动的东西,第一件事都是隔离;端口绑哪里,也直接决定攻击面。
问题从哪来#
装服务装到后面,系统里 Python、Java、各种库混在一起。图省事把包都 pip install 到全局,两个项目依赖同一库的不同版本时直接打架——升级一个弄坏另一个;更坑的是坏的那个当时不报错,过几天重跑才发现。
根因一句话:全局环境谁都能往里扔,出事难追是谁破坏的。
Docker 把应用和它需要的整个环境打成镜像,换机器跑结果一样。它解决的是环境问题——而那摊烂账,往往是自己图省事堆出来的。
venv 的放大版#
| venv | Docker | |
|---|---|---|
| 隔离什么 | Python 依赖 | 整个运行环境(系统库、运行时、配置) |
| 治什么 | 给 A 项目装依赖,弄坏了 B 项目 | 我机器能跑、你机器不行 |
逻辑同一条:会在多台机器 / 多个项目间流动的东西,第一件事是隔离。 从 venv → Docker,只是粒度从「依赖」放大到「整个运行时」。venv 只管 Python 包,管不了系统库版本、管不了「你机器是 Debian 我机器是 CentOS」;Docker 把这些一起冻进镜像。环境和进程一样,不会自己保持干净——不主动隔离,混乱是默认结局,区别只是早晚。
镜像和容器:用类与对象理解#
| 概念 | 类比 | 说明 |
|---|---|---|
| 镜像 Image | 类 / 模板 | 只读打包结果,含应用 + 依赖 + 系统库 |
| 容器 Container | 对象 / 实例 | 镜像跑起来的实例,可启停销毁 |
一个镜像可以跑出多个容器,就像一个类 new 出多个对象。把熟悉的 OOP 概念迁过来,Docker 就不玄了——这也是「底层是通的」的一次落地。
顺着这个类比还能多推一步:类改了要重新实例化,镜像改了也要重建容器。在容器里改文件,就像给某个对象临时 patch 属性——容器一删就没了。要留下来的改动,进 Dockerfile 重新构建,或者挂载出去。
容器不是轻量虚拟机#
我最初把 Docker 理解成「启动快一点的虚拟机」,这个心智模型会误导判断:
| 虚拟机 | 容器 | |
|---|---|---|
| 隔离层级 | 完整客机 OS + 虚拟硬件 | 进程级,共享宿主内核 |
| 启动 | 分钟级 | 秒级 |
| 开销 | 每台 VM 一份 OS 内存 | 几乎只有进程本身 |
容器本质是被隔离的进程,不是缩小的机器。 想通这一点,很多行为就解释得通:为什么容器里主进程退出容器就停(进程没了,「机器」自然没了);为什么不该在一个容器里塞一堆服务(一个进程一个容器,才谈得上独立启停和替换)。
共享内核同时也是代价:容器的隔离边界天然比虚拟机弱,一个内核漏洞理论上波及宿主机上所有容器。我的场景里跑的都是自己的服务,这个弱边界可以接受;要隔离互不信任的负载,虚拟机仍是更硬的答案。快和轻不是白来的,知道换走了什么,用起来才踏实。
端口绑哪,直接决定攻击面#
docker run -d -p 8080:80 --name web nginx-p 8080:80 把宿主机 8080 映射到容器 80。
容易忽略的一点:默认 -p 绑的是 0.0.0.0,对外暴露。
后面部署时我改成 127.0.0.1:8080:8080 只绑回环,外网进不来,一律由 Nginx 转发——这是防护面的事。
| 绑定方式 | 效果 |
|---|---|
-p 8080:80 | 绑 0.0.0.0,公网直接可达 |
-p 127.0.0.1:8080:80 | 仅本机,适合反代后置 |
更隐蔽的一层:Docker 发布端口时直接写 iptables 规则,ufw 里的「默认拒绝」并不覆盖它。以为防火墙会兜底、实际端口已经在公网裸奔,是这里最经典的误判。所以我不把安全压在防火墙上,直接从绑定层面收住——回环之外根本不存在这个端口。
端口绑哪,直接决定攻击面。 命令会了只是起点;绑法选错,等于自己把门开大。
这篇留下的判断就两条:环境要主动隔离,别等依赖打架才动手;端口默认全网可达,有了反代之后绑回环就该是常规动作,不是可选项。
