.env 檔案是每個 Laravel 專案第一個會打開、也最容易被誤用的檔案——今天把它的角色講清楚,順便講幾個實用技巧:怎麼加密 .env 內容、Debug 模式為什麼絕對不能在正式環境打開、以及 php artisan down 維護模式的進階參數。這塊在 Laravel 10 到 13 之間幾乎沒有破壞性變動,是相對輕鬆的一天,剛好可以把 Config 系統的細節一次講透。
Day 5 我們把 blog-app 的骨架建起來、認識了 bootstrap/app.php。今天要處理的是另一個維度:這個專案在不同環境(本機開發、測試、正式站)該怎麼用不同的設定值運作,而不需要改任何一行程式碼。
.env 存放的是「會隨環境改變的設定值」:資料庫連線資訊、API 金鑰、APP_URL 等等。專案根目錄還會有一個 .env.example,這是給團隊成員或部署流程參考的範本,裡面只列出需要哪些變數,不該放真實的機密值。
三個環境分類的慣例值得先建立:
APP_DEBUG 必須是 false。新專案建立時,composer create-project 或 laravel new 會自動執行 php artisan key:generate,在 .env 產生一組 APP_KEY。這個值的重要性遠超過它看起來的樣子——Laravel 用它來做所有加密相關操作的根金鑰,包括:
Crypt::encrypt()/encrypter() 的任何加密操作這代表一旦 APP_KEY 外洩,等於所有這些加密機制形同虛設——攻擊者可以偽造合法的 Cookie、破解你的 Session。同樣地,如果 APP_KEY 遺失或被更換,所有既有的加密資料(包含使用者的 Session、任何存進資料庫的加密欄位)會全部無法解密,這也是為什麼多台伺服器部署時,所有伺服器必須共用同一組 APP_KEY,不能各自產生。
php artisan key:generate # 產生新金鑰並直接寫入 .env
php artisan key:generate --show # 只顯示產生的金鑰,不寫入檔案(適合手動貼進正式環境的環境變數設定)
除了主要的 .env,Laravel 支援針對特定環境載入獨立的環境變數檔案,命名規則是 .env.{環境名稱}:
.env # 預設載入
.env.testing # 執行測試時(php artisan test / pest)優先使用,覆蓋 .env 裡同名的變數
.env.local # 部分部署流程/框架整合會用到,優先權高於 .env
.env.testing 是這系列 Day 27 寫測試時會實際用到的檔案——測試需要一個獨立的資料庫連線設定(通常是 in-memory SQLite 或獨立的測試資料庫),不應該不小心跑到開發用的資料庫把資料清空,這個檔案就是解決這個問題的機制。
.env 檔案理所當然不該被提交進版本控制(Day 29 會講到 .gitignore 的細節),但如果你需要把它安全地分享給團隊成員,或存放進 CI/CD 流程,Laravel 提供內建的加密機制:
# 加密目前的 .env,產生 .env.encrypted
php artisan env:encrypt
# 用密鑰解密回 .env
php artisan env:decrypt --key=你的密鑰
env:encrypt 會用 AES-256-CBC 加密整個檔案內容,並把加密用的密鑰印在終端機上(記得妥善保存,遺失就無法解密)。這個機制適合搭配部署流程:把加密後的 .env.encrypted 提交進版本控制是安全的,部署腳本再用存放在 CI/CD 密鑰管理系統裡的密鑰即時解密還原。
這個功能還支援針對不同環境加密成不同檔案:
php artisan env:encrypt --env=staging # 產生 .env.staging.encrypted
php artisan env:decrypt --env=staging --key=xxx
實務上這個機制適合團隊規模不大、還沒導入 Vault/AWS Secrets Manager 這類專門的密鑰管理系統的專案——它比「把 .env 直接丟到 Slack 群組」安全得多,但如果你的團隊已經有更成熟的密鑰管理基礎設施,直接用那套系統管理正式環境的環境變數,會比維護加密過的 .env 檔案更合乎規範。
.env 裡的值可以直接用全域函式 env('KEY', '預設值') 讀取,但這裡有一個容易被忽略、卻很重要的慣例:env() 只應該在 config/*.php 設定檔裡呼叫,不要在程式碼的其他地方(Controller、Model 等)直接呼叫。原因跟 Config 快取有關,下一節會解釋。正確的用法是:
// config/services.php
'line_bot' => [
'channel_access_token' => env('LINE_BOT_CHANNEL_ACCESS_TOKEN'),
],
// 在程式碼其他地方讀取,用 config() 而不是 env()
$token = config('services.line_bot.channel_access_token');
config/services.php 是 Laravel 內建、專門用來放第三方服務憑證的地方,但如果你的專案有一組屬於自己業務邏輯的設定值(例如 blog-app 想設定「一篇文章最少要幾個字才能發布」),不需要硬塞進既有的檔案,可以直接建立新的設定檔:
// config/blog.php
return [
'min_body_length' => env('BLOG_MIN_BODY_LENGTH', 50),
'featured_posts_limit' => env('BLOG_FEATURED_LIMIT', 5),
];
// 使用方式
$minLength = config('blog.min_body_length');
Laravel 沒有強制規定 config/ 目錄底下只能有框架預先給的那幾個檔案——任何 .php 檔案放進去、回傳一個陣列,就能用 config('檔名.鍵名') 存取。這是把「散落在程式碼各處的魔術數字」集中管理、且能透過環境變數覆蓋的乾淨做法,比在程式碼裡直接寫死 50 這種常數更容易維護跟測試。
config() 函式用點記法讀取巢狀設定:
$timezone = config('app.timezone');
$dbConnection = config('database.default');
// 帶預設值:讀不到指定的鍵時回傳預設值,而不是 null
$limit = config('blog.featured_posts_limit', 5);
// 一次讀取/設定多個值
config(['blog.min_body_length' => 100]); // 動態覆蓋(僅在當前請求生命週期內有效,不會寫回檔案)
正式環境上線前,建議執行:
php artisan config:cache
這個指令會把所有 config/*.php 檔案的內容合併、快取成單一檔案,大幅減少每次請求需要讀取解析的檔案數量。這裡是前面「只在 config 檔案呼叫 env()」這條規則真正重要的原因:一旦執行了 config:cache,Laravel 之後讀取設定值都是讀快取檔,不會再去解析 .env;如果你的程式碼裡直接呼叫了 env(),快取生效後那些呼叫會全部回傳 null,因為 .env 根本沒被載入。
開發階段不建議執行 config:cache(改 .env 不會立即生效,容易搞混),這個指令是給部署流程用的。
php artisan config:clear # 清除快取,讓 config/*.php 跟 .env 重新即時生效
php artisan config:show database # Laravel 10.36+/11 起可用:印出某個設定檔目前實際生效的完整內容(含來自 .env 的覆蓋值),排查設定問題很好用
程式碼裡想判斷目前跑在哪個環境,用 App::environment():
if (App::environment('production')) {
// 只在正式環境執行的邏輯
}
if (App::environment(['local', 'testing'])) {
// 本地或測試環境都執行
}
App::environment(); // 不帶參數,直接回傳目前環境字串,例如 'local'
環境名稱本身也是由 .env 裡的 APP_ENV 變數決定的,local/testing/production 只是慣例,不是框架強制的固定值——如果你的部署流程有 staging(預備環境)這種中間階段,直接在 .env 設 APP_ENV=staging,App::environment('staging') 一樣能正確判斷。
APP_DEBUG 這個變數控制的是「發生例外時,要不要把完整的錯誤堆疊、程式碼片段、環境變數直接顯示給使用者看」。開發時開著很方便,除錯效率高;正式環境一旦忘記關掉,等於把資料庫密碼、API 金鑰、內部程式碼結構全部攤在使用者面前,這是最常見也最不該發生的資安疏失之一。
# .env(正式環境)
APP_DEBUG=false
正式環境關閉 Debug 之後,使用者看到的會是一個通用的錯誤頁面(例如「500 | Server Error」),不會有任何內部細節。這時候唯一能知道「到底發生了什麼」的地方,就是 Day 15 會講到的 Log 系統——這也是為什麼日誌機制在正式環境特別重要:Debug 模式關閉之後,日誌檔案是你唯一的線索來源。
需要上版、跑資料庫遷移、或做重大調整時,php artisan down 可以讓應用程式暫時顯示維護頁面,而不是讓使用者看到一堆錯誤:
php artisan down
幾個實用參數:
# 允許特定 IP 或帶特定密鑰的請求繞過維護模式(方便自己先測試)
php artisan down --secret="my-secret-token"
# 之後用 https://blog-app.test/my-secret-token 存取即可繞過
# 加上 Retry-After 標頭,提示瀏覽器/爬蟲多久後可以重試
php artisan down --retry=60
# 把所有請求導向指定路徑,而不是顯示維護頁
php artisan down --redirect=/
# 恢復服務
php artisan up
維護結束後別忘了執行 php artisan up——這是很容易忘記的一步,尤其是自動化部署腳本如果中途失敗,可能會讓網站卡在維護模式。
--secret 產生的繞過網址每次都要手動貼上不太方便,你可以改用自訂渲染畫面 + Cookie 記住繞過權限的方式,讓自己或內部測試人員瀏覽時不用一直帶著那串網址:
php artisan down --secret="my-secret-token" --render="errors::503"
--render 參數讓維護頁面直接使用你專案裡已經編譯好的某個 Blade view(例如自訂的 503 錯誤頁),而不是 Laravel 內建的預設樣式——這在正式環境特別實用,因為維護模式啟動時,應用程式其實已經「暫停」了,沒辦法即時編譯 Blade 模板,--render 指定的是預先編譯好的 view 名稱。
多台伺服器部署的情境下,php artisan down 預設只會在執行指令的那一台伺服器生效,其他伺服器完全不知情,使用者可能因為負載平衡被導到還沒進入維護模式的伺服器。如果 blog-app 之後需要多台伺服器,config/app.php 的 maintenance 設定可以改用 cache 驅動:
// config/app.php
'maintenance' => [
'driver' => 'cache',
'store' => 'database',
],
這樣維護狀態會存進共用的快取(例如 Redis 或資料庫),所有伺服器都能讀到同一份維護狀態,php artisan down 只需要在其中一台伺服器執行一次即可讓全部伺服器同步進入維護模式。
.env 但網站行為沒變:先檢查是不是執行過 config:cache,快取存在時改 .env 不會生效,要先 php artisan config:clear 或重新執行 config:cache。key:generate 指令名稱打錯:注意是 php artisan key:generate(產生 APP_KEY),不是 key:gen 或其他縮寫,這個指令沒有簡寫別名。.env 裡的值有空白或特殊符號沒加引號:例如密碼裡有空格或 # 符號,會被誤判成註解或值被截斷,需要用雙引號包起來,例如 DB_PASSWORD="my pass#word"。APP_KEY 沒有跟舊伺服器同步,導致既有使用者全部被登出:APP_KEY 改變會讓所有既有的加密 Session/Cookie 失效,多台伺服器如果各自產生了不同的 APP_KEY,會出現「使用者這次請求被導到 A 伺服器登入成功,下次請求被導到 B 伺服器卻要求重新登入」這種詭異現象,務必確認所有伺服器共用同一組 APP_KEY。php artisan up,網站一直卡在維護模式:自動化部署腳本如果中途失敗(例如 migration 出錯),流程可能在 down 之後就中斷,沒有執行到 up,部署腳本設計時建議把 up 放進 finally/一定會執行到的區塊,或設定 --retry 讓外部監控至少知道現在是預期中的維護狀態。cache 驅動的維護模式,卻忘記快取存放的位置在部署時被清空:如果 store 指定的快取驅動(例如 file)跟部署流程會清空的目錄重疊,可能導致維護狀態意外提早解除,選擇 database/redis 這類獨立於部署檔案系統的儲存位置較穩妥。.env/Config 這塊在 Laravel 10 到 13 之間幾乎原地不動,今天除了建立正確的使用習慣(env() 只在 config 檔案呼叫、正式環境務必關閉 Debug、上版時記得用維護模式包起來且別忘記 up 回來),也深入了 APP_KEY 的重要性、環境專屬 .env 檔案、自訂 Config 檔案,以及多伺服器部署時維護模式需要特別注意的細節。
魔鬼藏在細節裡 — 西方諺語
Day 7 進入路由的世界,順便處理 RateLimiter 的定義位置——舊寫法放在 RouteServiceProvider,而那個類別已經不存在了。