有件事很讓人洩氣:花了好幾個小時,以為在除錯一個單純的 Docker build 問題,最後卻發現原因完全出乎意料。最近在部署 Laravel 時,遇到的就是這種情況。
其實是很常規的工作 - 當時要為一個 Laravel 應用程式建置 Docker image,準備跑在 Linode 上。沒什麼特別的,就是很典型的設定:
- PHP 8.3,搭配所有常見的擴充。
- 透過 PECL 安裝 MongoDB 的驅動與擴充。
- 安裝 Caddy Server。
- 用 Supervisor 來管理各項程序。
- 複製檔案並設定相關服務。
問題
一切看起來都很順利,直到 build 每次都剛好卡在 300 秒 失敗。準時得像時鐘一樣:
=> CANCELED exporting to image 37.6s
...
The following exception is caused by a process timeout
Check https://getcomposer.org/doc/06-config.md#process-timeout for details
ERROR: failed to solve: Canceled: context canceled
In Process.php line 1205:
The process "DOCKER_BUILDKIT=1 docker 'build' '--no-cache' '-t' 'my/api' '.'" exceeded the timeout of 300 seconds.
build [--dev] [--no-dev] [--] [<args>...]我的第一個念頭是:「太好了,Docker 又來了」。就是那種感覺 - 第一時間就把矛頭指向最明顯的戰犯。我花了不少時間鑽進 Docker 的官方文件,想找出某個我一定是漏掉的神祕 build timeout 設定。但有些地方兜不攏。許多官方 image 的建置時間遠超過 300 秒,卻都能正常運作。而且,如果 Docker 真的內建 timeout,不是應該會寫在文件的某個地方嗎?
情況變得更複雜
事情開始變得複雜:心想不如用 multi-stage build 來優化建置流程:
FROM php:8.3-fpm AS mongodb-builder
RUN apt-get update && apt-get install -y \
libssl-dev \
&& pecl install mongodb \
&& docker-php-ext-enable mongodb
# ...
FROM php:8.3-fpm
COPY --from=mongodb-builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions
COPY --from=mongodb-builder /usr/local/etc/php/conf.d/docker-php-ext-mongodb.ini /usr/local/etc/php/conf.d/
RUN apt-get update && apt-get install -y \
curl gnupg gosu zip unzip git supervisor sqlite3 libcap2-bin \
libpng-dev dnsutils librsvg2-bin fswatch vim libzip-dev \
libxml2-dev libcurl4-openssl-dev libssl-dev \
&& docker-php-ext-install zip pdo_mysql bcmath soap intl opcache \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# ...有解決問題嗎?並沒有。不過至少讓 Dockerfile 看起來漂亮多了,也算是有點收穫。
靈光乍現的瞬間
接著,我注意到錯誤訊息裡有一行,先前一直被我略過:
Check https://getcomposer.org/doc/06-config.md#process-timeout for detailsComposer?可是我在 Dockerfile 裡只不過是安裝 Composer 而已:
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \就在這一刻,我恍然大悟。我在 composer.json 裡放了幾個順手的捷徑:
"lint": "pint",
"clear": [
"@php artisan lighthouse:clear-cache",
"@php artisan config:clear",
"@php artisan route:clear",
"@php artisan view:clear",
"@php artisan cache:clear"
],
"test": [
"@lint",
"@clear",
"pest --profile --colors=always"
],
"docker": "DOCKER_BUILDKIT=1 docker",
"build": [
"@clear",
"@docker build --no-cache -t tgk/be .",
"@docker tag tgk/be heytakeshi/tgk-be:latest",
"@docker push heytakeshi/tgk-be"
]而我一直是透過 Composer、用 composer build 來執行 Docker 建置。猜猜看什麼東西的預設 timeout 剛好是 300 秒?沒錯,就是 Composer。
解決方法
解法簡單到有點難為情。兩個做法擇一:
- 請 Composer 把時間放寬一點(管理 process timeout):
"config": {
// ...
"process-timeout": 600
},- 或者把 Docker 指令移到 shell script 裡,讓 Composer 管不著:
#!/bin/bash
DOCKER_BUILDKIT=1 docker build --no-cache -t my/api .
docker tag my/api my/api
docker push my/api我選了第一個做法,因為懶得更新所有的部署腳本。
心得
有時候,問題並不在你以為的地方。
答案明明就寫在錯誤訊息裡,我卻花了太多時間找自以為是的戰犯。另外,把所有東西都塞進 Composer script 來跑,或許也不總是個好主意 - 不過那就留給未來的我去煩惱了。