Skip to content

树莓派 Docker 化网页服务器(1):当系统镜像遇上服务增长 —— 为什么我们需要 Docker

前言

树莓派4B自建网页服务器专栏里,我们从系统烧录,一路走到nginx\ddns-go\acme\frp逐个安装配置,完成了web服务器的搭建,迁移或容灾恢复方面我们也提供了镜像制作方案,整体是一个较为完善的闭环。作为基础网页服务环境,它够用了,即使损坏也能一键恢复,这很好。

但如果我想要在上面放更多的服务呢,比如数据库,java运行环境,对象存储等等,每安装配置完一个,为了能一键恢复,便需要连同系统和配置好的应用打包一个镜像包,而如果我只需要web+java运行环境,那就得回到web镜像,跳过数据库,安装配置Java再打系统镜像包,否则镜像包就是臃肿的、含额外服务的,环境一致性也就不成立了。

上面的情景反映出两个致命的问题:

  1. 随着服务数量的提升,系统镜像打包会失去灵活性,某几个服务的组合形成的组合爆炸,会极大的抬高系统镜像所需的打包次数,否则就要接受冗余的服务夹带其中。
  2. 单个服务链路中的每个组件应用,都要经历下载安装配置的过程,这个过程需要打包成一步到位的方式。

为了解决“服务生长”所要面临的这两个痛点,我决定引入Docker,基于旧专栏已完成的树莓派网页服务器底座,进行一次彻底的容器化改造

本篇导读

有了上面两个痛点的铺垫,“为什么要做容器化改造”这件事应该已经很清楚了。但先不急着动手 —— 在正式开始之前,有三件事需要在这一篇里先讲明白:Docker到底是什么、它和我们之前用的系统镜像有什么本质区别、以及整个改造计划的全貌是什么样的。

工欲善其事,必先利其器。这一篇我们没有任何实操,只做一件事:把认知框架搭好。后面的专栏篇章,每一篇都是对这个框架的一次具体填充。

如果你已经熟悉Docker的基本概念,可以直接跳到“框架预览”小节,按需选择对应篇章阅读。

什么是Docker

先从最直观的感受说起。

在旧专栏里,我们在一台树莓派上把某个服务跑起来,过程是这样的:

搜索软件安装教程 → 添加软件源(如果官方源里没有) 
    → 执行 `apt install` 或下载二进制文件 
    → 修改配置文件 
    → 设置开机自启(写 systemd service 或塞进 `/etc/rc.local`) 
    → 启动服务并验证

每一步都依赖当下那台机器的状态。同样的六步,在另一台树莓派上重走一遍,可能因为系统版本差一个小号、libc 版本不对、或者某个依赖被别的软件占用了,就卡在中间。

Docker 解决的就是这个问题。

一个类比

想象你要搬家。旧专栏给了你两种选择:

第一种:把家具拆成零件,搬到新家后再重新组装。如果新家的门框窄了两厘米,某件家具就装不进去了,你得现场锯掉一截。灵活,但慢,而且每搬一次都要重新调试。

第二种:连房子带家具外带地基,一整块切割运走。到了新地点直接落位,什么都不用重新配置。快,但死板——目标地块的承重规格必须符合下限,而且房子里所有的东西,哪怕你不需要的,也得一并带走。

第一种对应旧专栏里“逐个安装配置”的手工流程,第二种对应旧专栏第3篇教的系统镜像方案。

Docker的做法是第三种:把每件家具分别装进标准集装箱,贴上标签。搬到新家后,你需要哪件就开哪件,不需要的不开。家具之间互不干扰,换掉一件也不影响其他的。

这个“集装箱”在 Docker 的世界里叫做容器。而制作集装箱的模具,叫做镜像

两个核心概念

镜像(Image):一个只读的模板,包含了运行某个应用所需的一切——代码、运行时环境、系统库、配置文件。你可以把它理解成 ISO 文件,但它比 ISO 轻量得多,而且可以分层构建。

容器(Container):镜像的一个可运行实例。你可以对容器进行启动、停止、删除等操作,也可以在同一台机器上同时运行多个容器,它们互相隔离。

两者的关系可以这样记:

镜像是菜谱,容器是按菜谱做出来的那道菜。菜谱可以反复使用,每次做出来的菜都是一样的味道。

一个关键特性:环境一致性

这是 Docker 最核心的价值,也是我们引入它的根本原因。

回头看旧专栏里那四个组件,它们在裸机上的“落点”全都绑死了宿主系统:

  • Nginxsites-enabled 配置、日志路径、worker 进程用户,都按 Debian 的目录约定走;换到 Alpine 或 Ubuntu,路径和权限逻辑都不一样
  • ddns-go 是二进制下发,它的 config.yaml 默认落在 $HOME 下,开机自启要靠你手写一条 systemd service,service 里的 User=WorkingDirectory= 错一处就起不来
  • acme.shfrpc 也有类似的路径绑定问题,不再赘述。

也就是说,旧专栏那套“能跑”的状态,是这四个组件和这一台树莓派的系统环境共同协商出来的结果。机器、系统版本一换,协商结果作废。

而在 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引擎的树莓派系统镜像。这是整个容器化改造的起点,也是后续所有容器得以运行的基础。