器實(shí)戰(zhàn))
1. 為什么這篇要把全家桶整合當(dāng)成重頭戲樹莓派玩 Docker玩到第七篇很多東西已經(jīng)不是能不能跑的問題而是怎么跑得省心的問題。系列前面的文章分別處理了容器化 Nginx、數(shù)據(jù)庫容器化、存儲掛載、日志方案、反向代理與端口規(guī)劃這些單個模塊。每一塊單獨(dú)拆開看都能在樹莓派上穩(wěn)定運(yùn)行但真正把它們當(dāng)成一個完整的網(wǎng)頁服務(wù)器來使用時(shí)你會發(fā)現(xiàn)一個很現(xiàn)實(shí)的問題手動逐條執(zhí)行docker run去管理六個、八個甚至十幾個容器本身就是一種體力活而且特別容易出錯。這篇文章要做的就是把前幾篇拆散的零件重新組裝起來從一個完全純凈的樹莓派系統(tǒng)開始到 Docker 引擎就緒再到一套完整的網(wǎng)頁服務(wù)器容器全家桶通過一份docker-compose.yml編排好最后用一個部署腳本實(shí)現(xiàn)一條命令從零啟動整個服務(wù)集群。整個流程覆蓋了系統(tǒng)燒錄、基礎(chǔ)環(huán)境初始化、Docker 安裝與加速配置、鏡像規(guī)劃、容器編排、數(shù)據(jù)持久化、健康檢查與故障排查。這套方案適合誰如果你手里有一塊樹莓派 4B2GB 內(nèi)存以上版本想讓它承擔(dān)真實(shí)的網(wǎng)頁服務(wù)任務(wù)——不只是玩一個 Hello World 容器——同時(shí)又不想每次重啟或者搬家式遷移時(shí)手動敲十幾個docker run這篇文章就是給你準(zhǔn)備的。做完之后的體驗(yàn)大概就是TF 卡一插、上電、等幾分鐘整個服務(wù)棧自動拉起完全不需要盯屏幕輸命令。2. 純凈底座準(zhǔn)備從一張空白 TF 卡到可用的 Docker 主機(jī)2.1 系統(tǒng)鏡像選型64 位是底線先聊最容易被忽略但影響最大的決定操作系統(tǒng)鏡像。樹莓派 4B 的 CPU 是 Cortex-A7264 位架構(gòu)但很多教程還在推薦 32 位的 Raspberry Pi OS。如果是做桌面娛樂或者 GPIO 實(shí)驗(yàn)32 位問題不大但要做 Docker 化服務(wù)器直接上 64 位系統(tǒng)。原因很簡單Docker Hub 上大量的官方鏡像已經(jīng)以arm64為主推架構(gòu)你用 32 位系統(tǒng)拉arm/v7版本也不是不行但很多鏡像的arm/v7構(gòu)建已經(jīng)進(jìn)入維護(hù)甚至停止更新狀態(tài)。比如 MySQL 8.x 的官方鏡像對arm/v7的支持明顯沒有arm64積極。另一個現(xiàn)實(shí)問題是樹莓派 4B 的 4GB 內(nèi)存版本搭配 32 位系統(tǒng)時(shí)單個進(jìn)程可尋址內(nèi)存受限跑數(shù)據(jù)庫或者構(gòu)建鏡像時(shí)總覺得憋屈。我推薦兩個方案發(fā)行版優(yōu)點(diǎn)缺點(diǎn)適用場景Raspberry Pi OS Lite64位官方維護(hù)、驅(qū)動最穩(wěn)、社區(qū)資料最多軟件包版本偏保守大多數(shù)人的首選Ubuntu Server for Raspberry Pi64位內(nèi)核較新、軟件包新、Docker 兼容性極好驅(qū)動偶爾有小問題想緊跟新特性、跑較新容器鏡像的人我個人更傾向于 Ubuntu Server。原因是 Docker 官方安裝腳本對 Ubuntu 的適配最成熟遇到問題時(shí)排查資料也更多。但如果你對 Raspberry Pi OS 更熟悉Lite 版完全夠用只要確定下載的是 64 位鏡像。2.2 燒錄與開機(jī)初始化頭十分鐘決定后面順不順利燒錄環(huán)節(jié)官方出的 Raspberry Pi Imager 是目前最省事的工具。它現(xiàn)在支持直接在燒錄時(shí)預(yù)設(shè) SSH 開關(guān)、用戶名密碼、Wi-Fi 信息這一步非常關(guān)鍵——省去了無頭模式下必須接顯示器和鍵鼠才能開 SSH 的尷尬。具體操作打開 Raspberry Pi Imager選擇鏡像選 64 位 Lite 或 Ubuntu Server。選擇 TF 卡建議 32GB 以上16GB 裝完系統(tǒng)加幾個鏡像就捉襟見肘。點(diǎn)擊右下角設(shè)置按鈕提前寫入開啟 SSH允許密碼登錄或配置公鑰指定主機(jī)名比如rpisrv設(shè)置用戶pi的密碼配置 Wi-Fi如果走無線或者直接插網(wǎng)線燒錄完成后TF 卡插入樹莓派上電等一兩分鐘。上電后第一步是找到樹莓派的 IP。最簡單的方法是在路由器后臺看 DHCP 客戶端列表或者用ping rpisrv.local如果網(wǎng)絡(luò)支持 mDNS。連上 SSH 后先做三件事改源、更新系統(tǒng)、檢查固件。改源這件事很多人教條式地照抄網(wǎng)上的代碼塊結(jié)果換完發(fā)現(xiàn)源和目標(biāo)系統(tǒng)對不上。Raspberry Pi OS 和 Ubuntu 的源格式不同Debian 系里又分 bookworm、bullseye、noble、jammy 等版本代號抄錯了輕則更新慢重則直接apt update報(bào)錯。正確做法是先看系統(tǒng)版本文件cat /etc/os-release lsb_release -cs # 看代號然后根據(jù)代號去配對應(yīng)國內(nèi)鏡像站的源。這一步做完apt update和apt upgrade的速度會有質(zhì)的提升后續(xù)裝任何軟件都不至于卡在下載上。系統(tǒng)更新完重啟一次然后進(jìn)入固件配置sudo raspi-config需要動的地方只有兩處一是Advanced Options - Expand Filesystem雖然新版本系統(tǒng)鏡像默認(rèn)會自動擴(kuò)容但確認(rèn)一下沒有壞處二是如果不需要桌面環(huán)境確保 boot 到命令行模式。另外建議把 GPU 內(nèi)存調(diào)低到 16MBPerformance Options - GPU Memory這臺機(jī)器如果純做服務(wù)器GPU 完全用不上省下的內(nèi)存給容器更劃算。2.3 基礎(chǔ)軟件包與目錄規(guī)劃系統(tǒng)就緒后裝一些后面會用到的工具。這一步不需要太多夠用就好sudo apt update sudo apt upgrade -y sudo apt install -y curl wget git vim net-tools htop目錄規(guī)劃這里我先說一個超出常規(guī)教程的思路不要把數(shù)據(jù)直接散落在~/或者/opt里而是建立一個獨(dú)立的目錄結(jié)構(gòu)把應(yīng)用代碼、容器配置、數(shù)據(jù)卷、備份全部歸類放好。我習(xí)慣在樹莓派上用這個布局/srv/server/ ├── apps/ │ ├── nginx/ │ ├── mysql/ │ ├── redis/ │ └── monitor/ ├── compose/ │ └── docker-compose.yml ├── scripts/ │ └── deploy.sh └── backups//srv是 Linux 中專門給服務(wù)數(shù)據(jù)的目錄語義清晰也不容易被誤清理。這套目錄結(jié)構(gòu)會和后面的整個方案綁定后續(xù)所有路徑映射都以這里為根。3. Docker 引擎與鏡像加速ARM 平臺最容易翻車的地方3.1 Docker Engine 安裝不走 Docker Desktop 路線樹莓派上不要裝 Docker Desktop它面向的是桌面環(huán)境且需要額外的虛擬機(jī)層。我們要的是純粹的 Docker Engine 加 Compose 插件。安裝方式我推薦用 Docker 官方腳本簡單直接curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh腳本會自動識別系統(tǒng)架構(gòu)和發(fā)行版把 Docker Engine、containerd、Compose 插件一起裝好。裝完把用戶加入 docker 組省得每次敲sudosudo usermod -aG docker $USER newgrp docker驗(yàn)證安裝docker --version docker compose version一個容易被忽略的點(diǎn)docker compose帶空格v2 插件和docker-compose帶橫線v1 獨(dú)立二進(jìn)制是兩回事。新版 Docker 官方推薦用前者。網(wǎng)上大量舊教程還在要求你apt install docker-compose裝出來的是一個老版本 Python 實(shí)現(xiàn)不僅語法支持不全性能也差一截。請認(rèn)準(zhǔn)docker compose命令。3.2 鏡像加速配置讓拉取速度回歸正常這一步在國內(nèi)網(wǎng)絡(luò)環(huán)境下幾乎是必修課。樹莓派在國內(nèi)用默認(rèn) Docker Hub 拉鏡像速度經(jīng)常讓人懷疑 SD 卡是不是壞了。配置鏡像加速器的原理不復(fù)雜Docker 在拉鏡像時(shí)會先訪問daemon.json里配置的 registry mirror這些加速器相當(dāng)于 Docker Hub 的緩存節(jié)點(diǎn)。配置文件位置在/etc/docker/daemon.json如果不存在就新建一個{ registry-mirrors: [ https://docker.m.daocloud.io, https://dockerproxy.com, https://docker.nju.edu.cn ], log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 }, data-root: /var/lib/docker }這里同時(shí)做了一件非常重要的事日志限額配置。樹莓派的 TF 卡空間有限容器如果持續(xù)輸出日志默認(rèn)情況下/var/lib/docker/containers/xxx/xxx-json.log會無限增長。曾經(jīng)見過一個跑了半年 Nginx 的容器日志文件膨脹到 7GB 直接寫滿 TF 卡。限定單個日志文件 10MB、最多保留 3 份基本保證日志不會成為容量殺手。配置完重啟 Dockersudo systemctl restart docker sudo systemctl enable docker # 開機(jī)自啟enable這一步值得強(qiáng)調(diào)。樹莓派經(jīng)常因?yàn)橥k?、誤碰電源等原因意外關(guān)機(jī)如果 Docker 服務(wù)沒有設(shè)為自啟重啟之后全家桶不會自動回來等你 SSH 上去才發(fā)現(xiàn)在裸奔。3.3 ARM 平臺的鏡像拉取細(xì)節(jié)樹莓派 4B 裝 64 位系統(tǒng)后Docker 默認(rèn)拉取arm64鏡像。大多數(shù)情況下你不需要手動指定平臺。但有一個場景會踩坑某些鏡像只有amd64版本或者開發(fā)者只發(fā)布了linux/arm/v7沒有arm64。遇到這種情況Docker 會嘗試通過模擬運(yùn)行amd64容器性能會打折扣甚至直接失敗。我的建議是在選擇鏡像時(shí)養(yǎng)成看架構(gòu)標(biāo)簽的習(xí)慣。Docker Hub 上每個鏡像的 Tags 頁面都能看到不同架構(gòu)的 digest。如果你不確定某個鏡像是否支持arm64可以用docker manifest inspect快速查看docker manifest inspect mysql:8.0 | grep architecture這個方法不實(shí)際拉取鏡像就能看到支持的架構(gòu)列表選鏡像前花十秒鐘檢查一下能避免啟動之后才發(fā)現(xiàn)鏡像不兼容的尷尬。4. 全家桶陣容規(guī)劃網(wǎng)頁服務(wù)器周邊到底需要哪些容器4.1 我的全家桶組成與選型邏輯一套能承擔(dān)真實(shí)業(yè)務(wù)的網(wǎng)頁服務(wù)器顯然不可能只有 Nginx 一個容器。結(jié)合樹莓派的硬件條件4 核 CPU、4GB 內(nèi)存我最終定下的全家桶陣容是這樣容器鏡像作用端口內(nèi)存占用實(shí)測nginxnginx:stable-alpine前端入口反向代理80/443約 40MBmysqlmysql:8.0業(yè)務(wù)數(shù)據(jù)庫3306僅內(nèi)網(wǎng)約 280MBredisredis:7-alpine緩存、會話6379僅內(nèi)網(wǎng)約 60MBportainerportainer/portainer-ce:latestDocker 可視化管理9000內(nèi)網(wǎng)約 80MBwatchtowercontainrrr/watchtower:latest容器鏡像自動更新無約 25MBcadvisorgcr.io/cadvisor/cadvisor:latest容器資源監(jiān)控8080內(nèi)網(wǎng)約 100MB為什么選這幾個而不是更多我在規(guī)劃時(shí)有一個鐵律樹莓派上的容器數(shù)量寧少勿多。每一個容器背后都是一個常駐進(jìn)程都有內(nèi)存開銷和日志寫入。2GB 內(nèi)存版本如果硬上 Elasticsearch 全家桶Kafka啟動階段就能把 swap 吃爆。我的 4GB 版本跑這套陣容系統(tǒng)加容器整體內(nèi)存占用約 1.8GB還有一半余量這算是一個比較健康的水平。鏡像選型也有講究Nginx 用alpine版本體積小、攻擊面小MySQL 用官方8.0而不是latest因?yàn)閘atest的 tag 可能隨著大版本升級而漂移到時(shí)候自動更新一跑數(shù)據(jù)庫從 8.0 跳到了 8.4兼容性問題會讓人措手不及。固定主版本號是數(shù)據(jù)庫類容器的基本素養(yǎng)。4.2 為什么非要用 Portainer管理員的理智選擇有人會說命令行本來就能管理 Docker為什么非要加一個 Portainer我的回答是樹莓派服務(wù)器不是只服務(wù)你一個人。部署完一個完整系統(tǒng)之后如果家里其他人也想看服務(wù)狀態(tài)、重啟某個出問題的容器你不可能讓每個人都去學(xué)docker ps和docker logs。Portainer 提供的圖形界面讓整個管理過程變得幾乎是零門檻。另外Portainer 的 Stacks 功能原生支持 docker-compose可以直接用圖形界面管理整套 Stack 的啟停和更新和本文后面要寫的部署腳本不沖突——腳本負(fù)責(zé)首次部署Portainer 負(fù)責(zé)日常運(yùn)維。這種腳本初始化 面板管理的組合拳是我試出來的最佳配合。4.3 內(nèi)存預(yù)算與 Swap 策略4GB 內(nèi)存看著不小但 MySQL 默認(rèn)配置能吃掉大量內(nèi)存去做 buffer pool。樹莓派上必須對 MySQL 做內(nèi)存約束。用 Docker 的方式可以通過command參數(shù)覆蓋 MySQL 的啟動配置command: --innodb-buffer-pool-size256M --max-connections50 --performance-schemaOFFmax-connections從默認(rèn)的 151 降到 50對家庭級應(yīng)用綽綽有余performance-schema關(guān)閉能省掉約 200MB 內(nèi)存開銷。這是樹莓派上跑 MySQL 最值得做的兩個調(diào)整。Swap 方面樹莓派實(shí)際跑這套方案時(shí)內(nèi)存峰值可能沖到 2.5GB 左右剩余空間已經(jīng)不太從容。建議在系統(tǒng)層面加 2GB 的 swapfile防止瞬時(shí)內(nèi)存尖峰直接把進(jìn)程殺掉。開啟后free -h能看到 swap 總量變成 2GB 以上。注意把vm.swappiness設(shè)小一點(diǎn)比如 10讓系統(tǒng)優(yōu)先用物理內(nèi)存swap 只兜底。5. docker-compose 編排把容器依賴關(guān)系和數(shù)據(jù)持久化理清楚5.1 compose 文件的核心結(jié)構(gòu)所有容器最終通過一份docker-compose.yml統(tǒng)一管理放在/srv/server/compose/下。這是全家桶整合的核心文件我直接給出完整內(nèi)容并逐段說明services: nginx: image: nginx:stable-alpine container_name: web-nginx restart: always ports: - 80:80 - 443:443 volumes: - /srv/server/apps/nginx/conf.d:/etc/nginx/conf.d:ro - /srv/server/apps/nginx/html:/usr/share/nginx/html:ro - /srv/server/apps/nginx/logs:/var/log/nginx - /srv/server/apps/nginx/cert:/etc/nginx/cert:ro networks: - webnet depends_on: - mysql - redis healthcheck: test: [CMD, wget, -q, --spider, http://localhost/healthz] interval: 30s timeout: 5s retries: 3 start_period: 10s mysql: image: mysql:8.0 container_name: web-mysql restart: always command: --innodb-buffer-pool-size256M --max-connections50 --character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci environment: MYSQL_ROOT_PASSWORD: 換成強(qiáng)密碼 MYSQL_DATABASE: webapp MYSQL_USER: webuser MYSQL_PASSWORD: 換成強(qiáng)密碼 TZ: Asia/Shanghai volumes: - /srv/server/apps/mysql/data:/var/lib/mysql networks: - webnet healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -u, root, -p換成root密碼] interval: 30s timeout: 5s retries: 5 redis: image: redis:7-alpine container_name: web-redis restart: always command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru volumes: - /srv/server/apps/redis/data:/data networks: - webnet healthcheck: test: [CMD, redis-cli, ping] interval: 30s timeout: 5s retries: 3 portainer: image: portainer/portainer-ce:latest container_name: web-portainer restart: always ports: - 9000:9000 volumes: - /var/run/docker.sock:/var/run/docker.sock - /srv/server/apps/portainer/data:/data networks: - webnet watchtower: image: containrrr/watchtower:latest container_name: web-watchtower restart: always environment: WATCHTOWER_CLEANUP: true WATCHTOWER_SCHEDULE: 0 0 4 * * * WATCHTOWER_TIMEOUT: 30s volumes: - /var/run/docker.sock:/var/run/docker.sock networks: - webnet cadvisor: image: gcr.io/cadvisor/cadvisor:latest container_name: web-cadvisor restart: always ports: - 8080:8080 volumes: - /:/rootfs:ro - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker:/var/lib/docker:ro - /cgroup:/cgroup:ro networks: - webnet networks: webnet: driver: bridge5.2 網(wǎng)絡(luò)模式選擇為什么統(tǒng)一橋接到 webnet這里有一個前面系列文章已經(jīng)詳細(xì)拆解過、但值得再強(qiáng)調(diào)的設(shè)計(jì)所有容器放在同一個自定義 bridge 網(wǎng)絡(luò)webnet中并且只有 Nginx、Portainer、cAdvisor 對外開放端口。這個設(shè)計(jì)帶來三個直接好處。第一MySQL 和 Redis 不暴露宿主機(jī)端口外部無法直接訪問數(shù)據(jù)庫降低被掃描攻擊的風(fēng)險(xiǎn)。第二Nginx 可以通過容器名直接訪問 MySQL 和 Redis比如 PHP 應(yīng)用里配置數(shù)據(jù)庫主機(jī)名直接寫mysql而不是 IPDocker 內(nèi)置 DNS 會處理解析。第三端口沖突問題徹底消失不需要為每個容器手動規(guī)劃一個非標(biāo)準(zhǔn)宿主機(jī)端口。如果你想過用network_mode: host來獲得更好性能在樹莓派上其實(shí)沒必要。bridge 網(wǎng)絡(luò)模式在 Docker 內(nèi)部的性能損耗非常小而 host 模式會帶來端口沖突風(fēng)險(xiǎn)甚至可能和系統(tǒng)自身的 80 端口搶占。除非遇到極端性能需求一律用 bridge 加自定義網(wǎng)絡(luò)。5.3 數(shù)據(jù)持久化volume 和 bind mount 的使用邊界在這套方案里我用了兩種不同的掛載方式使用邊界很明確bind mount直接映射宿主機(jī)目錄用于需要直接查看、修改、拷貝的數(shù)據(jù)。Nginx 的 HTML 目錄、配置目錄、證書目錄用 bind mount因?yàn)槟憧赡芤?jīng)常直接編輯文件MySQL 數(shù)據(jù)目錄也用 bind mount方便備份時(shí)直接tar整個目錄。named volumeDocker 管理卷適合不想直接碰、只由容器讀寫的數(shù)據(jù)。如果某個服務(wù)的數(shù)據(jù)對宿主機(jī)透明性要求低應(yīng)該用 named volume。比如 Portainer 的數(shù)據(jù)目錄雖然寫成了 bind mount但其實(shí)用 named volume 會更好因?yàn)樗臄?shù)據(jù)目錄內(nèi)部結(jié)構(gòu)很復(fù)雜沒有直接操作的需求bind mount 還可能帶來文件權(quán)限和 SELinux 方面的小問題。MySQL 數(shù)據(jù)目錄用 bind mount 時(shí)需要注意權(quán)限容器內(nèi)的mysql用戶和宿主機(jī)的uid/gid并不一致直接映射后經(jīng)常出現(xiàn)Permission denied。解決方案是在掛載前手動設(shè)置目錄所有者sudo mkdir -p /srv/server/apps/mysql/data sudo chown -R 999:999 /srv/server/apps/mysql/data999 是 MySQL 官方鏡像中mysql用戶的 uid。這個細(xì)節(jié)能避免首次啟動時(shí)報(bào) Cant open file 的尷尬。6. 一鍵部署腳本從手動拉鏡像到一條命令收工6.1 部署腳本要覆蓋的完整階段compose 文件寫好后剩下的事情就是寫一個一鍵部署腳本。這個腳本的意義在于面對一臺全新的樹莓派只需要執(zhí)行一條命令就能完成環(huán)境檢查、目錄初始化、配置校驗(yàn)、拉鏡像、啟動服務(wù)、健康檢查的完整流程。腳本我放在/srv/server/scripts/deploy.sh整體結(jié)構(gòu)分六個階段。這里我按階段拆開講每個階段都有獨(dú)立函數(shù)方便出了問題單獨(dú)執(zhí)行。階段一環(huán)境預(yù)檢。腳本跑起來先檢查系統(tǒng)架構(gòu)、Docker 是否可用、端口是否被占用check_environment() { echo [1/6] 檢查運(yùn)行環(huán)境 arch$(uname -m) if [ $arch ! aarch64 ] [ $arch ! armv7l ]; then echo 錯誤非 ARM 架構(gòu)腳本僅適用于樹莓派 exit 1 fi if ! command -v docker /dev/null 21; then echo 錯誤Docker 未安裝請先完成 Docker 引擎安裝 exit 1 fi if ! docker compose version /dev/null 21; then echo 錯誤Docker Compose 插件未安裝 exit 1 fi echo 環(huán)境檢查通過架構(gòu) $archDocker 已安裝 }階段二目錄初始化。把整個/srv/server目錄結(jié)構(gòu)在當(dāng)前機(jī)器上補(bǔ)齊init_dirs() { echo [2/6] 初始化目錄結(jié)構(gòu) mkdir -p /srv/server/{apps/{nginx/{conf.d,html,logs,cert},mysql/data,redis/data,portainer/data},compose,scripts,backups} if [ ! -d /srv/server/apps/mysql/data ]; then echo MySQL 數(shù)據(jù)目錄不存在開始創(chuàng)建 mkdir -p /srv/server/apps/mysql/data chown -R 999:999 /srv/server/apps/mysql/data fi if [ ! -d /srv/server/apps/redis/data ]; then mkdir -p /srv/server/apps/redis/data chown -R 999:999 /srv/server/apps/redis/data fi echo 目錄結(jié)構(gòu)就緒 }階段三配置文件生成。如果 Nginx 的conf.d里還沒有默認(rèn)站點(diǎn)配置腳本順手生成一份最小可用的反向代理配置讓它能轉(zhuǎn)發(fā)到webapp容器假設(shè)業(yè)務(wù)應(yīng)用跑在另一個容器里。這一步的邏輯是不要假設(shè)每一臺新機(jī)器上都已經(jīng)有完整配置部署腳本要能在零配置的前提下生成一份可運(yùn)行的最小配置后續(xù)再手動覆蓋。generate_nginx_conf() { echo [3/6] 生成 Nginx 基礎(chǔ)配置 if [ ! -f /srv/server/apps/nginx/conf.d/default.conf ]; then cat /srv/server/apps/nginx/conf.d/default.conf EOF server { listen 80; server_name _; location / { proxy_pass http://webapp:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } EOF fi echo Nginx 配置生成完畢 }階段四拉取鏡像。手動觸發(fā)docker compose pull提前把所有鏡像拉到本地。這一步雖然up也會自動拉但單獨(dú)跑一次的好處是錯誤信息更清晰可以逐鏡像排查拉取失敗的原因。階段五啟動服務(wù)。用--wait參數(shù)等待 compose 服務(wù)完全啟動start_services() { echo [5/6] 啟動服務(wù)棧 cd /srv/server/compose docker compose pull docker compose up -d --wait }--wait是 Docker Compose v2 的一個實(shí)用特性它會阻塞直到所有配置了 healthcheck 的服務(wù)通過健康檢查。這樣腳本可以在下一步判斷服務(wù)是否真正就緒而不是只看到容器啟動了。階段六輸出訪問清單。腳本跑完后把服務(wù)地址、需要手動進(jìn)行的后續(xù)步驟列出來省得部署完還要去翻筆記回憶端口。這一步雖然簡單但在實(shí)際使用中非常提升體驗(yàn)——尤其是在樹莓派 IP 變化后腳本輸出能直接看到當(dāng)前機(jī)器的所有訪問入口。6.2 冪等性設(shè)計(jì)重復(fù)執(zhí)行不踩雷部署腳本最容易被人忽視的需求是冪等性。什么叫冪等就是同一個腳本在同一臺機(jī)器上執(zhí)行兩次、三次結(jié)果都正確不會因?yàn)橹貜?fù)執(zhí)行導(dǎo)致數(shù)據(jù)被覆蓋、目錄被重建、配置被重置。我在這份腳本里做了三處冪等保護(hù)目錄創(chuàng)建使用mkdir -p目錄已存在時(shí)靜默跳過不會報(bào)錯。Nginx 配置只在文件不存在時(shí)生成再次執(zhí)行不會覆蓋已有配置。MySQL 數(shù)據(jù)目錄的所有者修改只執(zhí)行一次如果目錄已經(jīng)存在且所有者正確chown不會造成額外影響。為什么要做冪等因?yàn)椴渴鹉_本不只是新裝機(jī)用的。你在日常使用中改了某一個容器的環(huán)境變量或者調(diào)整了 compose 文件里的某個卷映射會想重新執(zhí)行一遍部署腳本讓改動生效。如果腳本不具備冪等性第二次執(zhí)行可能就是一場災(zāi)難——目錄會被重建配置文件會被重置數(shù)據(jù)目錄權(quán)限可能會變得混亂。6.3 健康檢查與自檢邏輯Compose 文件里我們已經(jīng)給每個有檢測能力的服務(wù)配置了healthcheck。部署腳本的最后一階段就是對健康檢查結(jié)果做匯總判斷。一個實(shí)用的自檢函數(shù)長這樣check_health() { echo 服務(wù)健康狀態(tài)匯總 failed0 for container in web-nginx web-mysql web-redis web-portainer; do status$(docker inspect --format{{.State.Health.Status}} $container 2/dev/null || echo status: not found) if [ $status healthy ]; then echo $container: healthy else echo $container: $status failed1 fi done if [ $failed -eq 0 ]; then echo 所有服務(wù)健康檢查通過 else echo 存在異常服務(wù)請檢查 docker logs container_name 查看原因 exit 1 fi }這段腳本的價(jià)值在于它把容器在跑和服務(wù)可用區(qū)分開。一個容器進(jìn)程雖然活著但內(nèi)部的應(yīng)用可能還在啟動中或者已經(jīng)崩潰重啟。通過健康檢查判斷比單純看docker ps輸出更可靠。有時(shí)候docker inspect查出來的健康狀態(tài)會是starting這通常意味著start_period時(shí)間還不夠等服務(wù)內(nèi)部程序完全就緒后會變healthy。7. 實(shí)測與排障全家桶跑起來后遇到的真實(shí)問題7.1 啟動耗時(shí)與資源占用實(shí)測這套方案在樹莓派 4B4GB 版本Ubuntu Server 24.04TF 卡為 A2 規(guī)格上的表現(xiàn)我做了完整的實(shí)測記錄。首次部署需要拉取全部鏡像總耗時(shí)約 12 分鐘其中鏡像拉取占大頭MySQL 8.0 鏡像約 150MB在加速器正常情況下兩分鐘內(nèi)能拉完。后續(xù)啟動鏡像已緩存從執(zhí)行部署腳本到所有服務(wù)healthy實(shí)測約 55 秒。主要等待在于 MySQL 的初始化和健康檢查通過需要時(shí)間。內(nèi)存占用的真實(shí)分布總內(nèi)存 3.8GB4GB 減掉 GPU 預(yù)留 系統(tǒng)基礎(chǔ)進(jìn)程約 300MB Docker 各容器約 1.1GB - 1.3GB 緩存與余量約 2.2GB一次正常的業(yè)務(wù)高峰并發(fā) 20 個請求打 Nginx同時(shí)讀寫 MySQLCPU 使用率在 30%~50% 之間空閑時(shí)幾乎為 0溫度穩(wěn)定在 52°C 左右前提是加了一個幾十塊的鋁制散熱殼。如果你的樹莓派還是裸板加小散熱片建議跑這套方案前先把散熱問題解決MySQL 在高溫下會明顯降速。7.2 三次典型故障的完整排查鏈路好消息是這套方案穩(wěn)定跑了大半年壞消息是過程中踩過三個值得記錄的坑每個都花了不少時(shí)間定位。故障一重啟后 MySQL 起不來報(bào)錯指向數(shù)據(jù)目錄權(quán)限?,F(xiàn)象是樹莓派斷電重啟后docker compose up -d后 MySQL 容器一直在restarting狀態(tài)docker logs web-mysql看到[ERROR] Cant open file /var/lib/mysql/ibdata1 ... Permission denied。排查過程先查掛載目錄的所有者ls -ln /srv/server/apps/mysql/data發(fā)現(xiàn)數(shù)據(jù)目錄的所有者變成了1000宿主機(jī)第一個用戶的 uid。原因是我后來用rsync從舊 SD 卡遷移數(shù)據(jù)時(shí)tar 解包覆蓋了原有的所有權(quán)信息。修復(fù)方式是重新執(zhí)行chown -R 999:999 /srv/server/apps/mysql/data。這個坑讓我意識到一個經(jīng)驗(yàn)bind mount 目錄的權(quán)限信息是部署容器的隱藏依賴任何涉及目錄遷移的操作之后必須檢查并恢復(fù)所有者和權(quán)限。故障二Portainer 和 cAdvisor 容器內(nèi)能看到 Docker sock 掛載但界面無數(shù)據(jù)?,F(xiàn)象是 Portainer 可以連接但容器資源使用圖表全部空白cAdvisor 能訪問但指標(biāo)頁面報(bào)錯。排查發(fā)現(xiàn)兩個容器都掛載了/var/run/docker.sock但 cAdvisor 還需要額外掛載宿主機(jī)的根目錄、sys 目錄、cgroup 目錄才能采集到資源指標(biāo)。compose 文件里漏掉了/sys和根目錄映射導(dǎo)致數(shù)據(jù)采集失效。補(bǔ)齊卷映射后一切正常。這個問題的教訓(xùn)是某些容器的資源監(jiān)控類功能不僅依賴 Docker API還依賴宿主機(jī)文件系統(tǒng)的直接訪問掛載不全時(shí)功能會靜默降級不會直接報(bào)錯。故障三Watchtower 自動更新把 MySQL 從 8.0 拉到了 8.4。現(xiàn)象是某天早上發(fā)現(xiàn)業(yè)務(wù)報(bào)錯數(shù)據(jù)庫連不上。查日志發(fā)現(xiàn) watchtower 夜里自動拉取了 MySQL 鏡像的 latest tag然后重建了容器導(dǎo)致原本跑在 8.0 上的數(shù)據(jù)目錄被 8.4 識別時(shí)出現(xiàn)了不兼容問題。這個坑的根源在我的 compose 文件里image: mysql:8.0這個 tag 雖然指定了主版本但 watchtower 仍然會檢查這個 tag 指向的最新 digest只要官方把 8.0 系列的基礎(chǔ)鏡像重新構(gòu)建它就會拉下來替換。修復(fù)方案有兩種一是給 watchtower 配置WATCHTOWER_LABEL_ENABLEtrue然后在敏感容器上加com.centurylinklabs.watchtower.enablefalse標(biāo)簽二是干脆不用 watchtower 更新數(shù)據(jù)庫類容器只讓它更新 Nginx 這類無狀態(tài)服務(wù)。我最終選擇了后者數(shù)據(jù)庫這種有狀態(tài)服務(wù)的更新必須人工介入評估。7.3 性能調(diào)優(yōu)與日常維護(hù)的幾個小參數(shù)跑穩(wěn)定之后我做了幾處水磨工夫的調(diào)優(yōu)每項(xiàng)改動都不大但日積月累對 TF 卡壽命和系統(tǒng)穩(wěn)定性幫助很明顯。一是 Docker 數(shù)據(jù)目錄不要放在根分區(qū)。TF 卡的根分區(qū)空間本就有限鏡像層、日志、容器可寫層全都堆在/var/lib/docker里時(shí)間長了容易把根分區(qū)塞滿。如果你的樹莓派掛了一塊移動硬盤或者 SSD把>0 3 * * 1 docker image prune -f docker container prune -f注意不要用docker system prune -a那是無差別清理所有未使用的鏡像會把本地構(gòu)建但暫時(shí)沒跑的鏡像也刪掉。文章寫到這里從系統(tǒng)底座、Docker 引擎、鏡像規(guī)劃、Compose 編排到一鍵部署腳本和排障復(fù)盤一套完整流程已經(jīng)擺在面前。套用我在實(shí)際使用中養(yǎng)成的一個習(xí)慣每次新裝一套機(jī)器跑完部署腳本后立刻把腳本和 compose 文件備份到 U 盤或者云盤一份。樹莓派壞卡換新卡恢復(fù)到可用狀態(tài)只需要燒錄系統(tǒng)、執(zhí)行腳本、等完健康檢查三件事。