目錄

最佳化 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 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 清快取的指令為 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 執行的時候,通常都用不到,這些都是可以移除的目標。

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

剛開始使用 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 的一點心得。

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

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

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

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

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

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

參考 DOCKER基础技术:AUFS

再次提醒,一個 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 指令