最佳化 Dockerfile - 精簡 image

最佳化 Dockerfile 還有很多方向,以精簡 image 做為結尾,有興趣可以參考文末的參考資料連結。

精簡 image 分成兩個部分說明,一個是容量,另一個是 commit 數。到昨天為止,Dockerfile 內容有點少,先根據 Laravel 官網的 Server RequirementsRedisbcmathredis PHP Extension 安裝也加入 Dockerfile:

FROM php:7.3

# 全域設定
WORKDIR /source

# 安裝環境、安裝工具
RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer
RUN apt update && apt install unzip

# 安裝 bcmath 與 redis
RUN docker-php-ext-install bcmath
RUN pecl install redis
RUN docker-php-ext-enable redis

# 加速套件下載的套件
RUN composer global require hirak/prestissimo

# 安裝程式依賴套件
COPY composer.* ./
RUN composer install --no-scripts

# 複製程式碼
COPY . .
RUN composer run post-autoload-dump

CMD ["php", "artisan", "serve", "--host", "0.0.0.0"]

接著就看如何精簡這份 image

精簡容量

Docker 讓啟動 container 的流程變簡單,理論上啟動 container 應該是飛快的,但 image 如果又肥又大,那這件事就只能活在理論裡了。

移除暫存檔案

有的指令會在執行過程產生暫存檔案,如上例的 aptcomposer global require 都會產生下載檔案的 cache 並 commit 進 image。這些檔案通常沒有必要保留,因此可以移除節省空間。

這裡要重新提醒一個 Dockerfile 產生 Docker image 的觀念:一個指令就是一個 commit,有多少 commit 就會佔用多少容量。比方說下面的寫法:

RUN curl -LO https://example.com/download.zip && dosomething
RUN rm download.zip

這個寫法會產生一個有 download.zip 檔案的 commit 與一個移除 download.zip 檔案的 commit,這樣是無法確實地把 download.zip 容量釋放出來的,需要改用下面的寫法:

RUN curl -LO https://example.com/download.zip && dosomething && rm download.zip

這個寫法,RUN 指令最後的結果會是 download.zip 檔案已移除,因此 commit 就不會有 download.zip 的內容。

總之,當看到移除檔案的指令是獨立一個 RUN 的話,那個指令通常是有像上面範例一樣的改善空間。

apt 調整寫法實測

apt 清除快取的寫法為:

