深入淺出:掌握環境變數的設定與建置
在現代軟體開發中,管理應用程式的設定是一個不可或缺的環節。從資料庫連線字串、API 金鑰到各種服務的憑證,這些敏感或因環境而異的資訊,都不希望直接寫死在程式碼中。這就是「環境變數」發揮關鍵作用的地方。
本文將帶您深入了解環境變數的核心概念、為什麼它如此重要,以及如何在各種環境中有效地設定與使用它。
什麼是環境變數?
環境變數(Environment Variables)是在作業系統層級定義的動態具名值。它們存在於應用程式的執行環境中,可以被應用程式讀取和使用。簡單來說,它們是將設定與程式碼分離的一種強大機制。
為什麼要使用環境變數?
使用環境變數主要有以下幾個優點:
- 安全性 (Security):將 API 金鑰、密碼等敏感資訊儲存在環境變數中,可以避免將它們提交到版本控制系統(如 Git),從而降低洩漏風險。
- 可攜性 (Portability):應用程式可以在不同的環境(開發、測試、生產)中執行,而無需修改任何程式碼。只需為每個環境設定對應的變數值即可。
- 靈活性 (Flexibility):當設定需要變更時(例如更換資料庫),只需要更新環境變數的值並重新啟動應用程式,而不需要重新部署整個程式。
- 遵循十二因子應用程式 (The Twelve-Factor App):這是建構現代雲端原生應用程式的一套方法論,其中第三條原則明確指出「在環境中儲存設定」。
如何設定環境變數?
設定環境變數的方式因作業系統而異。
在 macOS 與 Linux
在類 Unix 系統中,可以使用 export
指令來設定一個在當前終端機 session 中有效的環境變數。
export DATABASE_URL="postgresql://user:password@host:port/dbname"
若要讓變數永久生效,可以將此行加入到 shell 的設定檔中,例如 ~/.bash_profile
、~/.zshrc
或 ~/.profile
。
在 Windows
在 Windows 中,可以使用圖形化介面或 setx
指令來設定。
-
圖形化介面:
- 在「開始」功能表中搜尋「編輯系統環境變數」。
- 點擊「環境變數…」按鈕。
- 在「系統變數」或「使用者變數」區塊中新增或編輯。
-
命令提示字元 (Command Prompt):
使用setx
指令可以永久設定一個環境變數。setx DATABASE_URL "postgresql://user:password@host:port/dbname"
請注意,
setx
設定的變數只會在新的命令提示字元視窗中生效。
在專案中使用 .env
檔案
在開發環境中,每次都手動 export
或 setx
變數可能有點繁瑣。一個更常見且方便的做法是使用 .env
檔案。
.env
是一個純文字檔案,可以在其中以 KEY=VALUE
的格式定義環境變數。
# .env file
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
API_KEY=your_secret_api_key
DEBUG=True
大多數現代程式語言和框架都有對應的函式庫可以自動讀取 .env
檔案並將其載入到執行環境中。
- Python:
python-dotenv
- Node.js:
dotenv
- Ruby:
dotenv-rails
- Go:
godotenv
重要提示:永遠不要將 .env
檔案提交到版本控制中!請務必將它加入到 .gitignore
檔案中。通常,您會提交一個名為 .env.example
或 .env.template
的範本檔案,其中包含所有必要的環境變數名稱,但值是空的或範例值,以供其他開發者參考。
# .gitignore
.env
多環境設定 (.env.dev
, .env.prod
)
當需要管理多個環境時,可以建立不同的 .env
檔案,例如 .env.dev
和 .env.prod
。
工具如何載入非標準 .env
檔案?
python-dotenv
:from dotenv import load_dotenv # 明確指定要載入的 .env 檔案路徑 load_dotenv(dotenv_path=".env.dev")
uv
/poetry
:# 使用 --env-file 旗標 uv run --env-file .env.dev -- python my_script.py
docker-compose
:# 在 docker-compose.yml 中明確指定 services: web: env_file: - .env.dev
docker run
:docker run --env-file .env.dev myimage
現代化 Python 工具的自動載入機制
值得注意的是,許多現代化的 Python 開發工具,例如 poetry
和 uv
,已經內建了自動偵測並載入 .env
檔案的功能。當使用 poetry run
或 uv run
來執行腳本或應用程式時,它們會自動將專案根目錄下的 .env
檔案中的變數載入到執行環境中。
這意味著在本地開發時,通常不需要在 Python 程式碼中額外引用 python-dotenv
這類的函式庫來手動載入 .env
檔案,讓開發流程更加簡潔。
如何在程式碼中讀取環境變數?
一旦環境變數被設定(無論是透過 export
、.env
檔案或容器設定),就可以在程式碼中存取它們。
以 Python 為例:os.getenv()
在 Python 中,標準函式庫 os
提供了讀取環境變數最直接的方法。
import os
# 讀取環境變數 'DATABASE_URL'
database_url = os.getenv("DATABASE_URL")
# 建議提供一個預設值,以防變數未被設定
# os.getenv('KEY', 'default_value')
api_key = os.getenv("API_KEY", "default_api_key_for_development")
if database_url:
print("成功讀取到資料庫連線資訊。")
else:
print("警告:未設定 DATABASE_URL 環境變數。")
os.getenv(KEY, default=None)
是最常用的函式。它的優點是當環境變數不存在時,它會回傳 None
(或指定的預設值),而不會導致程式出錯。這使得處理可選的設定變得非常方便。
Docker 與 Docker Compose 中的環境變數
Docker & Docker Compose 中的變數管理
核心:變數的優先級
理解變數如何被載入和覆蓋是關鍵。對於 docker-compose
,優先級順序如下(數字越小,優先級越高):
- Shell 環境變數:在執行
docker-compose up
的終端機中設定的變數,或是在 CIstep
的env
區塊中設定的變數。這是最高優先級。 docker-compose.yml
中的environment
區塊:這裡定義的變數。docker-compose.yml
中的env_file
區塊:env_file
指定的檔案中的變數。.env
檔案:位於專案根目錄下的.env
檔案(會被自動載入)。- Dockerfile 中的
ENV
:映像檔內建的環境變數(最低優先級)。
docker run
vs docker-compose
docker run
不會自動載入.env
檔案。它是一個較低階的指令,必須明確地使用-e
(單個變數) 或--env-file
(檔案) 來傳遞變數。# 從 shell 傳遞單個變數 export API_KEY="123" docker run -e API_KEY my_image # 使用 env file docker run --env-file ./.env.prod my_image
docker-compose
會自動載入.env
檔案,這使得它在開發中更為方便。
Dockerfile
中的 ENV
Dockerfile
中的 ENV
指令是在建置映像檔 (build time) 時設定的,它不支持 ${VARIABLE}
這種執行時的變數替換。它定義的是映像檔的「預設」環境變數,優先級最低,會被任何執行期的設定所覆蓋。
# Dockerfile
# 設定一個靜態的預設值
ENV GREETING="Hello"
# 這個值可以在執行時被 docker run 或 docker-compose 的設定覆蓋
關鍵差異:docker-compose
vs uv
的載入行為
這是一個極其重要的細節:
docker-compose
(合併與覆蓋): 當在docker-compose.yml
中使用env_file
指定了.env.dev
,docker-compose
仍然會先載入預設的.env
檔案,然後再載入.env.dev
。這是一個合併 (merge) 的過程,如果兩個檔案有相同變數,.env.dev
的值會覆蓋.env
的值。uv run
(精確指定): 當使用uv run --env-file .env.dev
時,uv
只會載入您指定的.env.dev
這一個檔案,它不會再去自動尋找並載入預設的.env
檔案。
CI/CD 整合策略
如何將 GitHub Actions 中的 secrets
安全地注入到 docker-compose
服務中?
策略一:隱性繼承 (最簡潔)
此策略充分利用了 Docker Compose 的自動化特性,實現了極致的簡潔。
1. docker-compose.yml
設定
docker-compose.yml
中完全不提及環境變數。
# docker-compose.yml
services:
web:
image: my_app
# 此處留空,讓容器直接繼承執行環境的變數
這是最關鍵的一點:當服務中沒有 environment
或 env_file
區塊時,Compose 會將從 Shell 或 .env
檔案中讀取到的所有變數自動傳遞給容器。
2. 本地與 CI 的實現
- 本地開發:開發者在專案根目錄下建立
.env
檔案。執行docker compose up
時,Compose 會自動讀取它並將變數注入容器。 - CI/CD (GitHub Actions):在
step
中使用env
區塊設定變數。
# .github/workflows/ci.yml
...
- name: Build and run containers
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
run: docker-compose up -d --build
由於 Shell 環境變數(由 env
區塊設定)的優先級最高,這些變數會被直接注入容器,完美實現了目標。
- 優點:
- 極致簡潔:
docker-compose.yml
和 CI 腳本都達到了最簡潔的狀態。 - 無縫開發體驗:本地開發者只需維護
.env
檔案,符合直覺。
- 極致簡潔:
- 缺點:
- 隱性依賴:容器的行為完全依賴於外部環境,對於不熟悉 Compose 優先級規則的人來說,可能會感到困惑(「變數是從哪裡來的?」)。
策略二:動態生成 .env
檔案 (最明確)
此策略的核心是,無論在哪個環境,都明確地透過一個 .env
檔案來提供設定。
1. docker-compose.yml
設定
docker-compose.yml
保持乾淨,不需要設定 env_file
,因為 Compose 會自動尋找 .env
。
# docker-compose.yml
services:
web:
image: my_app
# 留空,依賴自動載入的 .env 檔案
2. 本地與 CI 的實現
- 本地開發:開發者手動建立和維護
.env
檔案。 - CI/CD (GitHub Actions):在執行
docker-compose
前,動態生成.env
檔案。
# .github/workflows/ci.yml
...
- name: Create .env file from secrets
run: |
echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env
echo "API_KEY=${{ secrets.API_KEY }}" >> .env
- name: Run containers
run: docker-compose up -d
- 優點:
- 高度明確:
.env
檔案是唯一的事實來源,非常清晰。 - 工作流程一致:本地和 CI 都圍繞
.env
這個核心檔案工作。
- 高度明確:
- 缺點:
- CI 腳本稍繁瑣:需要在 workflow 中維護一個變數列表來生成檔案。
結論與如何選擇?
經過最終的分析,兩種策略都非常優秀,且都利用了 docker-compose
自動載入 .env
的特性。
- 策略一 (隱性繼承) 更為優雅、簡潔。它相信工具的自動化能力,適合追求極致效率和簡潔性的團隊。
- 策略二 (動態生成
.env
) 更為穩健、明確。它將「環境設定」這個關注點顯式地固化在一個檔案中,適合大型、複雜或對可讀性要求極高的專案。
最終的選擇取決於團隊的風格和偏好:是擁抱「隱性的魔法」,還是堅持「明確的契約」?理解這兩種方法的底層邏輯和權衡,是做出最佳決策的關鍵。
結論
有效地管理環境變數是專業軟體開發的基石。它不僅能提升應用程式的安全性與可攜性,更是實踐 CI/CD 與雲端原生開發的必要技能。
無論是個人開發者還是團隊的一員,都應該養成從專案一開始就使用環境變數來管理設定的好習慣。從本地開發利用 .env
和 os.getenv
,到深刻理解 Docker Compose 變數載入的優先級,為專案選擇最合適的 CI/CD 整合策略,再到最終在容器中進行標準化部署,掌握這些技巧將使開發流程更加順暢、安全和專業。