本機開發

我 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

核心概念是:

  • DB 與檔案不要放 compute machine
  • app server 可以重建
  • prod / staging 分開
  • 一台機器只放一個客戶
  • host paths 固定成:
/home/site-prod
/home/site-staging

container 內則可以用:

/var/www/html

或:

/app

當時覺得這樣很乾淨。


Staging / Production 的設計

我後來判斷:同一台機器跑 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 的判斷

我一開始想把 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 要不要用 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 實際踩到的麻煩

真正開始部署後,Docker Compose 並沒有讓事情變簡單。

我遇到幾類問題:

1. Docker 沒裝

新 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

但這件事本身已經是部署摩擦。


2. Composer 在 container 裡缺東西

跑:

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 裡。


3. Laravel .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。

這讓問題變得很煩:

  • Laravel .env
  • Docker Compose .env
  • container environment
  • config cache
  • volume mount
  • working_dir

這幾層全部混在一起,debug 體驗很差。


4. Volume / path / working_dir 很容易混亂

我必須一直確認:

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 成本變高。


5. APP_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 的實驗

我也測了 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 的感想

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 的觀察

我後來想到 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

所以既有案子不能直接上。


FrankenPHP 官方 production 文件帶來的結論

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。


Ubuntu direct install 也沒有多爽

我也思考過不用 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。


最終結論:Forge 很可能最適合我的接案生涯

繞了一圈後,我得到的結論是:

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

這些正是我最不想每案重做的東西。


⭐️ Shopify 網站開發服務(給品牌)
https://job.turn.tw/shopify-services

⭐️ 小網站開發服務(功能明確、規模不大的需求)
https://job.turn.tw/small-website-services

⭐️ 台灣 Shopify 商家交流 LINE 群(非官方)
https://line.me/ti/g2/PZ_1LILWVWWuzZQ50HNpYA-A3k6QXWF6znqoBQ

⭐️ 台灣 Shopify 開發者 LINE 群(非官方)
https://line.me/ti/g2/YUasX5K3CJ4QdIx76zppjHlh3-q8w-xkSyK1LA

共有 0 則留言


⭐️ Shopify 網站開發服務(給品牌)
https://job.turn.tw/shopify-services

⭐️ 小網站開發服務(功能明確、規模不大的需求)
https://job.turn.tw/small-website-services

⭐️ 台灣 Shopify 商家交流 LINE 群(非官方)
https://line.me/ti/g2/PZ_1LILWVWWuzZQ50HNpYA-A3k6QXWF6znqoBQ

⭐️ 台灣 Shopify 開發者 LINE 群(非官方)
https://line.me/ti/g2/YUasX5K3CJ4QdIx76zppjHlh3-q8w-xkSyK1LA
🏆 本月排行榜
🥇
站長阿川
📝14   💬2   ❤️1
661
🥈
我愛JS
📝1   ❤️1
70
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次