指南)
早上九點剛過監(jiān)控群里彈出一條告警某臺業(yè)務機的根分區(qū)使用率到了 93%。登上去一看/var/lib/docker這一個目錄就吃掉了一百二十多個 G而真正跑著的容器加起來不到三個。這不是我第一次碰到這種場面也不會是最后一次。Docker 用久了體積膨脹本質上是個記賬問題鏡像層、容器可寫層、日志文件、構建緩存、命名卷每一筆都有自己的存放位置和生命周期你不管它它就一直在那兒躺著。這篇內容我打算把四件事一次講透怎么在不誤刪重要數據的前提下把空間清出來、哪些東西必須先備份再動手、備份怎么恢復回來、Docker 的壓縮能力到底壓的是什么、在哪些環(huán)節(jié)用得上、以及把整個數據目錄搬到另一塊大盤上的完整操作鏈路。剛接觸 Docker 的朋友可以照著命令一步步來已經在一線維護機器的同行也能在里面的排查鏈路和踩坑記錄里找到點參考。1. 先定位再動手那上百個 G 究竟藏在 Docker 的哪個角落清理這件事最容易犯的錯是一上來就敲docker system prune -a。命令敲下去確實爽回車之后騰出幾十個 G然后第二天發(fā)現某個沒用latest之外的 tag 推上去的鏡像沒了某個只跑過一次的臨時容器依賴的卷也空了。所以我現在的習慣是任何清理動作之前先花十分鐘把占用地圖畫出來誰大、誰在用、誰可以重建心里有數再動手。1.1 用 du 從文件系統層面圈出體積大戶Docker 的所有落盤數據默認都在/var/lib/docker下先在這一層做一次分層統計速度最快、信息也最直觀du -h --max-depth1 /var/lib/docker | sort -hr | head -20這條命令跑完基本能看出大頭在哪。常見的幾個子目錄含義如下目錄存放內容能否直接刪overlay2/鏡像層與容器可寫層絕對不能手動刪刪了元數據全亂containers/容器配置與*-json.log日志日志可截斷目錄不可刪volumes/命名卷與匿名卷的真實數據需逐卷確認可能是業(yè)務庫image/鏡像元數據層數據在 overlay2不可手動刪buildkit/構建緩存可以用 prune 清tmp/臨時導入導出文件可清如果containers/這一層大得離譜那十有八九是容器日志。繼續(xù)往下鉆du -h --max-depth1 /var/lib/docker/containers | sort -hr | head -10 find /var/lib/docker/containers -name *-json.log -size 200M -exec ls -lh {} \;第二條命令會直接把超過 200M 的日志文件列出來帶上容器 ID。到這里你就能對上號哪個容器在瘋狂打日志。我遇到過最夸張的一次一個容器因為連不上依賴服務在重試循環(huán)里每毫秒打一行錯誤三天寫了 68G 的 json 日志而業(yè)務方完全不知道。1.2 docker system df 給出的是邏輯賬本文件系統層面看的是物理占用Docker 自己還有一套賬本兩者要對照著看docker system df -v不加-v只給四行匯總Images、Containers、Local Volumes、Build Cache。加了-v會展開到每個鏡像、每個容器、每個卷的SIZE和SHARED SIZE。這里有個概念必須先掰清楚否則很容易誤判SIZE是這個對象占的總空間SHARED SIZE是和別的對象共用的層UNIQUE SIZE才是它獨占的部分。因為鏡像分層復用的關系你把幾個鏡像的 SIZE 加起來得到的數字會遠大于實際占用。判斷刪掉這個鏡像能省多少要看 UNIQUE SIZE不是 SIZE。同理docker ps -s里面容器的size是可寫層virtual size是它加上所有底層的總和虛得厲害別拿它當清理依據。1.3 動手前先劃出能刪和碰不得的紅線我的紅線清單是這樣的每次清理前在腦子里過一遍碰不得/var/lib/docker下的任何目錄都不用手工rm -rf。overlay2、image、containers 這些目錄里存的是軟鏈接和元數據刪一半等于把 Docker 的內部數據庫撕了救不回來的概率很高。碰不得命名卷里如果是數據庫數據目錄刪卷等于刪庫。docker volume prune我先看docker volume ls -f danglingtrue的輸出確認沒有業(yè)務卷混在里面。可以動懸空鏡像danglingnone:none那種、停止很久的容器、構建緩存、超過閾值的歷史日志。需要確認不是懸空但也沒容器在用的鏡像。這類往往是你留作回滾備用的版本刪之前至少要docker image inspect看一眼創(chuàng)建時間。提示清理前把docker ps -a、docker image ls -a、docker volume ls三條命令的輸出重定向到文件里存一份命名帶上時間戳。真出事了至少知道丟了什么能照著清單重建。2. 分級清場從零風險的緩存到需要停機的鏡像瘦身空間大頭找到之后清理要按照風險從低到高的順序推進。我一般分三層第一層完全無損隨便清第二層影響運行中的日志和已停容器需要看一眼第三層涉及鏡像和卷必須逐個確認。這個順序的好處是往往清完前兩層問題就解決了壓根不用冒險動第三層。2.1 第一層構建緩存與懸空鏡像清了不影響任何運行構建緩存是純增量產物刪了最多下次 build 慢一點沒有任何運行風險docker builder prune --filter until168h--filter until168h表示只清理 7 天前的緩存比全量docker builder prune -a溫和得多。如果你的構建機是 CI 環(huán)境構建頻繁這一條能穩(wěn)定回收幾十個 G。懸空鏡像是指那些 TAG 被新構建頂掉、又沒有被任何容器引用的中間層docker image prune默認行為就是只刪懸空鏡像。這里有個經驗先docker image prune -a --filter until720h之前一定先用 dry-run 思路看一遍。Docker 的 prune 沒有--dry-run我的替代做法是先列出來docker images -f danglingtrue docker images --filter beforeimage:tag順手提一個常被忽略的點docker container prune清理的是已停止的容器可寫層會一起釋放。但如果你之前docker stop了某個容器只是想臨時停一下prune會把它連元數據一起干掉docker start就回不來了。停超過一周沒動的容器我才會清。2.2 第二層容器日志——那個最容易被忽視的隱形黑洞日志文件的清理有兩個層面的問題很多人卡在第一個為什么我rm了日志文件df -h顯示的空間沒變原因是 Linux 的文件引用計數機制。容器里的應用進程還持有這個文件的句柄你把目錄項刪了inode 仍然被進程引用空間不會釋放直到進程退出。所以正確做法是清空內容而不是刪除文件truncate -s 0 /var/lib/docker/containers/容器ID/容器ID-json.logtruncate保留 inode只是把長度置零進程繼續(xù)往同一個文件寫df立刻能看到空間回來。這也是為什么我說千萬不要用rm之后不管回頭du看不到這個文件df卻還是滿的會讓人懷疑人生。第二個層面是治本給后創(chuàng)建的容器加上日志上限從源頭卡住。編輯/etc/docker/daemon.json{ log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 } }改完執(zhí)行systemctl reload docker部分發(fā)行版需要restart。注意daemon.json里的日志配置只對之后新建的容器生效已經在跑的容器不會自動套用需要重建。這一點我在文檔里踩過坑配置改完以為萬事大吉結果老容器的日志照樣漲。另外max-size不要設得太小。50m 配合 3 個文件單個容器最多 150m對排查問題足夠了。設成 10m 會導致日志輪轉太頻繁真出問題的時候關鍵上下文已經被覆蓋了。2.3 第三層鏡像與卷一條命令之差就是數據事故到了這一層每一條命令我都要先在別的終端確認一遍docker system prune這條是相對安全的版本清的是已停止的容器、未被使用的網絡、懸空鏡像、懸空構建緩存。加-a之后語義就變了會刪掉所有沒有被任何容器引用的鏡像包括你打好的 tag。加--volumes更狠會連卷一起處理。關于卷這里必須說清楚版本差異不同 Docker 版本對docker volume prune的默認行為有過調整早期版本會把未使用的命名卷也算進去較新版本默認只處理匿名卷需要額外參數才碰命名卷。這種版本差異在緊急清理的時候最容易出事我的做法是永遠不依賴默認行為docker volume ls -f danglingtrue docker volume inspect 卷名先列出來逐個inspect看掛載點和創(chuàng)建時間確認是臨時數據之后再用精確的docker volume rm 卷名一個個刪。慢是慢了點但不會出現清理完發(fā)現數據庫空了這種事。鏡像同理docker rmi比prune可控得多。刪除前先看引用docker ps -a --filter ancestor鏡像名:tag有輸出說明還有容器依賴它刪了會報 conflict。這種情況下先處理容器別強行-f。3. 該留的一個都不能丟卷、鏡像與容器運行時的備份方案清理之前先備份這是常識但 Docker 場景下的備份具體備什么、備到哪、怎么驗證很多人的理解是模糊的。我見過有人以為docker commit出來的鏡像就包含了卷里的數據結果恢復的時候發(fā)現數據庫目錄是空的——因為卷的數據根本不在容器文件系統里。所以動手前先把三類東西分開看。3.1 命名卷的冷備份容器停掉tar 打包校驗結構卷是唯一存放有狀態(tài)數據的地方數據庫、上傳文件、配置。備份的基本原則是先停容器再備份讓文件處于一致狀態(tài)。熱備份在文件正在被寫入的時候打 tar很容易拿到半截文件尤其是數據庫的 WAL、雙寫日志這類。完整流程如下# 1. 停掉使用該卷的容器 docker stop mysql-prod # 2. 用臨時容器把卷掛載出來打包 docker run --rm \ -v mysql-data:/data:ro \ -v /backup/docker:/backup \ alpine:3.19 \ tar -czf /backup/mysql-data-$(date %F).tar.gz -C /data . # 3. 啟動容器 docker start mysql-prod幾個細節(jié)值得展開:ro只讀掛載防止打包過程中意外寫入-C /data .而不是-C /data *避免漏掉隱藏文件比如.my.cnf之類的點文件臨時容器用alpine體積小、啟動快不要用業(yè)務鏡像免得帶進額外依賴輸出目錄用宿主機綁定掛載不要輸出到卷里否則等于沒備份。備份產出之后一定要校驗不能只看命令退出碼tar -tzf /backup/docker/mysql-data-2024-06-01.tar.gz | head -20 tar -tzf /backup/docker/mysql-data-2024-06-01.tar.gz | wc -l看一眼目錄結構對不對、文件數量級合不合理。文件數量對不上說明源目錄少了東西。對于數據庫這類有自己備份工具的服務我更推薦做邏輯備份而非文件級拷貝。MySQL 用mysqldumpPostgreSQL 用pg_dump導出的 SQL 文件跨版本兼容性好恢復時也不會因為文件系統差異出問題。文件級 tar 備份適合配置目錄 上傳附件這類場景。3.2 鏡像與整機環(huán)境的導出save 與 export 不是一回事鏡像的備份用docker save它保留完整的層結構和元數據CMD、ENTRYPOINT、ENV、健康檢查docker save -o /backup/docker/app-1.4.2.tar \ registry.local/app:1.4.2 \ registry.local/worker:1.4.2一次 save 多個鏡像有個隱藏收益共享層只存一份。分別 save 兩個基于同一個基礎鏡像的服務加起來可能是 1.8G合并成一條命令可能只有 1.1G。這個差異在鏡像數量多的時候非??捎^。而docker export導出的是容器的文件系統快照丟失所有鏡像元數據環(huán)境變量、啟動命令、數據卷聲明全都沒了導回去只能docker import成一個新鏡像還得手工補 CMD。除非你在做鏡像取證或者特殊遷移否則不要用 export 做備份。還有一個容易混淆的點鏡像備份不包含卷數據。所以一套完整的恢復包應該是鏡像 tar 卷 tar 一份記錄啟動參數的文本。第三項特別重要我現在的做法是備份目錄里放一個run.sh把docker run的完整命令包括所有端口、環(huán)境變量、掛載寫在里面。半年后恢復的時候你絕對想不起來當初那個容器是怎么起的。3.3 恢復演練把備份灌進一個新卷驗證能起來再收工備份不演練等于沒備份。我的習慣是每月抽一次把最新的卷備份恢復到一個臨時卷上驗證目錄結構和關鍵文件# 1. 建一個驗證用的新卷 docker volume create verify-restore # 2. 把備份灌進去 docker run --rm \ -v verify-restore:/data \ -v /backup/docker:/backup:ro \ alpine:3.19 \ sh -c cd /data tar -xzf /backup/mysql-data-2024-06-01.tar.gz # 3. 檢查內容 docker run --rm -v verify-restore:/data alpine:3.19 \ sh -c ls -la /data du -sh /data # 4. 清理驗證卷 docker volume rm verify-restore如果要把正式環(huán)境恢復到新卷上還有個順序問題先創(chuàng)建卷、再填數據、最后啟動容器。如果順序反了容器啟動時會因為目錄非空而報錯有些應用比如某些版本的 MySQL遇到非空數據目錄會直接拒絕初始化。恢復鏡像則是docker load -i /backup/docker/app-1.4.2.tar docker image ls | grep appdocker load會自動識別 gzip 壓縮過的 tar不用先解壓?;謴屯曛笥胐ocker image inspect核對一下 Entrypoint 和 Env 是不是跟備份前一致這個檢查只要十秒鐘但能省掉后面半小時的排查。注意備份目錄本身也要有異地副本。我見過卷備份做得非常規(guī)范的團隊所有 tar 都在同一個宿主機上的/backup目錄然后一次磁盤故障機器和備份一起沒了。4. Docker 的壓縮到底壓的是什么鏡像導出、構建產物與存儲層壓縮這個詞在 Docker 語境下其實指向三個完全不同的東西經常被混為一談一是鏡像導出成 tar 文件時的壓縮二是構建輸出時的壓縮三是存儲驅動層面的壓縮。這三者的適用場景、能省多少空間、怎么操作差別很大。分清楚了你才知道自己需要的到底是哪一種。4.1 docker save 接 gzip離線分發(fā)的標準鏈路最常用也最直接的壓縮場景是把鏡像傳給別人或者歸檔到對象存儲docker save app:1.4.2 | gzip -c app-1.4.2.tar.gz體積收益通常在 40% 到 70% 之間取決于鏡像內容。基礎鏡像里的大量二進制文件壓縮率一般不錯如果鏡像里塞了已經壓縮過的資源jar、zip、打包好的前端產物壓縮率就有限。更進一步用pigz做并行壓縮多核機器上速度能快好幾倍docker save app:1.4.2 | pigz -p $(nproc) app-1.4.2.tar.gz加載的時候不用手動解壓Docker 自己認得docker load -i app-1.4.2.tar.gz我實測過一個 2.3G 的鏡像docker save直出 tar 是 2.3Ggzip 之后 890Mpigz -9能壓到 850M 左右但耗時明顯更長。日常用默認級別就夠了追求極限壓縮率的話再加參數。這里的經驗是gzip -1 到 -6 之間體積差距很小但耗時差距很大批量歸檔的時候別迷信-9。一個容易踩的坑docker save -o file.tar和管道寫法不要混用。有些人寫成docker save app:1.4.2 -o app.tar | gzip app.tar.gz結果得到的是一個空的 gz 文件因為-o已經把輸出重定向到文件了管道上什么都沒流過去。要么用-o直出要么用管道二選一。4.2 buildx 的輸出壓縮與鏡像瘦身是兩碼事如果你用 BuildKit 構建輸出階段本身就能控制壓縮算法和級別。比如導出一個 OCI 格式的歸檔docker buildx build \ --output typeoci,destapp-oci.tar,compressionzstd,compression-level5 \ -t app:1.4.2 .或者推到鏡像倉庫時順帶壓docker buildx build \ --output typeregistry,compressionzstd,compression-level3 \ -t registry.local/app:1.4.2 --push .zstd 相比 gzip 在壓縮率和速度上都有優(yōu)勢尤其是解壓速度對拉取鏡像的體驗改善明顯。前提是目標環(huán)境的 Docker 版本足夠新、能識別 zstd 壓縮的層老版本會報錯。做這種改動前先確認下游機器的版本別在灰度環(huán)境里試出來問題。但這里要分清的是輸出壓縮只影響傳輸和歸檔體積不影響鏡像在本地占用的空間。你把鏡像層從 gzip 換成 zstd本地docker system df看到的數字基本不變。真正減小本地占用的是鏡像瘦身屬于構建階段的事手段完全不同多階段構建把編譯工具鏈留在 builder 階段最終鏡像只帶運行時同一個RUN里apt-get update和apt-get install以及清理緩存寫完避免緩存留在某個中間層優(yōu)先用精簡的基礎鏡像.dockerignore把node_modules、.git、日志目錄排除掉這條最容易被忽略但收益經常是最大的。我見過一個前端鏡像因為沒寫.dockerignore把本地的node_modules整個 COPY 進去了鏡像從 180M 漲到 1.2G壓縮之后還是 700M。加個忽略規(guī)則問題當場解決。4.3 存儲驅動的壓縮邊界overlay2 壓不了別抱幻想很多人會問能不能讓 Docker 把鏡像層壓縮著存省點磁盤用默認的 overlay2 驅動時答案是不能。overlay2 是在普通文件系統上做聯合掛載鏡像層就是一堆普通文件和目錄不存在塊級壓縮的余地。你看到的鏡像占 2G那就是實打實的 2G疊加共享層之后。能提供存儲壓縮的是另一類驅動zfs 和 btrfs它們支持在文件系統層面開啟透明壓縮。如果你把 Docker 的數據目錄放在一個啟用了壓縮的 zfs 數據集上鏡像數據寫入時會自動壓縮讀的時候自動解壓。配置大致是這樣zfs create -o compressionlz4 docker/data systemctl stop docker # 把現有數據遷過去后文會講遷移流程lz4是速度優(yōu)先的選擇對容器場景比較合適CPU 開銷小zstd壓縮率更高但更吃 CPU。要不要折騰這條路取決于你的實際瓶頸如果磁盤 IO 不是問題、只是空間緊張值得考慮如果機器本身 CPU 已經很緊壓縮帶來的開銷可能得不償失。而且切換存儲驅動是高風險操作現有鏡像和容器都得重建不能在線切。我在生產環(huán)境做過一次評估了兩周、停機窗口留了四小時最終數據遷移加驗證用了接近三小時。所以對大多數場景清理 遷移到更大磁盤的性價比遠高于換驅動。5. 數據目錄整體搬家把 /var/lib/docker 遷到大容量磁盤清理只能解一時之困如果業(yè)務真的在長數據量遲早還會漲回來。這時候更徹底的辦法是把整個數據目錄搬到大盤上讓系統盤保持輕量。這件事本身不復雜但細節(jié)很多尤其是拷貝方式選錯、SELinux 上下文沒處理、daemon 配置改完沒生效這幾個坑我都踩過。5.1 遷移前要評估的三件事空間、文件系統、停機窗口第一件是空間。目標盤至少要有源目錄 1.2 倍的空閑留出拷貝過程中的臨時開銷。先量一下du -sh /var/lib/docker df -h /data第二件是文件系統。overlay2 對底層文件系統有要求XFS 需要ftype1較新的發(fā)行版默認滿足ext4 一般沒問題。用xfs_info /data | grep ftype確認一下如果ftype0overlay2 會拒絕啟動這個錯誤信息很晦澀提前查能省不少事。第三件是停機窗口。停止 Docker 服務期間所有容器都不可用必須提前和業(yè)務方對齊。如果有多臺機器建議先用一臺非核心機器完整走一遍流程。停服務的正確姿勢是把三個單元都停掉systemctl stop docker docker.socket containerd只停docker不頂用docker.socket會因為有人執(zhí)行 docker 命令而被激活導致服務在你拷貝過程中被拉起來數據目錄一邊寫一邊拷拷出來的是不一致的。順手確認一下當前的數據目錄到底在哪有些機器之前被人改過docker info | grep -i docker root dir同時看一眼存儲驅動docker info | grep -i storage driver這里有個新版本要注意的點較新的 Docker 在部分場景下使用 containerd 的鏡像存儲鏡像數據可能落在/var/lib/containerd而不是/var/lib/docker。如果你的docker info顯示存儲驅動是 containerd 相關的類型遷移時要一并考慮那個目錄或者確認它的實際占用不大、不需要搬。5.2 rsync 拷貝加>mkdir -p /data/docker rsync -aHAX --numeric-ids --infoprogress2 \ /var/lib/docker/ /data/docker/參數逐個解釋-a歸檔模式保留權限時間戳-H保留硬鏈接關鍵-A保留 ACL-X保留擴展屬性--numeric-ids不做用戶映射避免容器內 UID 被改。如果開了 SELinux還要處理上下文否則 Docker 啟動會各種權限拒絕chcon -R --reference/var/lib/docker /data/docker或者更穩(wěn)妥的方式是restorecon -Rv /data/docker前提是目錄策略已經配好??截愅瓿珊笞鲆淮尾町愋r炘傺a一遍增量此時服務仍然停著理論上不會有新數據但校驗一下更安心rsync -aHAX --numeric-ids --dry-run --itemize-changes \ /var/lib/docker/ /data/docker/ | head沒有輸出說明兩邊完全一致。接下來改配置。編輯/etc/docker/daemon.json{ data-root: /data/docker, log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 } }注意 JSON 語法最后一項后面不能有逗號很多人復制粘貼的時候帶了個尾逗號Docker 啟動直接失敗。啟動服務并驗證systemctl daemon-reload systemctl start docker docker info | grep -i docker root dir輸出應該是/data/docker。如果還是老路徑說明配置沒被讀到檢查文件位置和語法journalctl -u docker -n 50能看到解析錯誤的具體行號。5.3 遷移后的驗證清單與回滾路徑服務起來不等于遷移成功我一般會按這個清單過一遍驗證項命令期望結果數據目錄docker info | grep Root Dir指向新路徑鏡像完整docker image ls數量與遷移前一致容器列表docker ps -a全部容器都在狀態(tài)未變卷完整docker volume ls卷名與遷移前一致容器可啟動逐個docker start啟動無報錯新數據落盤docker run --rm alpine echo ok成功且新目錄體積增長空間釋放df -h系統盤使用率下降舊目錄一定要留著至少留一周。我一般的做法是先改名而不是刪除mv /var/lib/docker /var/lib/docker.bak確認新環(huán)境穩(wěn)定運行一段時間、業(yè)務無異常之后再rm -rf /var/lib/docker.bak。這一條看著保守但真的救過命有一次遷移后第三天發(fā)現某個容器的卷數據少了一部分追下去發(fā)現是拷貝時該容器其實還在寫停服務前有個 systemd 單元把它拉起來了靠舊目錄把數據撈了回來。回滾路徑也很簡單把daemon.json里的># /etc/cron.d/docker-gc 0 3 * * 0 root /usr/bin/docker builder prune -f --filter until168h /var/log/docker-gc.log 21 30 3 * * 0 root /usr/bin/docker image prune -f --filter until720h /var/log/docker-gc.log 21注意image prune這里只清懸空鏡像不加-a帶 tag 的鏡像不會被碰。--filter until720h再加一層保險30 天內的懸空鏡像也不動萬一有正在排查的問題還能回看。日志進/var/log/docker-gc.log是為了事后能查某天某個鏡像突然沒了翻日志能看到是被清理任務干掉的而不是懷疑存儲出問題。另外兩條約定俗成的規(guī)矩值得在團隊里推一下鏡像 tag 不用latest固定版本號這樣prune時你清楚知道每個 tag 對應什么容器不長期docker run -d手工起用編排文件描述好處是所有掛載、端口、環(huán)境變量都有據可查重建的時候不會漏參數。6.2 給磁盤體積加一道監(jiān)控別等 93% 才被動響應清理和遷移都是事后補救監(jiān)控才是提前量。最基礎的是磁盤使用率告警這個大多數環(huán)境都有。但更有價值的是趨勢告警不是用了 90%才報而是三天內漲了 20G就報。我常用的一個土辦法是每天定時采集一次關鍵目錄的體積寫到一個 csv 里配合簡單的環(huán)比判斷#!/bin/bash STAMP$(date %F) SIZE$(du -sb /var/lib/docker | awk {print $1}) echo $STAMP,$SIZE /var/log/docker-size.csv跑上兩周就能看出正常增速是多少。某天突然翻倍說明有容器在異常寫盤或者有鏡像在批量堆積這時候介入成本最低。再進一步docker system df的輸出也可以定期存檔看鏡像數量、卷數量的變化趨勢。我遇到過一個問題某個 CI 任務每天構建一個新 tag 但從來不清理鏡像數量三個月從 20 個漲到 900 多個每個都占著幾百 M 的獨占層加起來 200 多 G。這種緩慢增長靠單次告警是抓不到的只有看趨勢才能發(fā)現。最后分享一個我在實際維護中形成的小習慣每次執(zhí)行任何帶prune或rm的命令之前先把要刪的東西列出來看一眼。多花的那三十秒換來的是不用在下班前處理一個數據庫沒了的緊急電話??臻g是省出來的但數據是靠謹慎保住的這兩件事在 Docker 這里從來不矛盾。