apt clean && rm -rf /var/lib/apt/lists/*

# 或使用 apt-get
apt-get clean && rm -rf /var/lib/apt/lists/*

實測下面兩種寫法:

RUN apt update && apt install unzip
RUN apt update && apt install unzip && apt clean && rm -rf /var/lib/apt/lists/*

結果如下:

$ docker images laravel
REPOSITORY TAG IMAGE ID CREATED SIZE
laravel latest ac4543dab760 37 minutes ago 502MB
laravel optimized c68838f961f0 43 seconds ago 485MB

可以看得出明顯的差異。

composer global require 調整寫法實測

Composer 清快取的指令為 composer clear-cache。實測下面兩種寫法:

RUN composer global require hirak/prestissimo
RUN composer global require hirak/prestissimo && composer clear-cache

RUN composer install --no-scripts
RUN composer install --no-scripts && composer clear-cache

結果如下:

$ docker images laravel
REPOSITORY TAG IMAGE ID CREATED SIZE
laravel latest ac4543dab760 41 minutes ago 502MB
laravel optimized 5672f76be599 6 seconds ago 461MB

可以看出這個差異也非常多,兩個總合起來就能省掉 50MB 的空間,不無小省。

最終的 Dockerfile 如下:

FROM php:7.3

# 全域設定
WORKDIR /source

# 安裝環境、安裝工具
RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer
RUN apt update && apt install unzip && apt clean && rm -rf /var/lib/apt/lists/*
RUN docker-php-ext-install bcmath
RUN pecl install redis
RUN docker-php-ext-enable redis

# 加速套件下載的套件
RUN composer global require hirak/prestissimo && composer clear-cache

# 安裝程式依賴套件
COPY composer.* ./
RUN composer install --no-scripts && composer clear-cache

# 複製程式碼
COPY . .
RUN composer run post-autoload-dump

CMD ["php", "artisan", "serve", "--host", "0.0.0.0"]

雙管齊下的結果如下:

$ docker images laravel
REPOSITORY TAG IMAGE ID CREATED SIZE
laravel latest 097e4cc0805f 3 seconds ago 502MB
laravel optimized 55d3a5fcfd3d About a minute ago 443MB

移除非必要的東西

以最初的目標來看,是要啟動一個 Laravel 的 server 並看到歡迎頁,那其實有些套件就不是必要的,如單元測試套件。Composer 加上 --no-dev 參數即可排除開發階段安裝的套件。

RUN composer install --no-dev --no-scripts && composer clear-cache

結果如下:

$ docker images laravel
REPOSITORY TAG IMAGE ID CREATED SIZE
laravel latest 097e4cc0805f 38 minutes ago 502MB
laravel optimized fd655c5532e0 3 seconds ago 426MB

其他常見非必要的工具如 vim、sshd、git 等,在 container 執行的時候,通常都用不到,這些都是可以移除的目標。

本範例沒有非必要的工具。

改使用容量較小的 image

剛開始使用 Docker 的時候,通常會找自己比較熟悉的 Linux 發行版,如 Ubuntu。不同的發行版,其實容量也有點差異,以下是今天下載 UbuntuDebianCentOSAlpine 的容量比較:

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu latest 9140108b62dc 3 days ago 72.9MB
debian stable-slim da838f7eb4f8 2 weeks ago 69.2MB
debian latest f6dcff9b59af 2 weeks ago 114MB
centos latest 0d120b6ccaa8 7 weeks ago 215MB
alpine latest a24bb4013296 4 months ago 5.57MB

這裡可以看到 Alpine 小到很不可思議,而 Debian 則是有普通版跟 slim 版,這些都是可以嘗試的選擇。以 PHP 官方 image 來說,主要版本是 Debian,但也有 Alpine 的版本,因此如果有需要追求極小 image 的話,也是可以多找看看有沒有已經做好的 Alpine 版本可以 FROM 來用,網路也比較多文章在討論使用 Alpine 的好處。

以下為改寫成 Alpine 的 Dockerfile,主要差異在 apk 安裝指令:

FROM php:7.3-alpine

# 全域設定
WORKDIR /source

# 安裝環境、安裝工具
RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer
RUN apk add --no-cache unzip
RUN docker-php-ext-install bcmath
RUN apk add --no-cache --virtual .build-deps autoconf g++ make && pecl install redis && apk del .build-deps
RUN docker-php-ext-enable redis

# 加速套件下載的套件
RUN composer global require hirak/prestissimo && composer clear-cache

# 安裝程式依賴套件
COPY composer.* ./
RUN composer install --no-dev --no-scripts && composer clear-cache

# 複製程式碼
COPY . .
RUN composer run post-autoload-dump

CMD ["php", "artisan", "serve", "--host", "0.0.0.0"]

改完之後的結果如下,差了 300MB:

$ docker images laravel
REPOSITORY TAG IMAGE ID CREATED SIZE
laravel optimized ce9f7438e818 14 seconds ago 102MB
laravel latest 097e4cc0805f 53 minutes ago 502MB

雖然看起來很美好,但事實上改用 Alpine 並不是簡單的事,以下聊聊從 Ubuntu 轉 Alpine 的一點心得。

shell 不是 bash

因對寫腳本不熟,因此有發生 bash 指令在 sh 上不能使用的問題,當時處理了很久才解決。

不同的套件管理工具

Ubuntu 為 apt,Alpine 為 apk。用途都是下載套件,使用概念大多都差不多,而實際使用上最大的困擾在於套件名稱不同。比方說,Memcached 的相關套件,apt 叫 libmemcached-dev,apk 則是 libmemcached-libs,這類問題都得上網查詢資料,以及做嘗試才能找到真正要的套件為何。

系統使用的 libc 不同

Alpine 採用 musl libc 而不是 glibc 系列,因此某些套件可能會無法使用。

更詳細的說明可參考 PHP image 或其他官方 image 對於 Alpine image 的解釋。

官方說法是大多數套件都沒有問題,但聽朋友說過確實踩過這個雷,只是從來沒遇過,所以沒有範例可以說明。

精簡 commit

Docker 的 commit 數量是有限制的,為 127 個,這是精簡的理由之一。另一個更重大的理由是:如果檔案系統使用 AUFS 的前提下,只要 commit 數越多,檔案系統的操作就會越慢,因此更需要想辦法來減少 commit 數。

參考 DOCKER基础技术:AUFS

合併 commit

再次提醒,一個 Docker 指令就是一個 commit。通常能合併的會是 RUN 指令,如:

# 合併前
RUN apk add --no-cache git
RUN apk add --no-cache unzip

# 合併後
RUN apk add --no-cache git unzip

可是如果指令過長的話,就會失去維護性。通常會改寫成像下面這樣:

RUN apk add --no-cache \
git \
unzip

這樣比較能一眼看出目前裝了什麼套件,但相對在 build 的過程,資訊相較就會比較雜亂。

參考官方 Dockerfile,發現 set -xe 可以對查 log 有幫助:

FROM php:7.3-alpine

RUN set -xe && \
apk add --no-cache \
git \
unzip

實際作用沒有特別研究,但對於 build 過程的影響,最主要的差異在:不管串接幾個指令,它都會有一個像下面的 log:

+ apk add --no-cache git unzip

它還會同時把空白全部去除,這樣在看 build log 會非常清楚。

要如何決定合併的做法

追求極致,把所有 RUN 合併在一個 commit,那這樣就會喪失分層式可以共用 image 的優點,因此如何拿捏適點的 commit 大小,是很有藝術的。

以 PHP 為例,通常會分成下面幾種類型:

  1. 系統層的準備,如 unzip
  2. PHP extension 的準備,如 bcmathredis
  3. 依賴下載,包括 Composer 執行檔
  4. 執行程式初始化順序

這邊就不說明怎麼處理的,直接給結果吧:

FROM php:7.3-alpine

# 全域設定
WORKDIR /source

# 安裝環境
RUN apk add --no-cache unzip

# 安裝 extension
RUN set -xe && \
apk add --no-cache --virtual .build-deps \
autoconf \
g++ \
make \
&& \
docker-php-ext-install \
bcmath \
&& \
pecl install \
redis \
&& \
docker-php-ext-enable \
redis \
&& \
apk del .build-deps \
&& \
php -m

RUN set -xe && \
curl -sS https://getcomposer.org/installer | php && \
mv composer.phar /usr/local/bin/composer

# 加速套件下載的套件
RUN composer global require hirak/prestissimo && composer clear-cache

# 安裝程式依賴套件
COPY composer.* ./
RUN composer install --no-dev --no-scripts && composer clear-cache

# 複製程式碼
COPY . .
RUN composer run post-autoload-dump

CMD ["php", "artisan", "serve", "--host", "0.0.0.0"]

今日自我回顧

會影響 container 啟動快與否,有一部分會取決於 image 下載時間,這也是今天能解掉的問題;另外則是 process 的調整,未來有機會再討論。

  • 練習減少 image 的空間
  • 練習整理 RUN 指令

參考資料