自由學習的風

幽夢影 張潮 少年讀書,如隙中窺月;中年讀書,如庭中望月;老年讀書,如臺上玩月。皆以閱歷之淺深,為所得之淺深耳。

Laravel 排程在 Docker 中執行

2026年7月28日 星期二

Laravel 專案有排程要執行,但是 php-fpm container 裡沒有 crontab 指令,這是很常見的情況。

這時候不要急著把 cron 裝進 php-fpm。php-fpm 本來是處理網站 request 的,排程可以用別的方式做。

需求:

要定時執行:

php artisan forum:sync

例如每天 4 點和 15 點各執行一次。

方法一:由 Linux 主機 crontab 執行

可以直接在 host 的 crontab 裡呼叫 container 裡的 artisan:

docker exec -w /var/www/default php-fpm php artisan forum:sync

先手動測一次:

docker exec -w /var/www/default php-fpm php artisan forum:sync

如果可以正常執行,再加入 crontab:

crontab -e

每天 04:00 和 15:00 執行:

0 4,15 * * * /usr/bin/flock -n /tmp/forum-sync.lock /usr/bin/docker exec -w /var/www/default php-fpm php artisan forum:sync >> /var/log/forum-sync.log 2>&1

指令說明:

  1. 0 4,15 * * *

每天 4 點和 15 點執行。

  1. /usr/bin/flock -n /tmp/forum-sync.lock

避免上一輪還沒跑完,下一輪又啟動。

  1. /usr/bin/docker exec

從 host 執行 container 裡的指令。

  1. -w /var/www/default

指定 Laravel 專案所在目錄。

  1. >> /var/log/forum-sync.log 2>&1

把執行結果和錯誤訊息寫入 log。

方法二:建立 scheduler container

如果想比較 Docker 化,也可以另外建立一個 scheduler service:

laravel-scheduler:
  image: "${PHP_VERSION}"
  container_name: laravel-scheduler
  working_dir: /var/www/default
  volumes:
    - ./wwwroot:/var/www
    - ./session:/var/lib/php/session
  environment:
    TZ: ${TZ}
  command: sh -c "while true; do php artisan schedule:run --verbose --no-interaction; sleep 60; done"
  restart: unless-stopped
  depends_on:
    - mysql

啟動:

docker compose up -d laravel-scheduler

這種方式適合有使用 Laravel scheduler,也就是在 app/Console/Kernel.php 裡集中管理排程的專案。

常見錯誤:

如果出現:

Could not open input file: artisan

代表 -w /var/www/default 不是 Laravel 專案所在目錄,要改成實際路徑。

如果實際使用 PHP 8.3 container,就要把 container 名稱改成:

php83-fpm

結論:

php-fpm 裡沒有 crontab 不算問題。

簡單需求可以用 host crontab 呼叫:

docker exec ...

比較完整的 Laravel 排程,則可以用獨立 scheduler container。

不要把所有功能都塞進 php-fpm,之後比較好維護。

PHP-FPM Docker image 選擇與重建


最近整理 PHP-FPM 的 Dockerfile,順便把 PHP 8.4、PHP 8.2、PHP 8.3、PHP 7.4 的 image 選擇記錄一下。

PHP 8.4 要選哪一個?

如果是一般 Laravel、Symfony、WordPress 或自己寫的 PHP 網站,建議先用:

FROM php:8.4-fpm

如果想更明確,可以寫:

FROM php:8.4-fpm-trixie

Debian-based image 比較適合一般網站環境,extension 和系統套件比較不容易踩到奇怪的相容性問題。

Alpine base 呢?

Alpine image 比較小,例如:

FROM php:8.4-fpm-alpine

但 Alpine 使用 musl libc,有時 PHP extension、Composer package 或外部 binary 會比較麻煩。

除非真的很在意 image 大小,不然一般網站我會先選 Debian 版。

不建議用 buster

buster 是比較舊的 Debian 系列,不建議再拿來當新的 PHP 8.4 image 基礎。

如果要固定 Debian 版本,可以考慮:

FROM php:8.4-fpm-trixie

或依需求使用 bookworm 系列。

官方 image 更新後要重新 build

如果官方 php:8.4-fpm 有更新,本機自建 image 不會自己變新。

要重新 build,建議加上 --pull:

docker build --pull -f Dockerfile_php84-fpm -t tiebob/php:8.4-fpm .

如果用 Compose:

docker compose build --pull
docker compose up -d

如果想完全不用 cache:

docker compose build --pull --no-cache

Dockerfile 整理重點:

這次整理 Dockerfile 時,主要做了幾件事:

  1. 明確指定 base image
  2. apt-get update、apt-get install、清 cache 放在同一層
  3. 加上 --no-install-recommends
  4. 移除 production image 不必要的 vim
  5. Composer 改成從官方 image 複製

Composer 可以這樣裝:

COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer

這樣比在 Dockerfile 裡 curl installer 清楚一些。

PHP 7.4:

後來也建立了 PHP 7.4 的 Dockerfile:

FROM php:7.4-fpm-bullseye

不過 PHP 7.4 已經是 EOL,只建議給舊系統短期使用,不適合新的正式服務。

所以如果專案裡還有 PHP 7.4,最好在檔案裡加註解,提醒自己這是 legacy 用途。

結論:

我的選擇大概是:

一般網站:php:8.4-fpm 或 php:8.4-fpm-trixie
想省 image 大小:php:8.4-fpm-alpine
舊系統相容:php:7.4-fpm-bullseye,但要標示風險

Image 不一定要追最新,但版本和用途要寫清楚,之後維護才不會搞混。

