ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

树莓派服务器之路:从依赖地狱到Docker容器化

树莓派服务器之路:从依赖地狱到Docker容器化 走进死胡同的那天我重新思考了树莓派服务器的打开方式先说说我自己的经历。2022 年我用手头一块树莓派 4B 搭网页服务器刷好系统镜像、装好 Nginx、把静态博客放上去的时候整台设备安静得像个路由器。那时候我觉得“树莓派装服务器”这件事不就是刷个系统、配个 Nginx、写完收工吗文档里说的什么 Docker、容器化对一个只跑一个服务的场景来说完全是多余的。直到半年后同一台树莓派上堆了博客、图床、内网穿透、定时任务、数据库缓存、还有一个自己写的 Python 小接口。系统的/etc目录像一间堆满了杂物的地下室谁也说不清哪个配置被哪个服务改过。某个周五晚上我想给 PHP 装一个扩展apt install蹦出一长串“下列软件包将被移除”的警告。我没仔细看就按了回车然后 Nginx 挂了PHP 挂了连我自己写的 Python 服务也起不来了。那一刻我坐在屏幕前面对一个只剩系统的树莓派心里非常清楚环境全没了。那篇博客文章标题里说的“当系统镜像遇上服务增长”说的就是我这种人。所以这篇文章不谈具体配置细节先把最核心的问题讲透树莓派跑网页服务器服务一多起来为什么传统的“直接往系统里装环境”这条路会越走越窄以及为什么 Docker 才是更适合的解。1. 我的树莓派网页服务器是怎么从顺滑变成灾难的1.1 第一个服务时代一切都还美好刚开始搭服务器那会儿我刷的是 64 位系统镜像改完清华源之后装软件特别快。装 Nginx、改配置、丢一个静态页面进去浏览器输入树莓派 IP 就能看到内容。那种感觉真的很爽一台三十多度的 ARM 小机器安安静静提供网页服务电费几乎可以忽略不计。当时浏览器的书签栏里躺着很多 Docker 教程我翻过几篇心里想的却是“这么简单的服务Docker 纯属多此一举”。这种想法在只有一个服务时完全成立因为单服务的系统环境非常简单Nginx 装上去配一个站点剩下的就是更新页面内容。系统里只有一套 PHP、一套 Python 环境、一个数据库各自的依赖互不干扰根本感觉不到“环境管理”有什么成本。1.2 服务增长期每个新功能都在系统里留下一地鸡毛后来事情开始变化。博客需要动态化我装了 PHP-FPM 和 MySQL想给博客加个随机图片接口用 Python 写了 Flask 应用顺手全局装了一堆 pip 包为了应对搜索引擎抓取和手机端访问又加了个缓存服务装上了 Redis再后来为了做内网穿透又装了一个 node 写的工具。每一个服务单独看都很简单安装过程也都有教程。但问题在于它们全部挤在同一个操作系统里。PHP 需要某个版本的libxmlPython 的某个库又要求openssl新一些Node 工具自带的依赖和系统库又产生冲突。pip和apt各自维护一套依赖谁也不管谁。我想把 Python 接口的依赖整理一下结果没整理完就放弃了一一“能跑就行”成了我的口头禅。真正让人头疼的是系统升级。某次我看到 apt 提示有更新随手执行了apt upgrade然后第二天博客页面开始报 502。查了半天是 PHP 的某个扩展和更新后的库不兼容。那次我学乖了每次升级前先备份配置文件但备份也只是把/etc/nginx和网站目录打个 tar 包系统层面的依赖关系我根本没有能力管理。1.3 压垮我的最后一根稻草一次“常规操作”毁掉所有真正让我下定决心研究 Docker 的是那次装 PHP 扩展的事故。我想给 WordPress 加一个图片处理功能需要启用gd扩展。apt install php-gd打出来的安装计划里赫然列着“以下软件包将被移除php-fpm、php-mysql、nginx”。系统认为新版本的libgd和现有的 PHP 7.4 不兼容打算把旧 PHP 相关的全家桶全部替换掉。我犯了两个错误没仔细看移除列表就按了回车也没有记录当时系统里都装了哪些 PHP 组件。结果就是 Nginx 站点配置还在但后端已经没有 PHP-FPM 进程了。我试图把列表里的包原样装回来可apt报了一堆依赖错误越折腾越乱。最终我只好把数据库目录和网站目录单独拷出来老老实实重新刷了系统镜像。重刷之后的一整周我都在重新配环境。装 Nginx、装 PHP、装数据库、导入数据、配 HTTPS 证书、调缓存参数……每一个步骤在教程里都只是“一条命令”的事但组合到一起就是好几个晚上的折腾。那周我反复问自己一个问题为什么一台服务器要把所有环境都焊死在系统镜像里难道就没有一种方式能把每个服务的环境彼此隔离互不干扰吗2. 传统部署方式的问题不是“不够熟练”是架构上限到了2.1 依赖地狱所有服务共用一套全局环境直接往系统里装服务的本质是把所有服务的运行环境全部塞进同一个操作系统。打个比方这就像一间厨房里只有一个调料架和一块案板所有菜都在上面做。做第一道菜Nginx时很干净做第二道PHP时开始摆上自己的调料做第三道、第四道时各种调料已经混在一起了。这时你想再做一个需要特定生抽的菜新服务/新扩展就很容易和台面上已有的老抽、陈醋冲突。这种状态有非常具体的表现形式某个服务依赖libssl1.1另一个服务需要libssl3两个库不能同时存在时就有一个服务起不来Python 的全局site-packages被不同项目的依赖反复覆盖今天装 A 库把 B 库的版本顶掉了明天为了跑 B 项目又把 A 库降回去。网上搜解决方法最终都会得到那句万能回答“建议用虚拟环境。”但如果连 Nginx、PHP、MySQL 这些系统级服务都搅在一起仅靠 Python 虚拟环境根本救不了。2.2 系统镜像的脆弱性根坏了整棵树都活不了传统部署方式把服务和系统镜像紧紧绑在一起系统镜像就像一棵树的根所有服务都是树杈。某个服务升级失败、某个配置写错、甚至一次不完整的apt autoremove都可能让整棵树死掉。我之前那次事故本质就是这么回事系统层面删掉一组包上面所有的服务立刻失去运行基础。树莓派场景里这个问题更突出。SD 卡本身在频繁读写下就不算特别稳定如果又碰上断电、文件系统损坏系统镜像往往整体报废。传统模式下代码文件、数据库、配置、依赖、系统库全都散落在这张卡上的各个目录里没有清晰的边界。想备份不知道该拷哪些目录想迁移更找不到一个“打包带走全部环境”的办法。2.3 备份与迁移的笨重我试过 dd 整卡等得怀疑人生网上最常见的树莓派备份方案是dd整卡克隆把整张 SD 卡做成一个镜像文件。听着很省事“一张卡是什么样镜像就是什么样”。但实际操作起来64GB 的 SD 卡即便只用了 8GB备份时间也得按小时算。恢复的时候更麻烦还原到一张更大的卡上分区还要手动调整。而且整卡备份有个逻辑问题成本最高的部分恰恰是环境本身而不是数据。网页服务器的数据数据库、网页文件其实不大几百 MB 顶天了拷出来很容易。真正庞大的是系统库、编译缓存、依赖包这些东西却是不值得备份的因为它们可以用一套干净系统重新搭出来。传统模式把“数据”和“环境”搅在一起导致备份必须连环境一起搬于是被迫用最笨重的方式做最不划算的事情。2.4 隐形冲突端口、权限、自启脚本越想越头大服务多起来之后还有一些更难察觉的问题。首先是端口冲突Nginx 要占 80另一个服务也要监听 80就得协商谁换端口。接着是权限问题一个服务需要读取另一个服务产生的日志文件用户和组不对就得自己改权限改了又可能引发安全隐患。还有自启问题有的服务自己注册了 systemd 单元有的服务被装完脚本默默丢进了rc.local你自己都要查半天才能说清楚机器开机后到底启动了哪些进程。这些问题单拎出来都不难解决但它们非常消耗精力。每加一个新服务表面上只是“多跑一个进程”实际上是要跟已有的整套系统环境重新谈判一次端口让不让、依赖冲不冲突、开机顺序怎么排、日志归谁管。到后来维护这些琐碎关系的成本已经超过了服务本身带来的价值。3. 树莓派加上 Docker反而是最适合的解法3.1 ARM 架构的编译噩梦Docker 直接绕过去了如果只在 x86 台式机上玩服务器你可能体会不到源码编译的痛苦。但在树莓派上这个问题非常现实树莓派是 ARM 架构很多软件包没有现成的 ARM 预编译版本或者系统源里的版本太旧。你想装一个新工具大概率得从源码开始编译而编译前又要先装一堆编译依赖那些依赖可能又要编译……我在树莓派上编译过一次软件编译了四十分钟最后因为缺一个小库报错退出那一刻我连砸电脑的心都有。Docker 带来的改变是颠覆性的Docker Hub 上几乎所有主流镜像都提供了 arm64 版本。docker pull nginx:alpine自动匹配当前架构拉下来就是 ARM 版可以直接运行。你再也不需要为了跑某个软件去编译半天也不用担心系统源里包的版本太旧。这个优势在 x86 机器上不那么明显但在树莓派上它等于直接帮你绕过了 ARM 生态最伤人的一个环节。3.2 系统镜像与 SD 卡容器化之后重刷系统终于不再恐惧树莓派的系统镜像几乎都装在 SD 卡上一旦系统坏了重刷几乎不可避免。传统模式下重刷系统意味着所有环境全部重来所以我当时对“重刷”这两个字有本能的恐惧。但容器化之后系统的角色完全变了操作系统镜像只负责提供内核和基础驱动真正跑在上面的服务全部封装在容器里。这意味着你可以把“系统镜像”和“个人数据”彻底分开。网页服务器代码放在数据卷里具体概念下一节细讲容器配置写成 Docker Compose 文件系统万一坏了刷一个干净系统、安装 Docker、恢复数据卷目录、执行docker compose up -d半小时内业务就能回来。这个恢复速度对树莓派这种经常用来折腾的小设备太重要了我后来甚至养成了一个习惯每台树莓派新系统装完第一件事就是装 Docker再也不想回到“系统里塞满环境”的日子。3.3 一张卡跑多个角色网页服务器、下载机、智能家居中枢互不打架树莓派恐怕是全世界被分配最多兼职的设备白天是网页服务器晚上是下载机周末又变成智能家居中枢。传统模式下这些角色挤在同一个系统里你想把某个角色拆出去单独维护根本没门。容器化之后每个角色就是一个独立容器互不干扰。比如我的树莓派上网页服务在一个容器里下载工具在另一个容器里内网穿透工具单独跑一个容器。升级下载工具时不会再影响网页服务清理某个容器也不会误删系统的公共库。甚至同一份配置我可以直接复制到另一台树莓派上复用因为容器不关心宿主机的具体环境只要 CPU 架构一致、Docker 装好跑起来几乎是一模一样的。3.4 性能开销到底大不大实测下来的答案很明确“树莓派那点性能跑 Docker带得动吗”这是每次聊这个话题都避不开的疑问。这里先说明一个关键区别Docker 容器不是虚拟机。虚拟机要在宿主机里完整模拟一套硬件和操作系统资源开销当然大但容器只是一个隔离的进程它直接使用宿主机的内核和普通进程相比没有本质差别。我在树莓派 4B4GB 内存版上同时跑了 Nginx 容器、Python 接口容器和一个 Redis 容器用docker stats看过资源占用三个容器加起来内存占用大约 400MB 出头CPU 在空闲时几乎为 0只有请求进来时才会跳起来。也就是说容器化的额外开销主要来自 Docker daemon 本身和几层文件系统管理通常几十MB内存、几乎没有持续CPU占用对树莓派来说完全可以接受。如果你用的是树莓派 5那就更轻松了。唯一要注意的是内存1GB 内存的树莓派会比较紧张建议至少 2GB能上 4GB 就会舒服很多。4. 动手前要搞清的四个基础概念镜像、容器、数据卷、端口映射4.1 镜像与容器一个是模具一个是扣出来的蛋糕我刚接触 Docker 时最常搞混的就是镜像Image和容器Container这两个词。后来我用一个做糕点的例子把它记住了镜像是模具容器是用模具扣出来的蛋糕。模具本身是只读的、不会变的不管扣多少次出来的蛋糕形状都一样但每个蛋糕是独立的抹不同奶油、加不同水果互不影响。对应到实际操作docker pull nginx:alpine是把“Nginx 的模具”拉到本地docker run则是用这个模具扣出一个正在运行的实例也就是容器。同一个 Nginx 镜像你可以同时跑出三个容器分别服务不同的网站只要端口不冲突就行。这解释了一个最基本的优势传统模式下装两份 Nginx 几乎不可能但容器模式下想跑几份跑几份。4.2 数据卷容器可以随便删数据不能丢按上面的比喻如果你往蛋糕上撒了糖霜蛋糕一旦丢掉糖霜也跟着没了。容器也一样容器运行期间产生了文件、写入了数据这些数据默认存放在容器的可写层里一旦删除容器数据和容器一起消失。树莓派跑网页服务器时网页文件、数据库文件、日志文件都是不可丢失的数据必须挂到容器外面。Docker 提供了两种方式数据卷Volume和绑定挂载Bind Mount。通俗讲就是把宿主机上的某个目录“塞进”容器里的某个路径。容器删了重建只要挂载点没有换数据就还在。我就曾经因为没挂载数据卷删除一个 MySQL 容器重建时把整个数据库删没了。那次教训之后我养成了一个习惯任何有状态的服务启动参数里必须能看到挂载目录否则坚决不跑。4.3 端口映射宿主机和容器之间的“门牌翻译”容器的网络和宿主机是隔离的。你在树莓派上运行一个监听 80 端口的容器宿主机默认并不能直接通过“树莓派 IP:80”访问到它因为容器有自己独立的网络空间。想让外面访问到容器必须做端口映射-p 8080:80意思是把宿主机的 8080 端口流量转发给容器的 80 端口。这个机制带来很大的灵活性。比如你想在同一台树莓派上跑两个网站两个容器内部都用 80 端口一个映射到宿主机的 8080另一个映射到 8081两者互不冲突。后续你还可以在前面加一个 Nginx 反向代理容器用域名区分不同站点统一入口、统一配 HTTPS。端口映射是理解容器网络的第一步这一步搞通了后面部署多容器架构就会顺很多。4.4 重启策略树莓派反复断电服务也能自动回来树莓派通常放在角落、路由器旁边电源线接得很随意。小区偶尔停电、家人随手拔个排插这台小机器就会直接断电。传统模式下系统重新启动后Nginx、PHP-FPM、MySQL 这些服务能不能自动拉起取决于你是否给每个服务都写了 systemd 单元文件并 enable。总有一两个服务会被漏掉等你出差在外时才发现博客挂了。Docker 把这件事变得非常简单给容器设置--restart unless-stopped或者 Docker Compose 文件里写restart: unless-stopped然后就不用管了。宿主机开机后Docker daemon 会按策略自动启动所有标记为 restart 的容器。树莓派断电重启、插拔电源、系统更新重启服务都会自己回来这对无人值守的小服务器来说非常实用。5. 从零跑通第一个 Nginx 容器以及新手最常踩的坑5.1 树莓派上安装 Docker Engine别再纠结“哪个版本”树莓派上不需要安装 Docker Desktop。Docker Desktop 是给桌面操作系统Windows、macOS用的 GUI 工具在 Linux 服务器上完全没有必要。树莓派要装的是 Docker Engine 和 Docker Compose 插件都是一行命令的事。我用的是树莓派 OS64 位和 Ubuntu Server 两种系统安装方式略有区别但都可以用官方安装脚本来完成curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh安装完成后把当前用户加进 docker 组避免每次都要 sudosudo usermod -aG docker $USER newgrp docker然后查一下版本确认安装成功docker version docker compose version如果你在国内网络环境下装完之后第一个动作应该是配置镜像加速器也被一些人称为国内镜像源。这跟系统换清华源是同一个思路把拉取镜像的默认仓库改到国内可快速访问的地址。具体地址因服务商的可用性而异这里不写死。配置方法是在/etc/docker/daemon.json里增加 registry-mirrors 选项然后重启 Docker 服务sudo systemctl restart docker配置好加速器之后再去拉nginx:alpine这类镜像速度会明显好很多。这个步骤在树莓派上几乎是必做的否则拉一个镜像卡上十分钟是常事。5.2 跑起第一个容器并理解这条命令的每个部分装好 Docker 之后第一条命令就干点有仪式感的事起一个 Nginx 容器。docker run -d --name web -p 8080:80 nginx:alpine拆开来看每个部分都是有意义的参数作用docker run创建并启动一个新容器-d后台运行退出终端容器也不会停--name web给容器命名为 web方便后续管理-p 8080:80宿主机 8080 端口映射到容器 80 端口nginx:alpine使用 Nginx 的 alpine 版本镜像体积小跑起来之后在浏览器里访问http://树莓派IP:8080看到 Nginx 默认欢迎页第一个容器就算跑通了。接下来把页面换成自己的内容。先建一个目录放网页文件然后把它挂载进容器mkdir -p ~/web echo h1Hello Raspberry Pi/h1 ~/web/index.html docker run -d --name web -p 8080:80 \ -v ~/web:/usr/share/nginx/html:ro \ nginx:alpine这里-v就是在做“绑定挂载”把宿主机~/web目录挂到容器的网站根目录:ro表示只读避免容器内部篡改宿主文件。刷新浏览器看到自己写的页面说明挂载生效了。5.3 体验一下“删除重建”的魔力跑通之后做一个实验把容器删掉再重新创建感受一下容器化部署和传统部署的差别。docker rm -f web docker run -d --name web -p 8080:80 \ -v ~/web:/usr/share/nginx/html:ro \ nginx:alpine第二遍执行完打开浏览器页面内容还在。整个删除重建的过程只需要几秒钟因为数据在宿主机挂载目录里容器本身只是一层只读镜像加一个可写层。想升级 Nginx 版本把镜像标签改成新版本重新 run 一次就行不用像以前那样apt upgrade搞整个系统。这就是容器化最直观的价值环境永远是那个干净的环境需要变动的只有数据和配置。5.4 树莓派 Docker 新手避坑清单最后整理几条我自己踩过、或者看别人踩过的坑希望能帮你少走弯路。镜像拉取慢先配加速器再继续。在树莓派上首次拉镜像如果卡住不动90% 是网络问题。先去配好镜像加速器再执行拉取操作体验完全不一样。不要给容器开特权模式。--privileged和--network host这些参数能不用就不用。树莓派设备经常接触底层硬件但普通网页服务器容器根本不需要这些权限开了反而增加风险。注意日志文件的增长。容器的日志默认全部写到宿主机的/var/lib/docker/containers下跑久了可能占掉大量 SD 卡空间。可以在启动参数里限制日志大小--log-opt max-size10m --log-opt max-file3。不要进容器手动改配置。容器是临时实体删了重建就会恢复原状。想改配置正确的做法是挂载配置文件或者用环境变量传参。我在早期犯过好几次“改了配置、一重启就没”的错误。SD 卡空间要提前规划。Docker 镜像和容器数据默认都放在/var/lib/docker如果 SD 卡分区不大跑几个镜像就可能报警。建议把 Docker 数据目录迁移到大容量存储或者至少定期用docker image prune -a清理不用的镜像。最后再分享一点个人体会从那次重刷系统之后我给自己定了一条规矩树莓派系统镜像只负责最基础的功能任何额外服务一律容器化。运行到现在这台树莓派再也没有出现过因为装新服务导致旧服务崩溃的情况。有时候想试一个新软件直接docker run跑一个容器不满意就docker rm系统一点痕迹都不会留。这种感觉跟以前“装一个软件就像在系统里做一次手术”的状态比起来完全不在一个量级。如果你也正在用树莓派跑网页服务器并且开始觉得环境越来越乱、服务越来越难维护那么现在就是切换到 Docker 思路的最佳时机。先别急着把现有系统推倒重来可以从下一个新服务开始用容器方式跑体验几次“删除重建”的快感慢慢就会理解它的价值。下一篇我会写如何用 Docker Compose 把 Nginx、PHP 和数据库组合起来跑一个完整的动态网页服务到时候见。
返回列表