树莓派 Docker 化网页服务器(1):当系统镜像遇上服务增长 —— 为什么我们需要 Docker
前言
树莓派4B自建网页服务器专栏里,我们从系统烧录,一路走到nginx\ddns-go\acme\frp逐个安装配置,完成了web服务器的搭建,迁移或容灾恢复方面我们也提供了镜像制作方案,整体是一个较为完善的闭环。作为基础网页服务环境,它够用了,即使损坏也能一键恢复,这很好。
但如果我想要在上面放更多的服务呢,比如数据库,java运行环境,对象存储等等,每安装配置完一个,为了能一键恢复,便需要连同系统和配置好的应用打包一个镜像包,而如果我只需要web+java运行环境,那就得回到web镜像,跳过数据库,安装配置Java再打系统镜像包,否则镜像包就是臃肿的、含额外服务的,环境一致性也就不成立了。
上面的情景反映出两个致命的问题:
- 随着服务数量的提升,系统镜像打包会失去灵活性,某几个服务的组合形成的组合爆炸,会极大的抬高系统镜像所需的打包次数,否则就要接受冗余的服务夹带其中。
- 单个服务链路中的每个组件应用,都要经历下载安装配置的过程,这个过程需要打包成一步到位的方式。
为了解决“服务生长”所要面临的这两个痛点,我决定引入Docker,基于旧专栏已完成的树莓派网页服务器底座,进行一次彻底的容器化改造。
本篇导读
有了上面两个痛点的铺垫,“为什么要做容器化改造”这件事应该已经很清楚了。但先不急着动手 —— 在正式开始之前,有三件事需要在这一篇里先讲明白:Docker到底是什么、它和我们之前用的系统镜像有什么本质区别、以及整个改造计划的全貌是什么样的。
工欲善其事,必先利其器。这一篇我们没有任何实操,只做一件事:把认知框架搭好。后面的专栏篇章,每一篇都是对这个框架的一次具体填充。
如果你已经熟悉Docker的基本概念,可以直接跳到“框架预览”小节,按需选择对应篇章阅读。
什么是Docker
先从最直观的感受说起。
在旧专栏里,我们在一台树莓派上把某个服务跑起来,过程是这样的:
搜索软件安装教程 → 添加软件源(如果官方源里没有)
→ 执行 `apt install` 或下载二进制文件
→ 修改配置文件
→ 设置开机自启(写 systemd service 或塞进 `/etc/rc.local`)
→ 启动服务并验证每一步都依赖当下那台机器的状态。同样的六步,在另一台树莓派上重走一遍,可能因为系统版本差一个小号、libc 版本不对、或者某个依赖被别的软件占用了,就卡在中间。
Docker 解决的就是这个问题。
一个类比
想象你要搬家。旧专栏给了你两种选择:
第一种:把家具拆成零件,搬到新家后再重新组装。如果新家的门框窄了两厘米,某件家具就装不进去了,你得现场锯掉一截。灵活,但慢,而且每搬一次都要重新调试。
第二种:连房子带家具外带地基,一整块切割运走。到了新地点直接落位,什么都不用重新配置。快,但死板——目标地块的承重规格必须符合下限,而且房子里所有的东西,哪怕你不需要的,也得一并带走。
第一种对应旧专栏里“逐个安装配置”的手工流程,第二种对应旧专栏第3篇教的系统镜像方案。
而Docker的做法是第三种:把每件家具分别装进标准集装箱,贴上标签。搬到新家后,你需要哪件就开哪件,不需要的不开。家具之间互不干扰,换掉一件也不影响其他的。
这个“集装箱”在 Docker 的世界里叫做容器。而制作集装箱的模具,叫做镜像。
两个核心概念
镜像(Image):一个只读的模板,包含了运行某个应用所需的一切——代码、运行时环境、系统库、配置文件。你可以把它理解成 ISO 文件,但它比 ISO 轻量得多,而且可以分层构建。
容器(Container):镜像的一个可运行实例。你可以对容器进行启动、停止、删除等操作,也可以在同一台机器上同时运行多个容器,它们互相隔离。
两者的关系可以这样记:
镜像是菜谱,容器是按菜谱做出来的那道菜。菜谱可以反复使用,每次做出来的菜都是一样的味道。
一个关键特性:环境一致性
这是 Docker 最核心的价值,也是我们引入它的根本原因。
回头看旧专栏里那四个组件,它们在裸机上的“落点”全都绑死了宿主系统:
- Nginx 的
sites-enabled配置、日志路径、worker 进程用户,都按 Debian 的目录约定走;换到 Alpine 或 Ubuntu,路径和权限逻辑都不一样 - ddns-go 是二进制下发,它的
config.yaml默认落在$HOME下,开机自启要靠你手写一条 systemd service,service 里的User=和WorkingDirectory=错一处就起不来 - acme.sh 和 frpc 也有类似的路径绑定问题,不再赘述。
也就是说,旧专栏那套“能跑”的状态,是这四个组件和这一台树莓派的系统环境共同协商出来的结果。机器、系统版本一换,协商结果作废。
而在 Docker 里,上面四个组件各自被打成一个镜像:Nginx 用官方 arm64v8/nginx,ddns-go 用 jeessy/ddns-go,acme.sh 跑在 neilpang/acme.sh 容器里,frpc 用 snowdreamtech/frpc。每个容器内部该在哪读配置、该用什么用户、该把续签任务交给容器内的 cron 还是宿主机触发,都由镜像作者按标准定死。不管底层系统是 Raspberry Pi OS 还是 Ubuntu Server,不管宿主机是树莓派 4B 还是 x86 虚拟机,容器里跑出来的 Nginx 版本、ddns-go 的监听行为、acme.sh 的续签逻辑,都和你第一次构建时一致。
你在 2026 年 8 月构建的镜像,三年后在另一台机器上运行,里面的 Nginx 版本、ddns-go 的配置文件结构、acme.sh 的续签脚本,和你构建的那一刻完全一样。
Docker 和 系统镜像 的对比
在旧专栏第3篇里,我们花了不少精力打磨系统镜像方案:缩容、清理、用7-zip打包成.xz,最终把一个5GB左右的运行系统压缩到600MB以内。烧录时无需解压,写卡即用,恢复速度极快。
这套方案至今仍然是我眼中“灾难恢复”的最佳手段——卡坏了,刷回去,十分钟满血复活。
但它有一个天生的局限:颗粒度太粗。
系统镜像的局限
系统镜像的本质是对整张TF卡(或SSD)做一次快照。它打包的是“操作系统 + 所有已安装的软件 + 所有配置 + 所有数据”。这个特性决定了:
- 加一个服务,就得重做一次镜像。 旧专栏做完
Nginx + ddns-go + acme + frp,打包了一个镜像。如果后来想加一个MySQL,做完后又得重新缩容、重新打包。新的镜像比旧的多几百MB,因为MySQL的数据和日志也被塞了进去。 - 服务的组合越多,镜像的组合爆炸越严重。 假设你有三个服务A、B、C。有人需要A+B,有人需要B+C,有人需要A+C,还有人只需要A。要为每种组合分别打包镜像,维护成本直线上升。
- 镜像之间没有共享层。 两个镜像都包含相同的
Nginx配置,但各自打包、各自存储,没有复用。
系统镜像的优势在于它的整体性——要么全有,要么全无。当你只需要“一台和之前一模一样的树莓派”时,它无可替代。但当你的需求变成“在这台树莓派上灵活组合不同的服务”时,它就力不从心了。
Docker 的对应优势
Docker 解决的是上面提到的三个问题:
- 加一个服务,不需要重做任何镜像。 你只需要写一段Docker配置(或一条
docker run命令),拉取对应服务的镜像,启动即可。Nginx、ddns-go、acme、frp各自独立运行,互不干扰。 - 服务的组合由你按需编排。 想要A+B?写一个
docker-compose.yml只包含A和B。想要A+C?改一下配置文件,重启即可。不需要为每种组合单独打包。 - 镜像层可以复用。 多个镜像如果基于相同的基础层(比如都基于
debian:bookworm-slim),底层只需要存储一份,节省磁盘空间。
一个表格看清取舍
| 对比维度 | 系统镜像(旧专栏第3篇) | Docker容器 |
|---|---|---|
| 恢复/部署速度 | 极快(烧录即用) | 快(拉取镜像+启动) |
| 单服务升级 | 需重做整个镜像 | 单独拉取新版本镜像 |
| 服务组合灵活性 | 低(组合爆炸) | 高(按需编排) |
| 存储利用率 | 低(镜像间无共享) | 高(分层复用) |
| 学习成本 | 低(按步骤操作即可) | 中(需理解容器概念) |
| 跨架构迁移 | 不支持(ARM only) | 支持(需镜像有多架构版本) |
| 离线部署友好度 | 极高(一个文件搞定) | 一般(需提前拉取镜像) |
不是替代,是互补
到这里你可能已经看出来了:系统镜像和Docker不是二选一的关系。它们是不同层级的工具,解决不同粒度的问题。
系统镜像解决的是“这台机器坏了,我要原样恢复”的问题。它是你最后的防线。
Docker 解决的是“我想在这台机器上灵活地增删改服务”的问题。它是你日常操作的工具。
所以在接下来的改造中,我不会放弃系统镜像。相反,我会把系统镜像作为基础设施层,Docker作为应用层,两层叠加使用:
先用系统镜像固化一个“带Docker底座的纯净系统”,烧录到任意树莓派上,开机即有一个完整的Docker运行环境。
然后通过Docker拉取和编排各个服务,实现“系统镜像管底座,Docker管服务”的分层架构。
树莓派上的 Docker
Docker 最初是为 x86 架构设计的,但现在对 ARM 架构的支持已经非常成熟。树莓派 4B 是 64 位 ARM 处理器(aarch64),可以原生运行 Docker 容器,前提是拉取带 linux/arm64 架构的镜像。主流组件—— Nginx、ddns-go、acme.sh、frpc ——官方或维护者都已提供 ARM64 版本,这也是我们旧专栏选它们的原因:它们天生能进容器。
需要注意:如果误拉了 x86 镜像,容器启动会报
exec format error,这不是配置错了,是架构不匹配。后面的专栏篇章我们会专门讲这个坑。
Docker化 改造的框架预览
在正式动手之前,先把整趟改造的路线图画出来。下面是本次专栏将要覆盖的全部组件,以及每一篇要处理的内容:
| 篇章 | 组件 | 改造目标 | 旧专栏对应内容 | 状态 |
|---|---|---|---|---|
| 第2篇 | 系统镜像 | 制作一个预装Docker引擎的纯净系统镜像 | 第3篇(系统镜像制作) | ✅ |
| 第3篇 | Nginx | 从apt安装改为Docker容器运行,配置挂载 | 第4篇(Nginx静态页) | ⌛ |
| 第4篇 | ddns-go | 从二进制+systemd改为Docker容器运行 | 第5篇(DDNS动态解析) | ⌛ |
| 第5篇 | acme.sh | 从git clone+cron改为Docker容器运行 | 第6篇(SSL证书) | ⌛ |
| 第6篇 | frpc | 从二进制+systemd改为Docker容器运行 | 第7篇(FRP隧道) | ⌛ |
| 第7篇 | Compose全家桶 | 整合上述所有容器,形成一键部署方案 | 无(新内容) | ⌛ |
注:状态指示 ⌛ = 待写,✅ = 已发布,🔄 = 修订中
这个表格会在后续写作过程中随时调整。已完成的两个专栏的经验告诉我:计划赶不上实践,动手之后一定会发现新的坑和更好的做法。 所以这张表只是一个起点,不是终点。
总结
这一篇我们没有装任何软件,也没有改任何配置,只做了一件事:把认知框架搭好。
我们从旧专栏的两个痛点出发——镜像打包的组合爆炸,以及单服务链路无法一步到位——引出了Docker这个工具。然后解释了Docker的核心概念(镜像和容器)以及它最重要的特性(环境一致性)。接着把系统镜像和Docker做了一次诚实的对比,明确了它们各自的适用场景和互补关系。最后给出了本次专栏的改造路线图,让你对后续六篇的内容有一个整体的预期。
现在你应该已经清楚:
- 为什么要做容器化改造:为了解决服务增长带来的灵活性问题
- Docker能带来什么:环境一致性、按需编排、分层复用
- 它不能取代什么:系统镜像仍然是灾难恢复的最佳手段,两者是互补关系
- 接下来怎么走:从制作带Docker底座的系统镜像开始,逐个容器化旧专栏的四个组件,最后用Compose整合成一键部署方案
下一篇,我们将真正动手——制作一个预装Docker引擎的树莓派系统镜像。这是整个容器化改造的起点,也是后续所有容器得以运行的基础。