Docker container 的 UID/GID 權限整理

示意圖:Docker UID/GID 權限關係


這次整理 Docker 多網站環境時,遇到一個很常見的問題:不同 container 裡的使用者 UID/GID 不一樣,結果 bind mount 到 host 的檔案權限就容易亂掉。

問題:

目前有這些服務:

  1. nginx
  2. php-fpm
  3. php82-fpm
  4. php83-fpm
  5. mysql

PHP-FPM 大多已經是:

uid=1000(www) gid=1000(www)

但是 Nginx worker 可能是:

uid=101(nginx) gid=101(nginx)

MySQL 則是自己的:

uid=27(mysql) gid=27(mysql)

這時候如果 PHP 建立檔案、Nginx 要讀檔,或者 host 要直接修改檔案,就可能遇到權限問題。

原則:

不要把所有 container 都硬改成同一個 UID。

應該分成兩類:

網站檔案:
nginx、php-fpm、php82-fpm、php83-fpm

資料庫資料:
mysql

也就是網站相關服務統一,MySQL 保持自己的使用者。

建議目標:

nginx worker -> 1000:1000
php-fpm      -> 1000:1000
php82-fpm    -> 1000:1000
php83-fpm    -> 1000:1000
mysql        -> 保持 mysql 預設 UID/GID

網站目錄可以整理成:

sudo chown -R 1000:1000 ./wwwroot
sudo chown -R 1000:1000 ./session
sudo chown -R 1000:1000 ./log/nginx

MySQL 資料目錄不要一起改。

如果 MySQL container 裡查到是 27:27:

sudo chown -R 27:27 ./data/mysql/data

Nginx 注意事項:

原本以為可以直接在 nginx.conf 寫:

user 1000 1000;

結果啟動時出現:

getpwnam("1000") failed

原因是 Nginx 的 user 指令會去找使用者名稱,不是單純用數字 UID。

比較好的方式是自建 Nginx image,在 image 裡建立一個 www 使用者,UID/GID 是 1000:1000,然後 nginx.conf 寫:

user www www;

驗證:

先讓 PHP 建立測試檔:

docker compose exec php-fpm sh -c 'echo ok > /var/www/permission-test.txt'

再讓 Nginx 讀取:

docker compose exec nginx sh -c 'cat /var/www/permission-test.txt'

回 host 看 owner:

ls -lan ./wwwroot/permission-test.txt

如果看到:

1000 1000

而且 Nginx 也能讀到 ok,表示權限大致整理好了。

結論:

遇到 Docker 權限問題,不要先用:

chmod -R 777

這只是把問題蓋掉。

比較好的做法是先弄清楚誰要讀、誰要寫,再把共同使用同一批檔案的 container 對齊 UID/GID。

Docker 多網站部署架構整理

示意圖:Docker 多網站部署架構








圖片檔 blog-drafts/images

目前在 Linux 主機上利用 Docker 跑多個網站,這次順手把目前的建置方式整理一下,避免之後要調整時又要重新看一次。

環境:

  1. Linux 主機
  2. Docker / Docker Compose
  3. Nginx 或 Caddy 做 Web 入口
  4. 多個 PHP-FPM container
  5. MySQL container

目前架構:

大致上是這樣:

Browser
  ↓
Nginx / Caddy
  ↓
依不同網站轉給不同 PHP-FPM
  ↓
MySQL

這樣的做法有點像把傳統的 LEMP 主機,改成用 Docker 來管理。

目的:

  1. 一台主機可以放多個 PHP 網站
  2. 不同網站可以使用不同 PHP 版本
  3. 網站目錄、log、資料庫資料都放在 host 上,方便備份
  4. Web server 統一處理 80、443、SSL、virtual host
  5. 需要瀏覽器自動化時,可以用 geckodriver container

目前比較像「共享主機 Docker 化」,不是每個網站都獨立一整組服務。

建議調整:

1. 不要使用 latest

例如:

NGINX_VERSION=nginx:latest

建議改成固定版本:

NGINX_VERSION=nginx:1.29.3-alpine

不然之後重新 build 或 pull 時,可能會拉到不同版本,問題會比較不好追。

2. PHP 7.4 要列為舊系統

如果還有網站需要 PHP 7.4,可以先保留,但要知道這已經是 legacy 環境。

建議做法:

  1. 先列出哪些網站還需要 PHP 7.4
  2. 確認不能升級的原因
  3. 有計畫地改到 PHP 8.2 / 8.3 / 8.4

3. MySQL 不一定要開給外部

如果只有 container 裡的網站會連 MySQL,通常不需要:

ports:
  - 3306:3306

比較保守可以改成:

ports:
  - 127.0.0.1:3306:3306

或者乾脆不要 publish port,只讓 Docker network 內部連線。

4. volumes 和 bind mount 要整理

目前像這些目錄都會影響備份:

wwwroot/
data/mysql/data/
etc/nginx/
etc/caddy/
log/
session/

建議之後整理一份備份清單,才不會只備份 image,卻漏掉真正重要的網站資料。

結論:

這個架構是可行的,而且蠻適合維護多個 PHP 網站。

接下來優先處理的順序,我覺得可以是:

  1. 固定 image 版本
  2. 整理 Nginx / PHP-FPM 權限
  3. 收斂 MySQL 對外連線
  4. 補上 healthcheck
  5. 將 PHP 7.4 網站逐步升級
  6. 評估改用 Caddy 簡化 HTTPS

先把基礎整理好,之後要搬主機、重建服務或排除錯誤都會輕鬆很多。