跳至主要內容
技術

部署上線:從 Laravel Forge 到容器化的三條路

部署上線:從 Laravel Forge 到容器化的三條路
PHP/Laravel 完全指南 第 14 / 15 篇

本篇是「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.phproutes/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~45ms75%
Config 載入時間~12ms~1ms92%
Route 解析時間~8ms~1ms88%
記憶體使用~32MB~24MB25%

這些數字告訴你:上線前跑 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

建議: 除了 latest tag,永遠也打一個版本號 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特點適合場景
FrankenPHPGo 實作,簡單易用,支援 HTTP/3推薦首選,設定最少
SwooleC 擴充,功能最多(WebSocket、協程)需要 WebSocket 或高併發場景
RoadRunnerGo 實作,穩定成熟需要長期穩定的場景

對大多數 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-FPMOctane (FrankenPHP)提升幅度
Requests/sec~120~4503.7x
平均回應時間~83ms~22ms3.8x
P99 回應時間~250ms~65ms3.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 觸發——設定一個每分鐘打一次 /schedule endpoint 的 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"

三條路線比較

面向ForgeDocker + VPSCloud 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 自動部署需自己設定 pipelineDocker image + 一行指令
適合對象個人/小團隊有 DevOps 經驗的團隊Serverless 愛好者

成本比較(月流量 10 萬 PV)

項目Forge + DODocker + VPSCloud 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 就會出錯。

原理:每次部署建立一個新的完整目錄(例如 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 過,光是刪掉檔案不夠——歷史紀錄裡還在。你需要:

  1. 立即更換所有外洩的密鑰(資料庫密碼、API Key 等)
  2. 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=productionAPP_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 生態系裡還有哪些強大的工具等著你探索,然後幫你畫一張接下來的進階學習路線圖。你已經具備了從零到上線的完整能力——接下來的路,你可以自己走了。

留言討論

esc
輸入關鍵字搜尋文章...
查看收藏 →