ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

树莓派服务器为何需要Docker?从环境混乱到容器化运维

2026/9/18 19:42:12 拓冰建站 浏览量
树莓派服务器为何需要Docker?从环境混乱到容器化运维 1. 起因当树莓派服务越装越多我开始不敢关机手里的树莓派4B已经跑了小半年网页服务从最初的 Nginx 静态页到后来陆续加了 MySQL、Node.js API、内网穿透工具、定时爬虫脚本。系统是 Raspberry Pi OS Lite没桌面全靠 SSH 艹。最开始很爽apt install 一条龙一个 SD 卡撑起一个小网站。但是等到服务数量超过七八个之后事情开始不对劲了Python 版本冲突、Node 版本混乱、MySQL 的依赖库被某个安装脚本改掉、Nginx 的配置文件和另一个服务打架。最离谱的一次我升级系统依赖直接把 libssl 给搞挂了整机 SSH 都进不去最后只能拔卡重刷系统所有配置全部重来。那次事故之后我意识到一个道理裸机装服务本质上是在用一个正在运行的系统当作测试环境。对于一台需要长期稳定提供网页服务的树莓派来说这是不可接受的。所以我决定把整套服务器环境迁到 Docker 上也就是要给这篇博客的标题一个交代为什么我们需要 Docker。这篇文章就是这个系列的第一篇先把动机和核心概念讲清楚后面再逐步拆解具体怎么把树莓派上的网页服务 Docker 化。简单说这篇内容是给那些和我一样手里有一台树莓派把它当服务器用但是已经被环境问题折磨过的人看的。如果你正准备用树莓派跑点什么服务或者已经在裸机上装了一堆东西但还没出过大事这篇文章值得读完因为能帮你省下未来至少一个周末的修复时间。2. Docker 到底解决了什么问题服务增长带来的混乱本质是状态管理失控2.1 系统镜像的“快照”思维一台机器多个环境能共存吗裸机安装服务的本质是什么是在同一个系统镜像上不断叠加、修改、覆盖状态。你今天为服务 A 装了一个库明天服务 B 依赖同一个库的另一个版本apt 一升级服务 A 挂了。这种问题的根源不在于你操作失误而在于系统镜像本身是唯一的、共享的、全局的。所谓“系统镜像”其实就是一个完整的根文件系统加内核你的所有服务都共享这一份状态谁都没法保证自己的改动不影响别人。Docker 的思路不是改变服务本身而是改变服务的运行边界。每个容器都拥有独立的文件系统视图、独立的进程空间、独立的网络栈和独立的用户权限范围。容器用的底层内核是同一个但每个容器内部的 /usr、/etc、/var 这些都是自己独立的副本。这就相当于你在同一台物理机上虚拟出了多个互不干扰的“最小系统镜像”每个服务都以为自己跑在一台全新的机器上。打个比方裸机装服务就像合租一套房子所有租客公用厨房、卫生间、客厅一个人搞乱厨房其他人全部遭殃。Docker 则是给每个租客单独一个小隔间厨房、卫生间、卧室全都有独立的过滤和隔离系统外面再乱的装修也影响不到你。2.2 服务增长的必然结果环境冲突的临界点一定会到来有人会觉得我服务数量少装几个服务而已没必要上 Docker。确实当你只有一两个服务时裸机部署反而最简单。但是服务增长是一个必然趋势——你今天为博客加了评论数据库明天又加了一个搜索索引服务后天临时挂了一个微信机器人。每增加一个服务潜在冲突的对数就增加一分。我在树莓派4B上经历过的最典型场景一个 Python 项目需要 Python 3.9而系统默认是 3.7另一个 Node 项目需要 npm 全局包但版本又和其他项目要求的互相覆盖还有一个旧项目用 MySQL 5.7新项目要求 MySQL 8.0。在裸机上这些需求无法同时满足除非你手动管理多个版本的运行时而这是一件极其痛苦的事。如果你把这些服务各自放进一个容器每个容器里装自己需要的 Python、Node、MySQL整个系统就恢复了清净。2.3 系统镜像的大小和复杂度树莓派 SD 卡的脆弱性被放大了树莓派的 SD 卡存储和完整性比普通服务器的硬盘差得多。频繁读写、突然断电、依赖库覆盖安装这些操作都在让 SD 卡更接近损坏边缘。系统镜像越大、被修改得越频繁风险越高。Docker 镜像虽然也占用存储但它的数据是一层一层封装的每次部署新服务不需要再往系统根分区里写一堆动态链接库。服务运行产生的数据也可以挂载到独立的 volume 里和系统目录分离方便管理和备份。更重要的是如果 SD 卡真的损坏了裸机系统的恢复方式是重新刷整个镜像然后手动把服务一个一个装回来。而 Docker 化的环境只需要在另一台树莓派上执行 docker-compose up -d 即可一键恢复。这个差距在数据丢失和个人心力上的节省是巨大的。3. Docker 的核心原理拆解镜像层、容器层和挂载卷的关系3.1 镜像一套经过验证的“装机预演方案”Docker 镜像可以理解为一个打包好的系统文件系统快照它包含一个 service 运行所需的全部文件二进制文件、库文件、配置文件和依赖项。镜像是分层的每一层都是只读的。所有层叠加在一起构成一个完整的、可执行的根文件系统视图。镜像是怎么产生的在 Dockerfile 里每条指令RUN、COPY、ENV 等都会创建一个新层。这些层可以复用例如两个服务都基于同一个基础镜像比如 ubuntu:22.04那么它们的前几层是完全一样的不会重复存储。这意味着哪怕你有十个容器只要基础镜像相同实际磁盘占用并不是十份完整镜像而是一份基础层加上各自的小改动层。这个特性对 SD 卡容量有限的树莓派来说非常宝贵。镜像还具备不可变性。一旦构建完成镜像内容就不会被修改。你改代码改配置这些变更应该通过重新构建新镜像来完成而不是进入容器内手动改文件。这里有一个深刻的意义通过镜像你得到了一套经过验证的、可重复执行的“装机预演方案”。团队里任何人拿到镜像都能跑出完全相同的环境彻底告别“在我机器上明明是好的”这类推诿。3.2 容器镜像的运行时实例不是一台虚拟机容器是镜像跑起来之后的实例它是活着的、有状态的相对镜像。容器的文件系统是在镜像的只读层之上叠加了一个可写层所有运行时的文件写入都在这一层。容器内部运行着你的进程比如 Nginx但它的进程是运行在宿主机内核之上的只是通过 namespaces 和 cgroups 实现了隔离与资源限制。容器和虚拟机的最大区别在于虚拟机需要模拟硬件跑一个完整的客户机操作系统Guest OS每个虚拟机都占用几百 MB 甚至几个 GB 的内存对树莓派这种 ARM 小板上来说不可接受。而容器直接和宿主机共享内核只额外消耗进程级别的系统资源。一个精简的 Nginx 容器在树莓派上内存占用可能只要 30~50 MB这在同一台硬件上跑十个服务变得非常现实。那容器的隔离是不是绝对安全的不是。容器的隔离级别不是安全边界而是运维边界。它隔离的是运行环境不是内核。如果一个容器里的进程能获得宿主机的 root 权限它仍然可能影响整个系统。所以不要把容器当作安全沙箱来用它首先是一个环境隔离工具。3.3 数据卷容器消失后你的数据还在容器本身是可丢弃的。docker rm 之后整个容器的文件系统就删掉了。如果服务产生的重要数据写在容器可写层里就会跟着容器一起消失。所以 Docker 引入了 volume 机制主动把容器内的某个目录映射到宿主机的一个持久化存储路径。在树莓派场景下数据卷挂在 SD 卡上即可但更推荐挂在外部 USB 移动硬盘或 NFS 共享目录里。因为 SD 卡的写入寿命是个硬伤MySQL 这种频繁写日志和数据的服务如果跑在 SD 卡上会明显加速卡的损坏。我在自己的树莓派上把 MySQL 的数据目录挂到了移动硬盘上用了两年也没有出现过数据库文件损坏的问题。数据卷的第二种用途是共享配置或代码。比如你可以把宿主机上的某个目录映射到容器里的 /app这样改代码就不需要重构镜像直接改宿主机的目录容器内立即生效。这在开发调试阶段特别方便但是生产环境我会建议用镜像固化代码版本然后再映射日志和持久化数据目录。为什么要这样做因为生产环境里的容器可能分布在多台机器上如果依赖宿主机目录代码不一致就失去了镜像的一致性意义。4. 树莓派上 Docker 化网页服务器的实际收益部署、回滚和资源占用4.1 部署动作从“约等于重构系统”降级为“写一段配置”裸机部署一个网页服务的典型动线是apt 安装依赖、下载运行时、解压配置、准备 DB、修改 Nginx 站点配置、测试、重启服务。这个过程里最磨人的不是步骤多而是每一步都可能破坏系统的其他部分。今天你为了一个新服务把 MySQL 的端口改成了 3307搞不好老服务就没法连数据库了。Docker 将这一切变成了一份 docker-compose.yml。我来展示一个简化但真实的示例这段配置可以在树莓派 4B 上直接跑一个 Nginx 加 MySQL 的网页服务器基础环境version: 3.8 services: web: image: nginx:1.25-alpine container_name: web-server ports: - 8080:80 volumes: - ./html:/usr/share/nginx/html:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro depends_on: - db restart: unless-stopped db: image: mysql:8.0 container_name: web-db environment: MYSQL_ROOT_PASSWORD: example_pass MYSQL_DATABASE: webapp volumes: - db_data:/var/lib/mysql ports: - 3306:3306 restart: unless-stopped volumes: db_data:这不是炫技它就是日常工作的普通写法。你锁定镜像版本定义端口映射确定数据挂载关系然后一条 docker compose up -d 启动全部。以后再部署一个新服务不需要再考虑会不会影响系统上的老服务只要你新定义一个 service 块即可所有依赖都封装在镜像里。4.2 回滚操作有了真正的“时光机”裸机环境下你改了某个配置文件然后服务起不来了怎么回滚如果记得住改了哪些行可以还原如果不记得那就只能翻日志、查历史命令甚至重新配置整个服务。这个过程非常耗时而且你在现场操作时往往带着压力更容易做错。Docker 镜像天然带版本化。每次构建新版本时打一个新的 tag比如 my-web-server:v2、my-web-server:v3。如果 v3 出了问题只需要把 docker-compose.yml 里的镜像 tag 改回 v2然后重新 up几秒钟就回到上一个可用状态。它的背后原理很简单——镜像层是不可变的每一版都保留着当时构建的完整文件状态。这个能力在网页服务频繁改版时非常值钱因为网站依赖的配置、后端代码、静态文件是一个整体状态集合而不是散落在一堆目录里的文件。回滚还有一个高层层面的好处因为你敢随意回滚了就会更敢于做变更实验。我之前有一个项目的 HTTP 服务需要从 Nginx 迁移到 Caddy放在裸机上要折腾一个下午但在 Docker 里我新起一个 Caddy 容器把端口改好测试完直接切换流量入口不理想就切回来全程对系统零污染。4.3 资源占用容器并不是免费午餐但值得这个开销有人担心树莓派 4B 只有 4GB、8GB 内存跑 Docker 会不会太吃力。我的实测数据是基础系统Raspberry Pi OS Lite Docker Engine Docker Compose开机后内存占用约 400~500MB其中 Docker 守护进程长期驻留约 100~150MB。一个 Nginx Alpine 容器约 30~50MB一个 MySQL 8.0 容器约 250~400MB一个 Node.js API 容器约 80~150MB。整体跑一个中小规模的网页服务加数据库内存占用控制在 1.5GB 以内完全可行4GB 版树莓派跑三四个网站实例绰绰有余。CPU 方面Docker 容器有 cgroups 的资源限制能力。你可以在 docker run 或 compose 文件中限制某个容器最多使用多少 CPU 配额、多少内存。我非常建议给数据库类容器设定高于其他容器的内存限制同时给每个服务都设置 restart: unless-stopped这样即使某个服务崩溃退出守护进程也会自动拉起它不需要人工干预。存储层面Docker 镜像占用的实际空间可能比预想的要小因为镜像层复用的机制存在。举个实际例子我有五个服务其中三个都基于 node:18-alpine 基础镜像那么这几个镜像的基础层只存一份。最终用 docker system df 查看总占用可能比所有镜像尺寸加起来少 30%~40%。5. 为什么偏要在树莓派上 Docker 化ARM 平台的特殊考量5.1 ARM 架构镜像选择和外部生态的适配树莓派是 ARM 架构跟大多数云服务器x86_64不一样。Docker Hub 上大部分官方镜像都提供了 multi-arch 支持也就是说同一个 nginx:1.25-alpine 标签系统会根据你 CPU 架构自动拉取 ARM64 版本不需要手动指定。你要注意的反而是一些非官方镜像尤其是只有 amd64 架构的第三方镜像在树莓派上是无法直接运行的这时候你需要找替代镜像或者自己用 Dockerfile 从源码构建。另外一个考量是树莓派的系统镜像本身。Raspberry Pi OS 是 32 位为主的系统即使是树莓派 4B官方也推荐 32 位 Lite如果你用的是 32 位系统容器也只会跑 32 位版本。我自己后期换成了 64 位的 Raspberry Pi OS Lite原因很简单很多镜像是纯 64 位的且单个进程地址空间超过 4GB 限制在某些内存密集场景下有优势。如果你从零开始直接装 64 位系统后面少很多麻烦。5.2 GPIO 和硬件直通与 Docker 的结合树莓派不同于普通服务器的另一个特点是 GPIO通用输入输出接口。很多树莓派项目会用到传感器、摄像头、继电器等外设这些东西绕不开直接操作硬件资源。Docker 容器虽然隔离了环境和进程但硬件访问默认是受限的。要让容器访问 GPIO需要挂载 /dev/gpiochipX 等设备节点并以特权模式运行容器privileged: true。这个操作要谨慎特权模式会削弱容器的隔离性相当于告诉内核“允许这个容器的进程访问更多宿主资源”。我的实践结论是纯网页服务器场景不建议直接用 Docker 管理 GPIO。如果业务同时涉及网页展示和硬件控制我会把 GPIO 控制逻辑放在宿主机上的一个独立服务里然后通过 HTTP API 或 MQTT 桥接给容器内的网页服务。这样做的好处是硬件访问和网页应用解耦容器的重启不会导致引脚状态被系统重置。如果你一定要在容器里操作 GPIO务必记住挂载设备节点和特权模式是必须的同时你也要接受安全隔离性下降这一代价。5.3 网络模式的选择桥接、主机、还是 MacvlanDocker 提供多种网络模式但在树莓派上我最常用的是 bridge 和 host 两种。bridge 模式是默认的每个容器会分配一个独立的内部 IP宿主机通过端口映射把外部流量转发到容器。这种方式灵活多个容器可以用相同端口比如各个容器内部都用 80只是外部映射不能冲突。在前面的 docker-compose 示例里我就是把 web 容器内部的 80 端口映射到宿主机的 8080这就是 bridge 模式。host 模式则让容器直接使用宿主机的网络栈没有端口映射这一层。这个模式在两种场景下很好用一是服务需要大量使用主机端口比如 UDP 服务二是希望容器内看到的外部 IP 就是宿主机实际的 IP方便一些鉴权或者日志工具。缺点是多个容器不能同时监听同一个端口而且容器和宿主机网络环境混在一起隔离性降低。在树莓派的场景中如果你只跑少量服务host 模式可能让人感觉更简单但如果服务数量上来了我建议还是统一用 bridge 模式加反向代理来管理入口流量。还有一点经验值得提不要把树莓派直接暴露在公网端口上。如果网页服务需要公网访问用一个带 TLS 的 Nginx/Caddy 作为入口容器后面挂内部网络里的各应用容器。这既是安全习惯也是工程组织上的清晰边界。6. 当系统镜像“一路升级”到 Docker 化之后运维习惯的转变6.1 从“登录进系统敲命令”到“声明式描述状态”裸机运维的习惯是登录到系统用命令直接修改系统的状态。Docker 化之后运维对象从“这台机器上的文件与进程”变成了“描述期望状态的编排文件”。你不再关心某台树莓派上到底装了哪个版本的 Nginx因为镜像 tag 已经把版本钉死了。你关心的是 docker-compose.yml 里写了什么、镜像仓库里存了哪些版本。这个转变最直接的收获是——你不需要再手动记录系统里改了什么。因为一切可重复的配置都写在文件里了有据可查。全新的机器拉一个仓库、执行 compose up整个环境就回来了。如果你的服务是交给别人维护的别人也不需要翻你脑内的记忆他只需要读配置文件和 README。为了这个转变顺畅我强烈建议从一开始就做两件事第一所有 Dockerfile 和 docker-compose.yml 全部纳入 git 仓库管理第二使用 .env 文件统一管理变量数据库密码、域名、端口不要把这些信息写死在 compose 文件里。这样每一次改动都有历史记录回滚的尴尬少一半。下面是 .env 文件的一个示例# .env MYSQL_ROOT_PASSWORDchange_me_to_long_random MYSQL_DATABASEwebapp WEB_PORT8080然后在 docker-compose.yml 里通过 ${MYSQL_ROOT_PASSWORD} 引用Docker Compose 启动时会自动从 .env 读取并插入。但注意 .env 不要提交到 git 仓库避免泄露密码。你需要维护一份 .env.example 作为模板提交到仓库供别人参考。6.2 备份和迁移逻辑的变化裸机环境下备份 整个 SD 卡的 dd 镜像或者 cp 关键目录再或者 mysqldump 导出数据库。这套方式的问题在于全卡镜像太大、太慢局部备份又容易漏。Docker 化之后备份思路变成数据卷备份 配置文件备份 镜像清单备份。具体来说我会用 tar 直接打包 MySQL 数据卷或者用 mysqldump 导出 SQL再打包外部挂载的网站文件和 Nginx 配置。镜像本身不需要备份到 tar 文件里因为镜像可以通过 Dockerfile 随时重新构建只需要把 Dockerfile 和依赖的源码仓库备份好即可。这个思路本质上参考了“CowCopy-on-Write镜像层 Volume 持久化”的设计哲学镜像负责描述卷负责存储。迁移到另一台树莓派时新机器上只需要装好 Docker Engine把代码仓库和 .env 文件复制过来执行 docker compose up -d --build等镜像拉取和构建完成服务就回来了。这个过程里不需要手动安装 Node、MySQL、Nginx省下的是大量重复劳动和历史遗留问题。6.3 安全更新的节奏和坑Docker 化有一个容易忽视的安全问题镜像更新了本地仓库里的镜像也更新了但是已经运行的容器还是旧环境。裸机部署时系统提示安全更新你可能顺手就 apt upgrade 了Docker 化之后如果不主动重新拉取镜像并重建容器容器内部的系统和依赖其实一直停留在初始版本。所以我会在部署记录里做一个“镜像依赖清单”表格记录每一个服务使用的基础镜像、镜像 tag、最近检查时间。每三个月检查一次官方镜像是否有新版本如果有就构建新版发布。以下是一份简化的示例表格服务镜像当前版本检查时间是否有更新Nginx 入口nginx:1.25-alpine1.25.x2025-01-10否MySQL 数据mysql:8.08.0.x2025-01-10是8.0.41Node APInode:20-alpine20.x2025-01-10是20.12另一个值得注意的点镜像更新后重建容器那一下会造成短暂的服务中断。如果你是在一个真正对外提供服务的环境下需要设计优雅停机或者滚动更新。树莓派家庭服务器场景不用太纠结选择凌晨自动执行更新即可我有一个定时任务每晚 3 点跑 docker compose pull docker compose up -d再配合健康检查出问题的概率很低。7. 实践中的常见问题与排查技巧实录7.1 Docker 服务开机不自启重启后全挂树莓派作为服务器经常断电重启。如果你没有开启 Docker 服务的开机自启也没有给容器设置 restart 策略那么重开机之后所有容器都处于停止状态网页服务自然起不来。解决方法是两条命令配合使用sudo systemctl enable docker同时docker-compose.yml 里每个服务都加 restart: unless-stopped。两者的区别是systemctl 负责 Docker 引擎本身的开机自启restart 策略负责引擎起来后自动拉起容器。光有前者容器不会自动恢复光有后者Docker 引擎不启动也没用。别遗漏任何一条。我在排查这类问题时有一个习惯先跑 docker ps -a 看容器状态再看 docker logs 看启动报错最后才去改配置。有一次我以为容器没起来是配置问题整整看了二十分钟日志最后发现是宿主机的移动硬盘没挂载MySQL 数据卷挂载点不存在容器启动了但立刻退出了。这类问题靠日志说话最直接。7.2 容器里时间不准确日志时间戳飘了容器的默认时间通常是 UTC 而不是本地时间。如果你看容器日志时发现时间和宿主机差了八个小时别慌这不是容器坏了而是时区没配置。解决办法是给容器设置环境变量 TZAsia/Shanghai。在 docker-compose.yml 里这样写services: web: environment: - TZAsia/Shanghai如果是 docker run 命令加 -e TZAsia/Shanghai 即可。你可能会想为什么镜像不自带正确的时区因为容器是可移植的镜像作者不知道你运行在哪时区所以默认用 UTC 是合理的。这是容器的设计哲学——不预设外部环境一切由运行者注入。7.3 日志量爆炸SD 卡空间被日志填满Docker 默认没有限制容器日志文件大小如果一个服务打印日志比较激进/var/lib/docker/containers/... 下的日志文件会无限增长。树莓派 SD 卡本来容量就有限这个问题必须处理。我的建议是在 Docker daemon 配置里加上日志轮转{ log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 } }修改 /etc/docker/daemon.json 后重启 Docker 服务。这个配置的意思是每个容器最多保留三个日志文件单个不超过 10MB超过就滚动覆盖。这样日志占用的空间被严格限制在一个可控范围内。当然如果你有外部日志采集需求可以给容器配置 gelf 或 syslog 日志驱动把日志发送到集中日志平台。家庭服务器场景轮转就够了别为了一个玩具项目把架构搞复杂。7.4 端口占用导致容器启动失败这个错误场景很经典树莓派上如果已经有一个进程占了 80 或 8080 端口你新建的容器想映射这个端口就会报 bind: address already in use。这时候排查顺序是确认哪个进程占用了端口再用 lsof 或 ss 定位然后决定是杀进程、改端口映射还是干脆用 host 网络模式。还有一个容易掉坑的情况你看 docker ps -a 发现容器是 Exited 状态然后手动 docker start 起不来日志提示端口绑定失败。这可能是因为容器内部的服务监听的是宿主机某端口但是容器本身没起来导致端口还占着。我的建议是统一用一个入口反向代理容器管理 80/443所有业务容器映射到内部随机端口或者干脆不映射宿主机端口让代理容器通过 Docker 内部网络转发。这样端口冲突的概率会降到最低。7.5 容器内网络验证如何区分是“容器没跑好”还是“容器网络不通”排查容器网络问题的思路和裸机排查稍有不同。裸机上你直接 curl 一个地址看通不通容器里你需要先进入容器再发起请求。注意容器内的 localhost 是容器自己的 loopback不是宿主机的。很多新手在这上面栽过跟头容器内 curl 本机 3306 端口的 MySQL发现连不上以为 MySQL 没起来其实是自己在容器里访问的是自己的 3306 端口而 MySQL 跑在另一个容器里。你可以用 docker compose exec 进入容器内部做测试也可以用 docker inspect 查看容器的网络 IP然后在宿主机 curl 那个 IP。但最直接的方式是在同一个 Docker 网络里直接使用服务名作为主机名访问比如从 nginx 容器访问 mysql 容器URL 应该是 jdbc:mysql://db:3306/webapp其中 db 是 compose 文件里定义的服务名。Docker 内置 DNS 会把这个服务名解析到对应的容器 IP 上——前提是这两个容器在同一个用户自定义网络里。强调一下默认的 bridge 网络没有启用容器名自动解析所以你需要用 docker network create 自定义一个网络然后让 compose 服务都加入这个网络。在 docker-compose.yml 里如果指定了 networksCompose 会自动创建一个项目级别的网络服务名和容器名都可以在这个网络里被解析。8. 我个人在树莓派 Docker 化过程中的体会如果你现在问我树莓派上跑网页服务器能不能不用 Docker直接裸机也挺好答案是可以但是有一个隐含前提你确定你的服务数量长期保持在一两个且你不在意未来可能的灾难性环境问题。这就像住宿舍一个人住确实自由但一旦合租的人多了公共空间一定出乱子。Docker 本质上不是“多此一举”而是为服务增长预留的“房间隔断”。在我自己的实践经历中Docker 化带来的最大价值并不是“部署快”或“回滚方便”而是心态上的安稳。以前我半夜收到服务告警短信心里第一反应是“完了是不是系统哪里又坏了”现在我会想“先看日志如果环境坏了直接重新构建容器最多几分钟”。这份淡定是用一次惨痛的 SD 卡系统崩溃换来的也提醒我永远不要低估一个共享系统被多个服务反复修改所带来的复杂性。这个系列接下来会拆解更多的实操内容如何在树莓派上安装 Docker Engine、怎么写一个适合 ARM 平台的 Nginx Node.js MySQL 的 Dockerfile、如何组织 compose 多服务编排、如何做数据卷备份和恢复。如果你正在规划树莓派上的新服务或者在痛苦的维护旧环境真的可以趁早把 Docker 搬家提上日程。一步一步来第一篇只需要理解为什么后面的命令都是水到渠成。