找回那些被 Docker 吃掉的磁碟空間

Ian Chen
Starbugs Weekly 星巴哥技術專欄
9 min readApr 17, 2023

--

如果你是 Docker/Kubernetes 的重度使用者,應該多多少少會遇到一個問題:“no space left on device”

當然,如果你的硬碟空間很大,也不介意大把空間都用來存放不必要的 Docker 資源,那你可以先左轉離開了 XD

在討論如何有效避免 Docker 吃掉大把的磁碟空間之前,我們先來聊聊 Docker 有哪些(會佔用空間的)資源:

  1. Image:Container 的映像檔案,通常由 Dockerfile 搭配 docker build 產生。
  2. Container:具有獨立環境的 Process(在 Linux 作業系統中使用 namespace 實現環境的隔離),一般使用 docker create 或是 docker run 命令啟動。
  3. Volume:使用者可以藉由 Docker volume 讓 Container 掛載 Host 環境的資料(檔案或是整個資料夾)。
  4. Network:可以使多個 Docker Container(s) 的網路環境相通,Docker network 的運作方式很大程度取決於開發者使用何種 Network driver,常見的 driver 有 bridge、host、overlay、macvlan、none、external plugin。

了解 Docker 的常用資源後,我們來看看這些資源分別有哪些狀態:

  1. used:正在被某個 container(s) 使用的資源。
  2. unused:完全沒有被 container 使用到。
  3. dangling:失效的 image,永遠不會被使用到。

要判斷資源是 used 或是 unused 其實非常簡單,Docker 的判斷標準是該資源目前是否被至少一個 Container 使用,如果不是,該資源就是 unused resource。

而 dangling 是比較特別的狀態,它只存在於 docker image,原因是 docker image 是有版本概念的。一般來說,我們在 docker build 時會加上 -t flag 為 image 打上標籤(映像檔案名稱加上版本號碼)。如果我們不指定版本號碼,Docker 會預設它為 latest 版本。

然而,當我們重複編譯同一個 tag name 的 docker image,那麼第一次產生的 docker image 在第二次編譯結束時就會進入 dangling 狀態(用 docker image ls 會觀察到一個 tag name 為 <none> 的 image),而這些 dangling image(s) 永遠不會被使用到,如果不定期清出便會白白的佔據磁碟空間。

聊完 Docker 資源與相關狀態後,我們就來看看要如何清理這些 unused/dangling resources 吧!

Docker 提供了 docker <resource> prune 指令家族,讓我們清除不同的資源類型:

# Remove all unused images, not just dangling ones
$ docker image prune -a
# Remove dangling images
$ docker image prune

$ docker network prune

$ docker volume prune

$ docker container prune
# Remove all unused containers, networks, images (both dangling and unreferenced), and optionally, volumes.
$ docker system prune

docker prune 其實已經解決了部分場景,比如說對於一個運作著 production service(s) 的 VM 或是 CI/CD Runner,我們只要使用這些命令再配合 system cron job 就綽綽有餘了。

但對於開發者的個人環境,很多時候我們其實不希望去刪除那些 unused image(s),讓我們考慮以下情境:

小明使用 docker-compose 一次部署 20 個 container,這些 container 使用了 20+ 個 docker image。若小明使用 docker compose rm + docker image prune 清空資源,他會發現等到下次要啟用服務時,這些 image 可能已經被 docker 移除了。

為了避免這個問題(偷懶),我們一般在本地測試部署環境的時候,都會選擇忽略清空資源的步驟,久而久之這些 dangling image 以及 unused volume 就會把本機的磁碟空間佔滿…。

因此,比較好的作法其實是用 Makefile 或是 shell script 處理 docker image 的編譯工作,同時在編譯完成後移除 dangling image 或是指定 tag name 的 image:

docker image prune
# or
docker image ls | grep "<YOUR_TAG_NAME>" | awk '{print $3}' | xargs docker image rm

上方程式碼中的第二種命令可以一次刪除所有符合 grep 中帶入條件的 image(s),這也是筆者最常使用的作法(畢竟用 docker image prune 實在太容易錯殺無辜…)。

