Docker PHP-FPM / PHP-Worker 容器 SSL 憑證過期問題完整解決指南

前言

在維護 Laradock 或其他 Docker PHP 環境時,經常會遇到舊版容器因 CA 憑證過期,導致無法正常執行 HTTPS 請求的問題。本文將詳細說明問題的診斷方式與解決方案,適用於 php-fpm 和 php-worker 容器。


問題現象

錯誤訊息一:curl 錯誤

curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it.

錯誤訊息二:PHP 錯誤

PHP Warning: file_get_contents(): SSL operation failed with code 1.
OpenSSL Error messages: error:14090086:SSL routines:ssl3_get_server_certificate:certificate verify failed

PHP Warning: file_get_contents(): Failed to enable crypto

PHP Warning: file_get_contents(https://example.com): failed to open stream: operation failed

錯誤訊息三:Composer 錯誤

The "https://repo.packagist.org/packages.json" file could not be downloaded: 
SSL operation failed with code 1.

錯誤訊息四:Laravel/Guzzle HTTP 錯誤

cURL error 60: SSL certificate problem: unable to get local issuer certificate

問題原因分析

根本原因

Docker image 內建的 CA(Certificate Authority)根憑證過期或版本過舊,無法驗證新簽發的 SSL 憑證鏈。

常見情境

  1. 使用舊版 base image:例如 alpine:3.12php:7.4-alpine3.12 等 2020-2021 年的 image
  2. Let’s Encrypt 憑證鏈變更:2021 年 9 月 Let’s Encrypt 的 DST Root CA X3 過期,影響許多舊系統
  3. 長期未更新的容器:容器內的 ca-certificates 套件長期未更新
  4. 自建 image 未包含憑證更新:Dockerfile 中未加入憑證更新步驟

影響範圍

  • curl 命令
  • PHP 的 file_get_contents()fopen() 等函數
  • PHP cURL 擴展
  • Composer 套件管理
  • Guzzle HTTP Client
  • Laravel HTTP Client
  • 任何需要 HTTPS 連線的 PHP 應用

診斷步驟

步驟一:進入容器

# 進入 php-fpm 容器
docker-compose exec php-fpm sh

# 或進入 php-worker 容器
docker-compose exec php-worker sh

# 如果容器已停止,使用 run 指令
docker-compose run --rm php-fpm sh

步驟二:測試 curl HTTPS 連線

curl -v https://www.google.com/

觀察輸出,重點關注:

* successfully set certificate verify locations:
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: none
* TLSv1.2 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: unable to get local issuer certificate

這表示 curl 使用 /etc/ssl/certs/ca-certificates.crt 作為 CA 憑證檔案,但驗證失敗。

步驟三:檢查 PHP OpenSSL 憑證路徑

php -r "var_dump(openssl_get_cert_locations());"

輸出範例:

array(8) {
  ["default_cert_file"]=>
  string(17) "/etc/ssl/cert.pem"
  ["default_cert_file_env"]=>
  string(13) "SSL_CERT_FILE"
  ["default_cert_dir"]=>
  string(14) "/etc/ssl/certs"
  ["default_cert_dir_env"]=>
  string(12) "SSL_CERT_DIR"
  ["default_private_dir"]=>
  string(16) "/etc/ssl/private"
  ["default_default_cert_area"]=>
  string(8) "/etc/ssl"
  ["ini_cafile"]=>
  string(0) ""
  ["ini_capath"]=>
  string(0) ""
}

重要欄位說明:

欄位說明
default_cert_filePHP OpenSSL 預設讀取的憑證檔案路徑
default_cert_dirPHP OpenSSL 預設讀取的憑證目錄
ini_cafilephp.ini 中設定的 openssl.cafile
ini_capathphp.ini 中設定的 openssl.capath

步驟四:檢查憑證檔案狀態

# 檢查 /etc/ssl/ 目錄結構
ls -la /etc/ssl/

# 檢查憑證檔案詳細資訊
ls -la /etc/ssl/certs/ca-certificates.crt
ls -la /etc/ssl/cert.pem

# 如果 PHP 使用 /usr/lib/ssl/
ls -la /usr/lib/ssl/cert.pem 2>/dev/null || echo "路徑不存在"

輸出範例(問題狀態):

-rw-r--r-- 1 root root 214057 Apr 15  2021 ca-certificates.crt
lrwxrwxrwx 1 root root     25 Apr 14  2021 cert.pem -> certs/ca-certificates.crt

判斷標準:如果憑證檔案日期是 2021 年或更早,很可能已經過期或缺少新的 CA 憑證。

步驟五:測試 PHP HTTPS 連線

# 測試 file_get_contents
php -r "echo @file_get_contents('https://www.google.com/') ? 'OK' : 'FAIL';"

# 測試 curl 擴展
php -r "
\$ch = curl_init('https://www.google.com/');
curl_setopt(\$ch, CURLOPT_RETURNTRANSFER, true);
\$result = curl_exec(\$ch);
if (curl_errno(\$ch)) {
    echo 'Error: ' . curl_error(\$ch);
} else {
    echo 'OK';
}
curl_close(\$ch);
"

解決方案

方案一:容器內手動修復(臨時方案)

適用於快速修復已運行的容器,但容器重建後會失效。

# 1. 下載最新 CA 憑證(使用 -k 跳過 SSL 驗證)
curl -k -o /tmp/cacert.pem https://curl.se/ca/cacert.pem

# 2. 備份原有憑證(可選)
cp /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt.bak

# 3. 更新憑證檔案
cp /tmp/cacert.pem /etc/ssl/certs/ca-certificates.crt

# 4. 根據 PHP 實際讀取路徑更新(檢查 openssl_get_cert_locations 的結果)
# 如果 default_cert_file 是 /etc/ssl/cert.pem
cp /tmp/cacert.pem /etc/ssl/cert.pem

# 如果 default_cert_file 是 /usr/lib/ssl/cert.pem
mkdir -p /usr/lib/ssl
cp /tmp/cacert.pem /usr/lib/ssl/cert.pem

# 5. 清理暫存檔
rm /tmp/cacert.pem

# 6. 驗證修復結果
curl -v https://www.google.com/
php -r "echo @file_get_contents('https://www.google.com/') ? 'PHP OK' : 'PHP FAIL';"

方案二:修改 Dockerfile(永久方案)

Alpine Linux base image(php-fpm / php-worker)

#--------------------------------------------------------------------------
# 更新 CA 憑證
#--------------------------------------------------------------------------
# 解決舊版 Alpine image CA 憑證過期問題
# 必須在其他需要 HTTPS 的操作之前執行

RUN apk add --no-cache ca-certificates curl \
    && update-ca-certificates \
    && curl -k -o /tmp/cacert.pem https://curl.se/ca/cacert.pem \
    && cp /tmp/cacert.pem /etc/ssl/certs/ca-certificates.crt \
    && cp /tmp/cacert.pem /etc/ssl/cert.pem \
    && rm /tmp/cacert.pem \
    && echo "CA certificates updated successfully"

Debian/Ubuntu base image

#--------------------------------------------------------------------------
# 更新 CA 憑證
#--------------------------------------------------------------------------

RUN apt-get update \
    && apt-get install -yqq --no-install-recommends ca-certificates curl \
    && update-ca-certificates \
    && curl -k -o /tmp/cacert.pem https://curl.se/ca/cacert.pem \
    && cp /tmp/cacert.pem /etc/ssl/certs/ca-certificates.crt \
    && ln -sf /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert.pem \
    && rm /tmp/cacert.pem \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* \
    && echo "CA certificates updated successfully

Laradock 完整範例

php-fpm/Dockerfile 修改

ARG LARADOCK_PHP_VERSION
FROM php:${LARADOCK_PHP_VERSION}-fpm

LABEL maintainer="Your Name <your@email.com>"

ARG LARADOCK_PHP_VERSION

#--------------------------------------------------------------------------
# 更新 CA 憑證(必須在最前面)
#--------------------------------------------------------------------------

RUN apt-get update \
    && apt-get install -yqq --no-install-recommends ca-certificates curl \
    && update-ca-certificates \
    && curl -k -o /tmp/cacert.pem https://curl.se/ca/cacert.pem \
    && cp /tmp/cacert.pem /etc/ssl/certs/ca-certificates.crt \
    && ln -sf /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert.pem \
    && rm /tmp/cacert.pem \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

#--------------------------------------------------------------------------
# 其他套件安裝...
#--------------------------------------------------------------------------
# 以下為原有的 Dockerfile 內容

php-worker/Dockerfile 修改(Alpine)

ARG LARADOCK_PHP_VERSION
FROM php:${LARADOCK_PHP_VERSION}-alpine3.12

LABEL maintainer="Your Name <your@email.com>"

ARG LARADOCK_PHP_VERSION

#--------------------------------------------------------------------------
# 更新 CA 憑證(必須在最前面)
#--------------------------------------------------------------------------

RUN apk --update add \
    ca-certificates \
    curl \
    wget \
    && update-ca-certificates \
    && curl -k -o /tmp/cacert.pem https://curl.se/ca/cacert.pem \
    && cp /tmp/cacert.pem /etc/ssl/certs/ca-certificates.crt \
    && cp /tmp/cacert.pem /etc/ssl/cert.pem \
    && rm /tmp/cacert.pem

#--------------------------------------------------------------------------
# 安裝其他套件
#--------------------------------------------------------------------------

RUN apk --update add \
    git \
    build-base \
    libmcrypt-dev \
    libxml2-dev \
    pcre-dev \
    zlib-dev \
    autoconf \
    cyrus-sasl-dev \
    libgsasl-dev \
    oniguruma-dev \
    libressl \
    libressl-dev \
    supervisor

# ... 其餘 Dockerfile 內容

重建容器

# 重建 php-fpm
docker-compose build --no-cache php-fpm

# 重建 php-worker
docker-compose build --no-cache php-worker

# 重新啟動服務
docker-compose up -d php-fpm php-worker

# 驗證修復
docker-compose exec php-fpm php -r "echo @file_get_contents('https://www.google.com/') ? 'OK' : 'FAIL';"
docker-compose exec php-worker php -r "echo @file_get_contents('https://www.google.com/') ? 'OK' : 'FAIL';"

替代方案:php.ini 設定

如果不想修改 Dockerfile,可以透過 php.ini 指定憑證路徑:

[openssl]
openssl.cafile=/path/to/cacert.pem
openssl.capath=/etc/ssl/certs

或在 PHP 程式碼中動態設定:

// 設定 stream context
$context = stream_context_create([
    'ssl' => [
        'cafile' => '/path/to/cacert.pem',
        'verify_peer' => true,
        'verify_peer_name' => true,
    ]
]);

$content = file_get_contents('https://example.com/', false, $context);

// 設定 curl
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://example.com/');
curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);

