我 laradock 用很順了 跑十幾個 repo 但就是用太久 停在 php8.1 + mysql8.0
如果需要新版本環境呢?比方說 php8.3 + mysql8.4
我發現在本機 多準備一份 laradock 即可 非常方便 我另外準備腳本
laradock-up() {
laradock83-down
cd ~/laradock-projects/laradock
docker compose up -d nginx mysql phpmyadmin redis workspace
}
laradock-down() {
cd ~/laradock-projects/laradock
docker compose down
}
laradock-bash() {
cd ~/laradock-projects/laradock
docker compose exec workspace bash
}
laradock83-up() {
laradock-down
cd ~/laradock-projects/laradock83
docker compose up -d nginx mysql phpmyadmin redis workspace
}
laradock83-down() {
cd ~/laradock-projects/laradock83
docker compose down
}
laradock83-bash() {
cd ~/laradock-projects/laradock83
docker compose exec workspace bash
}
效果非常好 我很滿意 讓我對大量接案更有信心
為了讓每個客戶都有獨立環境 同時我又方便部署 我設計流程與環境
stateful:
- Linode Managed DB
- Cloudflare R2
stateless:
- Linode Shared Compute
- Caddy
- Docker Compose
- php
- node
- redis
- queue
核心概念是:
/home/site-prod
/home/site-staging
container 內則可以用:
/var/www/html
或:
/app
當時覺得這樣很乾淨。
我後來判斷:同一台機器跑 prod / staging,兩個 folder 最單純。
/home/site-prod
/home/site-staging
兩邊各自:
.env
git repo
runtime config
storage
vendor
prod / staging 用不同:
COMPOSE_PROJECT_NAME
APP_PORT
DB_DATABASE
R2 bucket
third-party API keys
例如:
# production
COMPOSE_PROJECT_NAME=site_prod
APP_PORT=8080
APP_ENV=production
APP_DEBUG=false
# staging
COMPOSE_PROJECT_NAME=site_staging
APP_PORT=8081
APP_ENV=staging
APP_DEBUG=true
Caddy 則 reverse proxy:
www.example.com {
reverse_proxy 127.0.0.1:8080
}
staging.example.com {
reverse_proxy 127.0.0.1:8081
}
這套概念本身沒有錯。
我一開始想把 Makefile 納入 protocol。
後來判斷:
Makefile 不應該是核心,只適合作為 shortcut。
例如:
php:
docker compose exec php bash
art:
docker compose exec php php artisan $(c)
npm:
docker compose run --rm node npm $(c)
logs:
docker compose logs -f
但如果主要只是跑 artisan,其實沒必要多一層抽象。
最後結論:
Required:
- Docker Compose or Forge deploy flow
Optional:
- Makefile as shortcut
我一開始也在想 queue 要不要用 supervisor。
後來得到比較清楚的結論:
queue worker = 獨立長駐 process
scheduler = cron 每分鐘觸發
在 Docker Compose 裡,queue 可以是獨立 service:
queue:
command: php artisan queue:work --sleep=3 --tries=3 --timeout=90
restart: unless-stopped
這樣就不需要在 container 裡再塞 supervisor。
心法:
單一長駐 process -> Docker Compose / systemd 管
同一個環境內多個長駐 process -> 才考慮 Supervisor
這個觀念是對的。
真正開始部署後,Docker Compose 並沒有讓事情變簡單。
我遇到幾類問題:
新 Linode Ubuntu server 上直接跑:
docker compose build
結果:
Command 'docker' not found
這時要決定用 snap、apt、Docker 官方 repo。
最後發現如果只是想無腦,Ubuntu apt 直接裝就好:
apt update
apt install -y docker.io docker-compose-v2 git unzip
systemctl enable --now docker
但這件事本身已經是部署摩擦。
跑:
docker compose exec php composer install --no-dev --optimize-autoloader
遇到:
no unzip nor 7z command installed
Failed to download predis/predis from dist
原因是 php container 裡沒有:
unzip
git
不是 host 裝了就好,因為 composer 是在 container 裡跑。
所以 Dockerfile 又要補:
RUN apt-get update && apt-get install -y \
git \
unzip \
curl \
zip \
&& rm -rf /var/lib/apt/lists/*
這類問題讓我開始覺得 Docker 只是把環境問題搬到 Dockerfile 裡。
.env / APP_KEY 問題我遇到 Laravel 一直報:
Unsupported cipher or incorrect key length
或 config 讀到空的 APP_KEY。
後來發現問題跟 compose.yml 有關。
coding agent 把 Laravel .env 當成 Docker Compose 的:
env_file:
- .env
這其實不該這樣做。
Laravel 自己會讀專案根目錄 .env,Docker Compose 不應該整包拿去當 container environment。
這讓問題變得很煩:
.env.env這幾層全部混在一起,debug 體驗很差。
我必須一直確認:
docker compose exec php pwd
docker compose exec php ls -la
docker compose exec php grep "^APP_KEY=" .env
還要確認 Laravel 實際 base_path() 是哪裡。
如果 working_dir 是 /app,但 volume 掛到 /var/www/html,或反過來,就會出現很難看的問題。
這種問題不是 Laravel 難,是 Docker 多了一層映射後,debug 成本變高。
因為 Laravel production 可能有 config cache:
bootstrap/cache/config.php
所以 .env 改了還要:
php artisan optimize:clear
php artisan config:clear
docker compose restart php queue
這本來我懂,但跟 Docker 疊在一起後,又變成一層要排查的東西。
我也測了 Linode Managed DB。
遇到一個 Laravel 8 舊 migration 問題:
SQLSTATE[HY000]: General error: 3750 Unable to create or change a table without a primary key
原因是 Linode Managed MySQL 預設:
mysql.sql_require_primary_key = Enabled
Laravel 8 預設的 password_resets table 沒有 primary key:
Schema::create('password_resets', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
所以 migration 失敗。
解法有兩種:
在 Linode Managed DB advanced config 關掉:
mysql.sql_require_primary_key
所有 migration 都應該有 primary key。
例如:
$table->id();
Laravel 10 之後預設的 password reset table 比較不會遇到這個問題,因為新表通常有 primary key。
這件事提醒我:
Managed DB 比自架 MySQL 嚴格。
這是好事,但舊 Laravel 專案會踩坑。
Caddy 本身我覺得很不錯。
它解決:
HTTPS
reverse proxy
簡化 web server config
比 Nginx 看起來舒服很多。
但後來我發現:
Caddy 只解決 web server,不解決整體 deployment workflow。
它不會幫我處理:
PHP runtime
Composer
Node build
queue worker
scheduler
deploy script
env UI
server provisioning
permission
rollback
所以 Caddy 很好,但它不是完整答案。
我後來想到 FrankenPHP。
它本身就是 built on top of Caddy,所以如果用 FrankenPHP,其實不一定需要再裝一層 host Caddy。
FrankenPHP 的 Caddyfile 可以這樣:
www.example.com {
root * /home/site-prod/public
encode zstd gzip
php_server
}
staging.example.com {
root * /home/site-staging/public
encode zstd gzip
php_server
}
這很漂亮。
但我目前是:
Laravel 8 + PHP 8.1
FrankenPHP 需要 PHP 8.2+,實務上更適合:
Laravel 10+ / 11 / 12
PHP 8.3
所以既有案子不能直接上。
https://frankenphp.dev/docs/production/
我看了 FrankenPHP production 文件後,發現官方主線還是:
Docker Compose
Dockerfile
compose.yaml
docker compose up
這讓我有點不樂意。
因為我本來是想用 FrankenPHP 逃離:
Nginx
PHP-FPM
Supervisor
複雜部署設定
結果官方 production 文件又把我帶回:
Dockerfile
compose.yaml
image build
cache layer
container env
volume
所以 FrankenPHP 簡化的是:
web server + PHP runtime
但沒有完全簡化:
deployment workflow
因此它很適合未來新案實驗,但不一定適合作為我現在的接案 default。
我也思考過不用 Docker,直接在 Ubuntu 上裝:
PHP
Composer
Node
Caddy / Nginx
Supervisor
cron
但這樣步驟也不少。
差別只是問題從 Docker 變成 host machine:
PHP extensions
permission
systemd
Supervisor
server package version
deploy user
Node version
Composer version
我開始寫 bash 腳本 但我發現 那我乾脆用 forge 不是更方便 = =
我真正討厭的不是 Docker,也不是 Caddy,也不是 FrankenPHP。
我真正討厭的是:
環境設定
server provisioning
runtime setup
web server config
queue / cron / supervisor
deploy script
SSL
env 管理
production debug
我想做的是:
Laravel 商業邏輯
Shopify / CRM / 訂閱整合
DB schema
API 設計
客戶需求拆解
接案 protocol
長期維護
所以我不應該把自己訓練成半個 SRE。
繞了一圈後,我得到的結論是:
Laravel Forge
+ Linode Compute
+ Linode Managed DB
+ Cloudflare DNS / CDN / R2
很可能是我接案 default stack。
Forge 可以幫我處理:
server provisioning
PHP version
Nginx
SSL
Git deploy
Composer install
NPM build
queue worker
scheduler
daemon / supervisor
environment variables
deploy script
這些正是我最不想每案重做的東西。