到目前為止,我們已經了解如何在不同的使用情境中處理 unused image 以及 dangling image。但是,上方的解法對入門者來說並不是很友善(而且也不方便)。為了節省時間,我們可以將這些步驟撰寫成一份 shell script 並且放在系統的 /bin 下,這樣一來就可以直接通過 terminal 執行該腳本:

#!/bin/bash

force=false

while getopts ":f" opt; do
case ${opt} in
f )
force=true
;;
\? )
echo "Invalid option: -$OPTARG" 1>&2
exit 1
;;
: )
echo "Option -$OPTARG requires an argument." 1>&2
exit 1
;;
esac
done

shift $((OPTIND -1))

if [ $# -ne 2 ]; then
echo "Usage: docker-clean [-f] <resource> <keyword>"
exit 1
fi

resource=$1
keyword=$2

if [ "$force" = true ]; then
force_args="-f"
else
read -p "Are you sure you want to delete all $resource with $keyword? [y/N] " confirmation
if [ "$confirmation" != "y" ] && [ "$confirmation" != "Y" ]; then
echo "Operation cancelled."
exit 0
fi
fi

case $resource in
"image")
docker images | grep "$keyword" | awk '{print $3}' | xargs docker rmi $force_args
;;
"container")
docker ps -a | grep "$keyword" | awk '{print $1}' | xargs docker rm $force_args
;;
*)
echo "Invalid resource type. Must be either 'image' or 'container'."
exit 1
;;
esac

BTW:

整份 shell script 都是用 chatGPT 產生的,對於一些簡單的應用案例,chatGPT 真的是很棒的工具(但也記得反思 chatGPT 提供之資料的真實性,避免毀掉重要資料的慘劇發生在你身上…)

該腳本能夠移除特定 tag name 的 image 以及 container,使用教學參考下方範例:

# 安裝
$ git clone https://github.com/ianchen0119/docker-clean.git
$ mv docker-clean/docker-clean /bin/docker-clean
$ chmod 777 /bin/docker-clean

# 使用
$ docker-clean
Usage: docker-clean [-f] <resource> <keyword>

總結

docker 這類容器化方案對開發者來說是一個非常友善的工具,但使用不當的話其實很容易影響到執行環境的機器,本篇文章簡單的介紹一些可參考的處理方式,如果讀者也有不錯的管理方式也歡迎在留言處發表意見!

最後補充一下個人不喜歡批量處理 unused & dangling image(s) 的原因:

docker image 在重新編譯時會根據 commit hash 使用對應的 cache layer 避免 re-work,如果每次編譯都固定將 unused image 移除,也會順帶的把這些 cache 清空,大幅提高 image 編譯的時間。

在一些微服務的測試場景下,如果一個 image 需要額外的 3 分鐘來重現之前的步驟,要將整個服務跑起來可能會額外的佔用 30 分鐘以上的時間…

如果想在使用 docker 的同時最佳化磁碟空間的使用率,除了定期清理用不到的 docker 靜態資源,我們也可以從其它方向下手:

  • 使用精簡化的 docker image 作為 base
  • base image 盡量重用:如果應用程式沒有特別的依賴關係,應盡量選用重複的 base image 來編譯或是作為應用程式的執行環境
  • 避免過多的 docker commit:這點跟上面那點類似,有助於保持 image 的精簡。不過也並非屬於強制性的,因為有些時候維運人員可能會頻繁的更新某些特定的 image,如果 Dockerfile 寫的太過精簡可能會造成難以閱讀以及難以利用 cache 的情況發生
  • 清除編譯 image 期間製造的垃圾(在安裝完成後使用 rm 或是 apt-get remove 等命令移除安裝包)
  • 使用 distroless image:現在圈內也有許多人在呼籲盡量不要使用 alpine 這類的 image 作為 base,而是建議開發者盡可能的選用 distroless image,背後的原因出於 container security 的考量,筆者之後會寫一篇專欄詳談

--

--