部署上線:從 Laravel Forge 到容器化的三條路
本篇是「PHP/Laravel 完全指南」系列的第 14 / 15 篇。你可以從系列總覽開始閱讀,也可以直接接著看本文。
程式寫好了、測試通過了、功能也確認沒問題了。然後呢?「部署」這件事聽起來很簡單——把程式碼放到伺服器上就好了嘛——但實際操作起來,從環境設定、效能調校、SSL 憑證、到零停機部署,每一步都有它的眉角。選錯部署方式,你可能花更多時間在維運上,而不是開發新功能。
Laravel 生態系提供了三條截然不同的部署路線:Laravel Forge 讓你在 DigitalOcean 或 AWS 上一鍵部署,幾乎不用碰伺服器設定;Docker 容器化讓你的應用在任何環境都能一致運行;Cloud Run 和 Fly.io 這類雲端平台則讓你連伺服器都不用管。三條路各有優缺點,適合不同階段和不同規模的團隊。
這一章我們會走過上線前的必要準備——Config Cache、Route Cache、Octane 加速——然後實際帶你用兩種方式部署揪好買:Forge 的一鍵流程和 Docker 的容器化流程。不管你最後選哪條路,上線前該做的事情都一樣,而這些知識會讓你省下無數個半夜被叫起來修 Bug 的夜晚。
Laravel 部署上線前的 Production 設定
不管你選哪條部署路線,上線前有一份 checklist 是每個 Laravel 專案都必須走過的。這些設定漏掉任何一項,輕則效能低落,重則資料外洩。
環境變數核心設定
打開你的 .env(或部署平台的環境變數設定),確認這三項:
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
APP_ENV=production 告訴 Laravel 現在是正式環境,會影響一些行為——例如錯誤頁面不會顯示堆疊追蹤、某些開發工具會被停用。APP_DEBUG=false 絕對不能忘記——如果設成 true,使用者在瀏覽器裡就能看到你的完整錯誤訊息,包含資料庫密碼、API Key、程式碼路徑,等於把系統的所有弱點攤在陽光下。
APP_KEY 是 Laravel 用來加密 Session、Cookie、Eloquent 加密欄位的密鑰。如果你在部署時沒有設定,所有加密功能都會失效。在新環境第一次部署時執行:
php artisan key:generate
警告: 正式環境的
APP_KEY一旦設定就不要隨意更換。換了之後,所有舊的加密資料(包含使用者的 Session)都會失效,等同於強制所有使用者重新登入。
上線前完整 Checklist
# 1. 環境設定
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:... (已設定)
# 2. 資料庫
DB_CONNECTION=mysql # 或 pgsql
DB_HOST=your-production-db # 不要用 127.0.0.1,除非 DB 在同一台機器
DB_DATABASE=jiuhaobuy
DB_USERNAME=jiuhaobuy_user # 不要用 root
DB_PASSWORD=strong-password # 至少 20 字元
# 3. Cache & Session
CACHE_STORE=redis # 正式環境用 Redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
# 4. Mail(正式環境不要用 log)
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailgun.org # 或 SES、Postmark
# 5. URL
APP_URL=https://jiuhaobuy.com
執行 Migration
在正式環境執行 migration 時,加上 --force flag——因為 Laravel 在 production 環境會要求確認:
php artisan migrate --force
最佳做法: 不要直接 SSH 進伺服器手動跑 migration。把它放進你的部署腳本(deploy script),讓它在每次部署時自動執行。Forge 和 Docker 都支援這種做法,後面會示範。
Queue Worker 設定
上一章我們用 php artisan queue:work 在本地跑 Queue Worker。正式環境你需要一個 process manager 來確保 worker 持續運行。最常用的是 Supervisor:
# /etc/supervisor/conf.d/jiuhaobuy-worker.conf
[program:jiuhaobuy-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/jiuhaobuy/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/jiuhaobuy/storage/logs/worker.log
Scheduler 設定(Cron)
Laravel 的 Task Scheduler 需要一個 cron job 每分鐘觸發一次。在伺服器上執行 crontab -e,加上這一行:
* * * * * cd /var/www/jiuhaobuy && php artisan schedule:run >> /dev/null 2>&1
這一行 cron 會每分鐘執行 schedule:run,然後由 Laravel 內部判斷哪些排程任務該在這一分鐘執行。上一章設定的截止提醒通知、定時清理過期團購,都靠這一行 cron 來驅動。
Config / Route / View Caching:榨乾每一滴效能
Laravel 在每次 request 進來時,預設會重新讀取所有 config 檔案、解析所有路由、編譯 Blade 模板。這在開發時很方便——改了 config 馬上生效——但在正式環境,這些重複的 I/O 和解析是純粹的浪費。
Config Cache
php artisan config:cache
這個指令把 config/ 目錄下所有的設定檔合併成一個 PHP 檔案 bootstrap/cache/config.php。Laravel 載入一個檔案的速度遠快於掃描整個目錄。效果有多好?在有 30 個 config 檔案的專案裡,這一個指令就能減少 request bootstrap 時間約 40-60%。
注意事項: 一旦 cache 了 config,env() function 只能在 config 檔案裡使用。如果你在 Controller 或 Model 裡直接呼叫 env('SOME_VAR'),它會回傳 null。正確做法是永遠透過 config('services.some_var') 來取值,而不是直接呼叫 env()。
Route Cache
php artisan route:cache
把所有路由編譯成序列化的 PHP 陣列。Laravel 載入預編譯的路由比每次解析 routes/web.php 和 routes/api.php 快非常多——路由越多效果越明顯。有上百條路由的專案,啟動時間可以減少 50% 以上。
View Cache
php artisan view:cache
預先編譯所有 Blade 模板成 PHP 檔案,存放在 storage/framework/views/。正式環境不該讓第一個使用者承受模板編譯的延遲。
Event Cache
php artisan event:cache
把 Event 和 Listener 的對應關係快取起來,省去每次 request 掃描和自動發現的開銷。
一鍵全部搞定
Laravel 提供了一個 optimize 指令,做上面所有快取的事:
php artisan optimize
這等同於執行 config:cache + route:cache + view:cache + event:cache。部署腳本裡通常就放這一行。
開發時如果要清除所有快取:
php artisan optimize:clear
Before / After 比較
以「揪好買」的實際測量為例,在一台 1 vCPU / 1GB RAM 的伺服器上:
| 指標 | 未快取 | 快取後 | 改善 |
|---|---|---|---|
| 首次 request 回應時間 | ~180ms | ~45ms | 75% |
| Config 載入時間 | ~12ms | ~1ms | 92% |
| Route 解析時間 | ~8ms | ~1ms | 88% |
| 記憶體使用 | ~32MB | ~24MB | 25% |
這些數字告訴你:上線前跑 php artisan optimize 不是選配,是必做。
路線一:Laravel Forge 一鍵部署
如果你不想花時間在伺服器管理上——安裝 PHP、設定 Nginx、調整防火牆、更新系統套件——Laravel Forge 就是你的答案。它是 Laravel 官方團隊提供的伺服器管理服務。定價分為兩個主要方案:$12/月的 Hobby 方案支援無限台 Laravel 自家 VPS,但只允許連接 1 台外部主機商(DigitalOcean、AWS、Hetzner 等)的伺服器;若要連接無限台外部雲端主機商伺服器,需升級至 $19/月的 Growth 方案。對「揪好買」這種單機小專案,Hobby 方案已經足夠。
Forge 是什麼
Forge 不是主機商——它不賣伺服器。它是一個管理層:你提供 DigitalOcean、AWS、Hetzner、Vultr 等主機商的 API Key,Forge 幫你自動建立(provision)伺服器,然後持續管理它。Forge 幫你做的事包括:
- 安裝 PHP(支援多版本切換)、Nginx、MySQL/PostgreSQL、Redis
- 設定防火牆規則、SSH Key 管理
- 自動化部署:連結 GitHub repo,Push 到 main 就自動部署
- SSL 憑證:Let’s Encrypt 一鍵設定、自動續約
- 排程任務和 Queue Worker 管理
- 資料庫備份
部署流程
第一步:註冊並連結主機商
在 forge.laravel.com 註冊帳號。到 Server Providers 頁面,把你的 DigitalOcean(或其他主機商)API Token 填進去。
第二步:建立伺服器
點「Create Server」,選擇主機商、地區、規格。以「揪好買」來說,一台 DigitalOcean $6/月的 Droplet(1 vCPU / 1GB RAM)就足以應付初期流量。Forge 會花大約 10 分鐘自動安裝所有需要的軟體。
第三步:連結 GitHub Repo
在 Forge 的 Sites 區塊點「New Site」,填入你的 domain name(例如 jiuhaobuy.com),然後連結 GitHub repository。Forge 會在伺服器上 clone 你的程式碼。
第四步:設定 Deploy Script
Forge 預設的 deploy script 大概長這樣:
cd /home/forge/jiuhaobuy.com
git pull origin $FORGE_SITE_BRANCH
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
php artisan migrate --force
php artisan optimize
# 如果你用了 npm assets
npm ci
npm run build
php artisan queue:restart
每次你 Push 到 GitHub,Forge 就會自動執行這段腳本。
第五步:SSL 憑證
在 Forge 的 SSL 區塊,點「Let’s Encrypt」,填入你的 domain,點確認。Forge 會自動申請、設定、並且在到期前自動續約。整個過程不到一分鐘。
Forge 的優缺點
| 優點 | 缺點 |
|---|---|
| 幾乎零學習曲線,UI 操作 | 月費 $12(加上主機費用) |
| 自動化部署、SSL、備份 | 你的伺服器管理依賴 Forge |
| 適合個人開發者和小團隊 | 不適合需要複雜基礎設施的場景 |
| 官方維護,與 Laravel 整合最好 | 如果哪天想離開,需要自己接手伺服器管理 |
對於「揪好買」這種小型到中型的專案,Forge 是最快上線的方式。你可以把省下來的時間拿去做功能開發,而不是除錯 Nginx 設定。
路線二:Docker 容器化部署
Docker 解決的核心問題是:「在我電腦上可以跑」和「在伺服器上可以跑」不再是兩件事。 你把應用程式和它的所有依賴(PHP 版本、擴充套件、系統套件)打包成一個 image,這個 image 在任何有 Docker 的機器上都能一致地運行。
為什麼要用 Docker
- 環境一致性——開發、CI、正式環境用同一個 image,不會有「PHP 版本不對」的問題
- 可攜性——image 可以跑在 AWS、GCP、Azure,或你辦公室的 NAS 上
- CI/CD 友善——Build image → Push to registry → Deploy,流程標準化
- 隔離性——每個服務跑在自己的 container,互不干擾
Dockerfile:多階段建置
多階段建置(multi-stage build)讓你的 production image 只包含必要的檔案,不會把 Composer、npm、開發工具帶進去:
# ===== 第一階段:安裝 PHP 依賴 =====
FROM composer:2 AS composer-build
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
COPY . .
RUN composer dump-autoload --optimize
# ===== 第二階段:建置前端資源 =====
FROM node:24-alpine AS node-build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# ===== 第三階段:Production Image =====
FROM php:8.4-fpm-alpine
# 安裝系統依賴和 PHP 擴充
RUN apk add --no-cache \
nginx \
supervisor \
libpng-dev \
libzip-dev \
icu-dev \
&& docker-php-ext-install \
pdo_mysql \
gd \
zip \
intl \
opcache \
pcntl
# 複製 Nginx 設定
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
# 複製 Supervisor 設定
COPY docker/supervisord.conf /etc/supervisord.conf
# 複製應用程式碼
WORKDIR /var/www/html
COPY --from=composer-build /app/vendor ./vendor
COPY --from=node-build /app/public/build ./public/build
COPY . .
# 設定權限
RUN chown -R www-data:www-data storage bootstrap/cache \
&& chmod -R 775 storage bootstrap/cache
# OPcache 設定
COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
docker-compose.yml
開發和測試時,用 docker-compose 把所有服務串起來:
# docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:80"
environment:
- APP_ENV=production
- APP_DEBUG=false
- DB_HOST=mysql
- REDIS_HOST=redis
env_file:
- .env
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
volumes:
- storage:/var/www/html/storage
mysql:
image: mysql:8.4
environment:
MYSQL_DATABASE: jiuhaobuy
MYSQL_USER: jiuhaobuy_user
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: root_secret
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
queue-worker:
build:
context: .
dockerfile: Dockerfile
command: php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
env_file:
- .env
environment:
- DB_HOST=mysql
- REDIS_HOST=redis
depends_on:
- app
scheduler:
build:
context: .
dockerfile: Dockerfile
command: sh -c "while true; do php artisan schedule:run --verbose --no-interaction & sleep 60; done"
env_file:
- .env
environment:
- DB_HOST=mysql
- REDIS_HOST=redis
depends_on:
- app
volumes:
mysql-data:
redis-data:
storage:
建置與執行
# 建置 image
docker compose build
# 啟動所有服務
docker compose up -d
# 第一次啟動:跑 migration 和 cache
docker compose exec app php artisan migrate --force
docker compose exec app php artisan optimize
# 查看 logs
docker compose logs -f app
# 停止所有服務
docker compose down
推送到 Container Registry
正式部署時,你會把 image 推到 container registry(Docker Hub、GitHub Container Registry、Google Artifact Registry 等),然後在伺服器或雲端平台上拉取這個 image:
# 建置並標記 image
docker build -t ghcr.io/your-org/jiuhaobuy:latest .
docker build -t ghcr.io/your-org/jiuhaobuy:v1.0.0 .
# 推送到 GitHub Container Registry
docker push ghcr.io/your-org/jiuhaobuy:latest
docker push ghcr.io/your-org/jiuhaobuy:v1.0.0
建議: 除了
latesttag,永遠也打一個版本號 tag(例如v1.0.0或 Git commit SHA)。這樣你可以隨時回滾到指定版本。
Laravel Octane:Swoole / FrankenPHP 加速
傳統的 PHP 執行模式是:每個 request 進來 → 載入框架 → 處理 request → 回應 → 丟掉所有東西。下一個 request 再從頭來一次。這就像每次客人進餐廳,你都要重新蓋一次廚房、擺一次桌椅、然後煮完菜再把整間餐廳拆掉。
Laravel Octane 改變了這個模式:它在第一次啟動時載入整個框架到記憶體裡,之後每個 request 都是在已經準備好的環境中處理,不需要重新 bootstrap。
FrankenPHP vs Swoole
Octane 支援三種 server:
| Server | 特點 | 適合場景 |
|---|---|---|
| FrankenPHP | Go 實作,簡單易用,支援 HTTP/3 | 推薦首選,設定最少 |
| Swoole | C 擴充,功能最多(WebSocket、協程) | 需要 WebSocket 或高併發場景 |
| RoadRunner | Go 實作,穩定成熟 | 需要長期穩定的場景 |
對大多數 Laravel 應用來說,FrankenPHP 是目前最推薦的選擇——它是 Caddy web server 的一部分,安裝簡單,而且自帶 HTTPS 支援(Caddy 的 automatic HTTPS)。
安裝與設定
composer require laravel/octane
php artisan octane:install
# 選擇 frankenphp
啟動 Octane:
# 開發環境(帶 watch 模式,改檔案自動重啟)
php artisan octane:start --watch
# 正式環境
php artisan octane:start --host=0.0.0.0 --port=8000 --workers=4
效能提升
在「揪好買」的實測環境(同一台 1 vCPU 機器),使用 Apache Bench 測試 /api/group-buys endpoint:
| 指標 | PHP-FPM | Octane (FrankenPHP) | 提升幅度 |
|---|---|---|---|
| Requests/sec | ~120 | ~450 | 3.7x |
| 平均回應時間 | ~83ms | ~22ms | 3.8x |
| P99 回應時間 | ~250ms | ~65ms | 3.8x |
| 記憶體使用 | 每 request 獨立 | 共享 ~80MB | 整體更省 |
吞吐量提升 3-5 倍是常見的數字。對於流量還小的新專案,你可能感覺不到差異;但當流量成長到一定程度,Octane 可以讓你晚幾個月才需要升級伺服器規格。
注意事項:記憶體和靜態狀態
Octane 把應用保持在記憶體裡,這帶來一個重要的副作用:靜態變數和全域狀態會跨 request 存活。
// ❌ 危險:這個計數器會在所有 request 之間共享
class SomeService
{
private static int $count = 0;
public function handle(): void
{
self::$count++; // 每個 request 都會累加
}
}
// ✅ 正確:用 request-scoped 的方式
class SomeService
{
public function handle(Request $request): void
{
// 從 request 或 container 取值,不用靜態狀態
}
}
Laravel 12 的核心服務都已經處理好 Octane 相容性。但如果你用了第三方套件,需要確認它們是否「Octane-friendly」。常見的陷阱:
- 不要在 Service 裡存 request 相關的狀態——每個 worker 會處理多個 request
- 不要在 singleton 服務裡快取 per-request 資料——它不會在下一個 request 被清除
- 資料庫連線可能因為太久沒用而斷開——Octane 有內建的連線池管理,但要注意超時設定
路線三:Cloud Run / Fly.io 雲端部署
如果你不想管伺服器,也不想管伺服器上的作業系統更新和安全修補——那就讓雲端平台幫你管。
Google Cloud Run
Cloud Run 的概念很單純:你給它一個 Docker image,它幫你跑起來。沒有流量的時候自動縮到零、有流量的時候自動擴展、SSL 自動搞定、domain mapping 一行指令。而且計費是 per-request——真的沒人用的時候,你一毛錢都不用付。
部署揪好買到 Cloud Run:
# 建置 image 並推送到 Google Artifact Registry
gcloud builds submit --tag asia-east1-docker.pkg.dev/your-project/jiuhaobuy/app:latest
# 部署到 Cloud Run
gcloud run deploy jiuhaobuy \
--image asia-east1-docker.pkg.dev/your-project/jiuhaobuy/app:latest \
--platform managed \
--region asia-east1 \
--allow-unauthenticated \
--set-env-vars APP_ENV=production,APP_DEBUG=false \
--set-secrets APP_KEY=app-key:latest,DB_PASSWORD=db-password:latest \
--min-instances 0 \
--max-instances 10
Cloud Run 的限制:它是 stateless 的,container 隨時可能被回收。這意味著:
- Session 和 Cache 不能用 file driver——必須用 Redis 或 database
- Queue Worker 需要另外跑——可以用 Cloud Run Jobs 或另一個 always-on 的 Cloud Run service
- Scheduler 要用 Cloud Scheduler 觸發——設定一個每分鐘打一次
/scheduleendpoint 的 cron job
Laravel Cloud(官方第一方 serverless)
如果要談「serverless 部署」,2026 年最對口的官方答案其實是 Laravel Cloud——Laravel 官方在 2025-02-24 隨 Laravel 12 一起推出的全託管部署平台,由 Laravel 團隊自己營運。它和 Laravel 整合最深:git push 就部署,內建 usage-based 計費與 scale-to-zero(閒置時自動縮到零、只付實際運算時間),佇列、排程、資料庫都幫你託管好,省去自己拼湊 Cloud Run Jobs + Cloud Scheduler 的工。
實務上的取捨:Laravel Cloud 上手最快、最不用碰基礎設施,適合想專心寫 Laravel、不想管 DevOps 的團隊;Cloud Run / Fly.io 則給你更多底層掌控與跨雲彈性。如果你已經在 GCP/AWS 生態系裡,Cloud Run 仍是整合最順的選擇;但只要是 Laravel 專案要找 serverless,Laravel Cloud 都值得先列入評估。
Fly.io
Fly.io 的思路和 Cloud Run 類似,但它提供了 persistent volumes(持久化儲存),對需要本地檔案儲存的應用更友善。部署同樣基於 Docker image:
# 安裝 Fly CLI
curl -L https://fly.io/install.sh | sh
# 初始化專案
fly launch
# 部署
fly deploy
Fly.io 的 fly.toml 設定檔:
[build]
dockerfile = "Dockerfile"
[env]
APP_ENV = "production"
APP_DEBUG = "false"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
[[vm]]
size = "shared-cpu-1x"
memory = "512mb"
三條路線比較
| 面向 | Forge | Docker + VPS | Cloud Run / Fly.io |
|---|---|---|---|
| 學習曲線 | 低 | 中 | 中 |
| 初始設定時間 | 30 分鐘 | 2-4 小時 | 1-2 小時 |
| 月費(小型專案) | ~$18 (Forge $12 + VPS $6) | ~$6 (VPS only) | ~$0-5 (pay-per-use) |
| 伺服器管理 | Forge 代管 | 自己管 | 雲端平台代管 |
| 擴展性 | 手動加機器 | 手動或用 K8s | 自動擴展 |
| 自訂程度 | 中 | 高 | 受平台限制 |
| CI/CD 整合 | Git push 自動部署 | 需自己設定 pipeline | Docker image + 一行指令 |
| 適合對象 | 個人/小團隊 | 有 DevOps 經驗的團隊 | Serverless 愛好者 |
成本比較(月流量 10 萬 PV)
| 項目 | Forge + DO | Docker + VPS | Cloud Run |
|---|---|---|---|
| 主機/運算 | $12 (2 vCPU) | $12 (2 vCPU) | ~$8 |
| 管理服務 | $12 (Forge) | $0 | $0 |
| 資料庫 | 含在 VPS | 含在 VPS | ~$10 (Cloud SQL) |
| Redis | 含在 VPS | 含在 VPS | ~$6 (Memorystore) |
| 合計 | ~$24/月 | ~$12/月 | ~$24/月 |
結論:小專案的話,三條路成本差不多。Docker + VPS 最便宜但你要自己管;Forge 和 Cloud Run 花錢買時間。
SSL / HTTPS 設定
2026 年了,HTTPS 不是選配——瀏覽器會把 HTTP 網站標記為「不安全」,SEO 也會被扣分。好消息是,免費的 SSL 憑證現在垂手可得。
Let’s Encrypt(免費、自動續約)
Let’s Encrypt 提供免費的 SSL 憑證,有效期 90 天,支援自動續約。在不同部署方式下的設定方式:
Forge: 前面提過,在 SSL 區塊點「Let’s Encrypt」就搞定了。Forge 會自動設定 Nginx、自動續約。零操作。
Docker + Caddy: 如果你用 Caddy 取代 Nginx 當反向代理,HTTPS 是全自動的——Caddy 預設就會幫你申請和續約 Let’s Encrypt 憑證:
# Caddyfile
jiuhaobuy.com {
reverse_proxy app:8080
}
就這樣。不需要任何額外設定。這也是為什麼越來越多人在 Docker 部署時選擇 Caddy 而不是 Nginx。
Docker + Nginx + Certbot: 如果你堅持用 Nginx,需要搭配 Certbot:
# 安裝 Certbot
apt install certbot python3-certbot-nginx
# 申請憑證
certbot --nginx -d jiuhaobuy.com -d www.jiuhaobuy.com
# 自動續約(Certbot 會自動設定 cron)
certbot renew --dry-run
Cloud Run / Fly.io: 自動提供 HTTPS,不需要任何設定。
Laravel 的 HTTPS 設定
在 AppServiceProvider 裡強制所有 URL 使用 HTTPS:
public function boot(): void
{
if ($this->app->environment('production')) {
URL::forceScheme('https');
}
}
同時在 .env 確認 APP_URL 使用 https://:
APP_URL=https://jiuhaobuy.com
Zero-Downtime 部署策略
使用者正在瀏覽你的網站,你按下部署按鈕,程式碼更新過程中——使用者看到一個 500 錯誤。這就是「有停機時間的部署」的問題。對一個團購平台來說,如果在結單的關鍵時刻出現錯誤,直接就是商譽和金錢的損失。
問題出在哪
傳統的 git pull + composer install 部署方式,中間有一段過渡期:舊的程式碼已經被覆蓋了,但新的依賴還沒裝好、cache 還沒更新。這段期間的 request 就會出錯。
Atomic Symlink 部署(Forge / Envoyer)
原理:每次部署建立一個新的完整目錄(例如 releases/20260831120000/),在新目錄裡把所有東西準備好——Composer install、npm build、migration、cache——然後用一個 symlink 切換,把 current/ 指向新的 release 目錄。Symlink 的切換是原子操作(atomic),不會有中間狀態。
/var/www/jiuhaobuy/
├── releases/
│ ├── 20260830_120000/ ← 上一版
│ └── 20260831_120000/ ← 新版(已經準備好了)
├── current -> releases/20260831_120000/ ← symlink 一切換,馬上生效
├── storage/ ← 所有 release 共用
└── .env ← 所有 release 共用
Laravel Forge 內建就支援這種部署模式。2026 年所有新的 Forge 訂閱(含 $12 Hobby 方案)都已內建單機 zero-downtime / atomic 部署,官方明確說明「不需要再額外訂閱 Envoyer」。Laravel Envoyer($12/月)現在的價值在於「把同一個專案部署到多台伺服器」這種多機情境——如果你只是單機部署,Forge 本身就夠用,不必為了 zero-downtime 再多付這筆錢。
Docker Rolling Update
Docker 環境的零停機部署靠的是 rolling update:先啟動新版 container,確認健康檢查通過後,再停止舊版 container。
如果用 Docker Compose:
# 建置新 image
docker compose build app
# 滾動更新(先起新的,再停舊的)
docker compose up -d --no-deps --build app
在 Cloud Run 或 Kubernetes 環境,rolling update 是預設行為——你部署新版本後,平台會自動漸進式地把流量切換到新的 container。
Blue-Green 部署
更進階的做法:同時維護兩套完全相同的環境(Blue 和 Green)。目前 Blue 在服務使用者,你把新版本部署到 Green,測試通過後,把 load balancer 切換到 Green。如果新版本有問題,馬上切回 Blue——回滾時間幾乎是零。
這種做法的缺點是成本比較高(你要維護兩套環境),所以通常是有一定規模的團隊才會採用。「揪好買」初期用 Forge 的 atomic symlink 或 Docker 的 rolling update 就綽綽有餘。
環境變數管理:Production 的 .env
.env 檔案裡有你的資料庫密碼、API Key、加密金鑰——這些東西如果外洩,後果不堪設想。管理 production 環境變數有幾個鐵律:
絕對不要 Commit .env 到 Git
確認 .gitignore 裡有 .env。如果你不小心 commit 過,光是刪掉檔案不夠——歷史紀錄裡還在。你需要:
- 立即更換所有外洩的密鑰(資料庫密碼、API Key 等)
- 用
git filter-branch或 BFG Repo-Cleaner 清除 Git 歷史
各部署方式的環境變數管理
Forge: 在伺服器的 Environment 頁面直接編輯。Forge 會把內容寫到伺服器上的 .env 檔案,並設定正確的檔案權限。
Docker: 兩種常見做法:
# 方式一:env_file(適合開發和 CI)
services:
app:
env_file:
- .env.production
# 方式二:Docker Secrets(適合正式環境)
services:
app:
secrets:
- db_password
- app_key
secrets:
db_password:
external: true
app_key:
external: true
Cloud Run: 使用 Google Cloud Secret Manager,在部署時透過 --set-secrets 把 secret 注入成環境變數:
# 建立 secret
echo -n "your-database-password" | gcloud secrets create db-password --data-file=-
# 部署時注入
gcloud run deploy jiuhaobuy \
--set-secrets DB_PASSWORD=db-password:latest
金鑰輪替策略
好的安全實踐包括定期更換敏感的金鑰:
- 資料庫密碼——每 90 天更換一次
- 第三方 API Key——依照服務商建議的頻率
- APP_KEY——除非有外洩疑慮,否則不要換(會影響所有加密資料)
- JWT Secret——更換後所有舊的 token 會失效,要在低流量時段操作
每次更換後,更新部署平台的環境變數,然後重新部署或重啟應用。
實作:部署揪好買到正式環境
講了這麼多理論,現在來實際操作。我們提供兩條路線,挑一條適合你的跟著做。
Option A:Forge 快速上線(5 步驟)
步驟 1: 到 forge.laravel.com 註冊,連結你的 DigitalOcean 帳號。
步驟 2: 建立伺服器——選 DigitalOcean、新加坡區域(離台灣最近)、$6/月的 1GB Droplet。等待約 10 分鐘。
步驟 3: 新增 Site——填入 domain(例如 jiuhaobuy.com),連結 GitHub repo,選 main branch。
步驟 4: 設定環境變數——在 Forge 的 Environment 頁面,貼上你的 production .env 內容(記得改 APP_ENV=production、APP_DEBUG=false、設定正確的資料庫連線)。
步驟 5: 按下「Deploy Now」。Forge 會執行 deploy script:拉程式碼、裝依賴、跑 migration、清快取。部署完成後,到 SSL 區塊申請 Let’s Encrypt 憑證。
完成。從註冊到上線大約 20-30 分鐘。
Option B:Docker + Cloud Run(Dockerfile → Build → Deploy)
步驟 1: 確認專案根目錄有前面提到的 Dockerfile。
步驟 2: 在 Google Cloud 建立專案和 Artifact Registry:
# 建立 Artifact Registry
gcloud artifacts repositories create jiuhaobuy \
--repository-format=docker \
--location=asia-east1
# 設定 Secret Manager
echo -n "base64:your-app-key" | gcloud secrets create app-key --data-file=-
echo -n "your-db-password" | gcloud secrets create db-password --data-file=-
步驟 3: 建置並推送 image:
gcloud builds submit \
--tag asia-east1-docker.pkg.dev/your-project/jiuhaobuy/app:v1.0.0
步驟 4: 部署到 Cloud Run:
gcloud run deploy jiuhaobuy \
--image asia-east1-docker.pkg.dev/your-project/jiuhaobuy/app:v1.0.0 \
--platform managed \
--region asia-east1 \
--allow-unauthenticated \
--set-env-vars APP_ENV=production,APP_DEBUG=false,APP_URL=https://jiuhaobuy.com \
--set-secrets APP_KEY=app-key:latest,DB_PASSWORD=db-password:latest \
--min-instances 1 \
--max-instances 10 \
--memory 512Mi
步驟 5: 設定 custom domain 和執行 migration:
# 對應你的 domain
gcloud run domain-mappings create --service jiuhaobuy \
--domain jiuhaobuy.com --region asia-east1
# 執行 migration(用 Cloud Run Jobs)
# 第一次要先建立 job——直接 execute 會得到 job not found
gcloud run jobs create jiuhaobuy-migrate \
--image asia-east1-docker.pkg.dev/your-project/jiuhaobuy/app:latest \
--region asia-east1 \
--set-secrets APP_KEY=app-key:latest,DB_PASSWORD=db-password:latest \
--command php \
--args artisan,migrate,--force
# 建立後即可執行(之後每次部署重跑這行就好)
gcloud run jobs execute jiuhaobuy-migrate --region asia-east1
部署後 Checklist
不管你選哪條路,部署完成後走過這份 checklist:
# 1. 確認應用可以存取
curl -I https://jiuhaobuy.com # 應該回 200
# 2. 確認 HTTPS 正常
curl -I http://jiuhaobuy.com # 應該回 301 redirect 到 https
# 3. 確認 migration 已執行
php artisan migrate:status # 所有 migration 應該都是 Ran
# 4. 確認 cache 已建立
php artisan optimize # 建立 config/route/view/event cache
# 5. 確認 Queue Worker 在運行
php artisan queue:monitor redis:default # 檢查 queue 狀態
# 6. 確認 Scheduler 在運行
# 等一分鐘後檢查 logs,看 schedule:run 有沒有被觸發
# 7. 測試核心功能
# - 註冊/登入
# - 建立團購
# - 加入團購
# - 付款流程(用 Stripe test mode)
小結:選擇適合你的部署方式
部署不是一個技術問題,它是一個「我願意花多少時間在維運上」的決策。
決策矩陣:
- 一個人做 Side Project → Forge。$18/月買回你的時間。
- 有 Docker 經驗、在意成本 → Docker + VPS。$6/月跑起來,但 server 你自己管。
- 已經在用 GCP/AWS → Cloud Run / ECS。跟既有基礎設施整合最順。
- 想要極致效能 → 任何路線都可以加上 Octane。
這一章涵蓋了從上線前設定、效能快取、三種部署路線、SSL、零停機部署、到環境變數管理——這些是 Laravel 應用正式上線的完整知識。不管你選哪條路,核心步驟都一樣:設定環境變數、跑 migration、建立 cache、確保 queue worker 和 scheduler 在跑。
下一章是這本書的最後一章。我們會回顧整個「揪好買」的旅程、盤點你學到的所有技能、展望 Laravel 生態系裡還有哪些強大的工具等著你探索,然後幫你畫一張接下來的進階學習路線圖。你已經具備了從零到上線的完整能力——接下來的路,你可以自己走了。