注意:不建議設定 verify_peer = false,這會完全關閉 SSL 驗證,造成安全風險。


常見憑證路徑對照表

系統/環境curl 憑證路徑PHP OpenSSL 憑證路徑
Alpine Linux/etc/ssl/certs/ca-certificates.crt/etc/ssl/cert.pem
Debian/Ubuntu/etc/ssl/certs/ca-certificates.crt/etc/ssl/certs/ca-certificates.crt
CentOS/RHEL/etc/pki/tls/certs/ca-bundle.crt/etc/pki/tls/cert.pem
部分 PHP 編譯版本/usr/lib/ssl/cert.pem

疑難排解

Q1: 更新憑證後仍然失敗?

檢查是否所有路徑都已更新:

# 找出所有可能的憑證路徑
find /etc/ssl -name "*.pem" -o -name "*.crt" 2>/dev/null
find /usr/lib/ssl -name "*.pem" 2>/dev/null
find /usr/share/ca-certificates -type f 2>/dev/null

Q2: 如何確認憑證檔案是否有效?

# 檢查憑證檔案內容
openssl x509 -in /etc/ssl/certs/ca-certificates.crt -text -noout | head -20

# 測試特定網站的憑證鏈
openssl s_client -connect www.google.com:443 -CAfile /etc/ssl/certs/ca-certificates.crt

Q3: Composer 仍然報錯?

清除 Composer 快取後重試:

composer clear-cache
composer install

Q4: 如何自動化憑證更新?

在 CI/CD 流程中加入定期重建 image 的步驟,或使用以下 cron 任務:

# 每月更新一次憑證
0 0 1 * * curl -o /etc/ssl/certs/ca-certificates.crt https://curl.se/ca/cacert.pem

參考資源


總結

解決 Docker PHP 容器 SSL 憑證問題的關鍵步驟:

  1. 診斷:使用 curl -vopenssl_get_cert_locations() 確認問題和路徑
  2. 下載:從 curl.se 取得最新 CA bundle
  3. 更新:將憑證複製到 curl 和 PHP 實際讀取的路徑
  4. 驗證:測試 HTTPS 連線是否正常
  5. 永久化:修改 Dockerfile 並重建 image

最後更新:2026年 1 月

適用環境:Laradock php-fpm、php-worker、Alpine Linux、Debian/Ubuntu

404NOTE
404NOTE
文章: 48

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *