在說明 Multi-stage Build 之前,先來簡單了解持續整合(Continuous Integration,以下簡稱 CI)的 Build 與 DevOps 的 Pipeline。
CI 裡面用了 Build 這個關鍵字,實際它背後做的事包含了 compilation、testing、inspection 與 deployment 等;而 DevOps Pipeline 則提到軟體生命週期有 development、testing、deployment 不同的階段(stage),同時每個階段都有可能會產生 artifacts。
對 CI 有興趣可以參考筆者過去寫的鐵人賽:CI 從入門到入坑
綜合上述說明,在 build 的過程會做不同的任務,並產生 artifacts,而在不同階段又會做不同的任務。
接著來看範例,延續最佳化 Dockerfile 完之後的結果如下:
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"]
這個 Dockerfile 已可以建置出 Laravel image,但思考以下這兩個奇妙的問題:
目前 Dockerfile 的最佳化方法,面對這兩個問題會是矛盾大對決,要嘛偏維運,要嘛偏開發,無法同時解決。
幾乎所有語言都會有這個問題,所以回頭看完各種框架如何 build image 後,現在再來看這個問題,相信更有感覺。
最單純直接的解決方法,就是針對維運與開發寫兩個 Dockerfile,但這在使用上非常不方便,於是第三方 Rancher 就寫了一個工具--Dapper,主要 Dockerfile 作為維運用,而用另一個指令來處理開發用的 Dockerfile。
若需要在開發用的環境上,處理不固定的任務(如:依狀況進入 container 下不同的指令),Dapper 是非常好用的;如果在 container 上是處理固定的任務(如:執行 phpunit
),則使用 Docker 17.05 開始推出的 Multi-stage Build 會更方便。
概念其實很簡單,參考下面的 Dockerfile:
FROM alpine AS build
RUN touch test
FROM alpine
COPY --from=build /test .
RUN ls -l /test
這個 Dockerfile 有三個特別的地方跟過去不大一樣:
FROM
指令,每個 FROM
指令都代表一個 stage,每個 stage 的結果都是 imageFROM
指令使用 AS
可以為 image 取別名COPY
多了一個選項 --from
,選項要給的值是 image,實際行為是從該 image 把對應路徑的檔案或 artifacts,複製進 container執行過程如下:
這兩個 stage 是有互相依賴的,因此 touch test
指令若移除的話,就會出現找不到檔案的錯誤。
上面有提到 COPY
的 --form
選項要給值是 image,實際上不只可以使用 stage image,而是連 remote repository 都能使用。因此像 Composer 有 image,且執行檔單一檔案,所以安裝 Composer 的方法,可以改成下面這個寫法:
COPY --from=composer:1 /usr/bin/composer /usr/bin/composer
因 stage image 的 alias 可以當作 image 用,所以下面這個寫法是可行的:
FROM alpine AS curl
RUN apk add --no-cache curl
FROM curl AS build1
RUN curl --help
FROM curl AS build2
RUN curl --version
最後這裡就舉複雜的範例:一開始提的最佳化 Dockerfile,我們把拿它套用 Multi-stage Build。這個 Dockerfile 它可以分作下面幾個 stage:
# PHP 環境基礎
FROM php:7.3-alpine AS base
# npm 建置 stage
FROM node:12-alpine AS npm_builder
# Composer 安裝依賴
FROM base AS composer_builder
# 上線環境
FROM base
Composer 與上線環境依賴 base 是因為像 bcmath
和 redis
套件依賴是屬於底層共用的套件,所以打一個共用 image 會比較方便一點;另外 Laravel 框架 skeleton 有內帶 npm 相關檔案,這次也加入成一個 stage。
先看 base image,這段應該沒問題,因為它只做安裝 extension:
FROM php:7.3-alpine AS base
# 安裝 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
再來 npm image 應該也沒有太大問題:
FROM node:14-alpine AS npm_builder
WORKDIR /source
COPY package.* ./
# 依照 npm run production 提示把 vue-template-compiler 先安裝進去
RUN npm install && npm install vue-template-compiler --save-dev --production=false
COPY . .
RUN npm run production
Composer image,包括安裝 Composer 的調整,與安裝依賴套件。在這個 stage 還可以做單元測試:
FROM base AS composer_builder
WORKDIR /source
COPY --from=composer:1 /usr/bin/composer /usr/bin/composer
# 加速套件下載的套件
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
# 執行測試
RUN php vendor/bin/phpunit
# 移除測試套件
RUN composer install --no-dev
最後是最難的,正如昨天最後的回顧所提到的,對框架要非常了解,才知道如何哪些檔案該複製,還有先後順序等。
FROM base
WORKDIR /var/www/html
COPY --from=composer_builder /source/vendor ./vendor
COPY --from=npm_builder /source/public/js ./public/js
COPY --from=npm_builder /source/public/css ./public/css
COPY --from=npm_builder /source/public/mix-manifest.json ./public
COPY . .
COPY --from=composer_builder /source/bootstrap ./bootstrap
CMD ["php", "artisan", "serve", "--host", "0.0.0.0"]
Build image 是以最後一個 stage 為主。結果又會再更小一些。
$ docker images laravel
REPOSITORY TAG IMAGE ID CREATED SIZE
laravel latest 02b76d8ded9b 12 minutes ago 99.5MB
使用 Multi-stage Build 不但可以減少容量,同時還能使用 Docker 創造多個環境執行建置階段的任務。
當建置階段與執行階段間,使用 artifacts 做區隔的時候,通常都適合使用 Multi-stage Build。如:Golang、Java 等,所有編譯語言,需要編譯並產生 artifacts 才能執行。