深入淺出:掌握環境變數的設定與建置

在現代軟體開發中,管理應用程式的設定是一個不可或缺的環節。從資料庫連線字串、API 金鑰到各種服務的憑證,這些敏感或因環境而異的資訊,都不希望直接寫死在程式碼中。這就是「環境變數」發揮關鍵作用的地方。

本文將帶您深入了解環境變數的核心概念、為什麼它如此重要,以及如何在各種環境中有效地設定與使用它。

什麼是環境變數?

環境變數(Environment Variables)是在作業系統層級定義的動態具名值。它們存在於應用程式的執行環境中,可以被應用程式讀取和使用。簡單來說,它們是將設定與程式碼分離的一種強大機制。

為什麼要使用環境變數?

使用環境變數主要有以下幾個優點:

  1. 安全性 (Security):將 API 金鑰、密碼等敏感資訊儲存在環境變數中,可以避免將它們提交到版本控制系統(如 Git),從而降低洩漏風險。
  2. 可攜性 (Portability):應用程式可以在不同的環境(開發、測試、生產)中執行,而無需修改任何程式碼。只需為每個環境設定對應的變數值即可。
  3. 靈活性 (Flexibility):當設定需要變更時(例如更換資料庫),只需要更新環境變數的值並重新啟動應用程式,而不需要重新部署整個程式。
  4. 遵循十二因子應用程式 (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 指令來設定。

  • 圖形化介面

    1. 在「開始」功能表中搜尋「編輯系統環境變數」。
    2. 點擊「環境變數…」按鈕。
    3. 在「系統變數」或「使用者變數」區塊中新增或編輯。
  • 命令提示字元 (Command Prompt)
    使用 setx 指令可以永久設定一個環境變數。

    setx DATABASE_URL "postgresql://user:password@host:port/dbname"
    

    請注意,setx 設定的變數只會在新的命令提示字元視窗中生效。

在專案中使用 .env 檔案

在開發環境中,每次都手動 exportsetx 變數可能有點繁瑣。一個更常見且方便的做法是使用 .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 開發工具,例如 poetryuv,已經內建了自動偵測並載入 .env 檔案的功能。當使用 poetry runuv 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,優先級順序如下(數字越小,優先級越高):

  1. Shell 環境變數:在執行 docker-compose up 的終端機中設定的變數,或是在 CI stepenv 區塊中設定的變數。這是最高優先級
  2. docker-compose.yml 中的 environment 區塊:這裡定義的變數。
  3. docker-compose.yml 中的 env_file 區塊env_file 指定的檔案中的變數。
  4. .env 檔案:位於專案根目錄下的 .env 檔案(會被自動載入)。
  5. 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.devdocker-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
    # 此處留空,讓容器直接繼承執行環境的變數

這是最關鍵的一點:當服務中沒有 environmentenv_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 與雲端原生開發的必要技能。

無論是個人開發者還是團隊的一員,都應該養成從專案一開始就使用環境變數來管理設定的好習慣。從本地開發利用 .envos.getenv,到深刻理解 Docker Compose 變數載入的優先級,為專案選擇最合適的 CI/CD 整合策略,再到最終在容器中進行標準化部署,掌握這些技巧將使開發流程更加順暢、安全和專業。