快轉到主要內容
  1. 教學文章/

Python dotenv 實戰:安全管理 .env、環境變數與部署設定

·7 分鐘· loading · loading · ·
Python Dotenv Environment Variables Configuration Security
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 10: 本文

一. 前言:秘密不要寫死,設定也不要散落各地
#

拍拍君先講結論:如果你的 Python 專案有 API key、資料庫網址、外部服務 token、部署環境差異,請不要把它們直接寫進程式碼。 你可能看過這種寫法:

OPENAI_API_KEY = "sk-..."
DATABASE_URL = "postgresql://user:password@localhost:5432/app"
DEBUG = True

這樣跑起來很快,但也很快就會出事。 第一個問題是安全:程式碼通常會進 Git,一旦 secret 被 commit,就算你之後刪掉,歷史紀錄裡還是可能找得到。 第二個問題是環境差異:本機、測試機、正式機的設定通常不一樣,寫死在程式裡會讓部署變成手動改 code。 第三個問題是協作:同事 clone 專案後,不知道要設定哪些環境變數,也不知道哪些值是必要的。 python-dotenv 解決的是其中一個很實用的痛點:

讓本機開發可以從 .env 檔讀取環境變數,同時讓程式在部署環境仍然使用真正的 environment variables。

也就是說,.env 是本機開發的方便入口,不是正式部署的秘密保險箱。 這篇會帶你把 python-dotenv 用得穩一點:從安裝、載入、override 規則、.env.example、簡單驗證,到部署時要注意的安全細節。 如果你想要更進階的「型別安全設定物件」,可以接著看 pydantic-settings 實戰。 這篇先專心處理最基礎、最常用、最容易被搞混的 .env 工作流。

二. 安裝:只需要一個小套件
#

python-dotenv 不是標準庫,需要另外安裝。 用 pip

pip install python-dotenv

uv

uv add python-dotenv

專案裡最常見的配置是 .env.env.example.gitignore 和你的 Python 程式放在同一個專案根目錄。其中:

  • .env 放你本機真正使用的值
  • .env.example 放範例 key,不放真正 secret
  • .gitignore 必須排除 .env
  • app.pyload_dotenv() 載入

最基本的 .gitignore

.env
.env.*
!.env.example

這段的意思是:忽略 .env.env.local 之類的本機檔案,但保留 .env.example。 拍拍君建議一開始就這樣設,不要等 token 進 Git 之後才補救。

三. 第一個 .env:把值放到專案外層設定
#

先建立 .env

APP_NAME=Daily Pypy Lab
DEBUG=true
API_BASE_URL=https://api.example.com
API_KEY=replace-me-with-real-secret

然後在 Python 裡載入:

from dotenv import load_dotenv
import os

load_dotenv()

app_name = os.getenv("APP_NAME")
debug = os.getenv("DEBUG")
api_base_url = os.getenv("API_BASE_URL")
api_key = os.getenv("API_KEY")

print(app_name)
print(debug)
print(api_base_url)
print(api_key)

load_dotenv() 會讀取目前工作目錄附近的 .env 檔,把裡面的 key/value 放進 os.environ。之後你就可以用 os.getenv("API_KEY")os.environ["API_KEY"] 讀取。兩者差異很重要:os.getenv() 找不到時會回傳 None,也可以給預設值:

port = os.getenv("PORT", "8000")

os.environ["API_KEY"] 找不到時會直接丟 KeyError。 拍拍君的習慣是:

  • 必填 secret 用 os.environ["KEY"]
  • 可選設定用 os.getenv("KEY", default)
  • 啟動時集中檢查,不要散落在每個函式裡

例如:

from dotenv import load_dotenv
import os

load_dotenv()

API_KEY = os.environ["API_KEY"]
API_BASE_URL = os.getenv("API_BASE_URL", "https://api.example.com")
DEBUG = os.getenv("DEBUG", "false")

這樣少一個必要設定時,程式會在啟動階段失敗,而不是跑到一半才爆炸。

四. .env 格式:簡單,但有幾個細節
#

.env 基本上是一行一個 key/value:

API_KEY=abc123
PORT=8000
DEBUG=true

可以加註解,也可以替含空白的值加引號:

# local development only
APP_NAME="Daily Pypy Lab"
WELCOME_MESSAGE="hello, dotenv"
PASSWORD="abc#123"

如果值裡有空白或 #,建議加引號,避免被當成註解或被解析錯。 .env 裡的值都是字串。 所以這段:

DEBUG=false
PORT=8000

讀出來會是字串,不是 boolean 或 int:

debug = os.getenv("DEBUG")
port = os.getenv("PORT")

print(type(debug), debug)
print(type(port), port)

如果你需要型別轉換,要自己處理:

debug = os.getenv("DEBUG", "false").lower() == "true"
port = int(os.getenv("PORT", "8000"))

簡單專案這樣可以。 設定很多、規則變複雜時,就適合改用 pydantic-settings 這類工具。

五. override 規則:為什麼我的 .env 沒有生效?
#

python-dotenv 最常見的困惑是:

我明明改了 .env,為什麼程式讀到的值還是舊的?

關鍵在 load_dotenv() 預設不會覆蓋已經存在的環境變數。 例如你的 shell 已經有:

export API_KEY=from-shell

.env 裡有:

API_KEY=from-dotenv

Python 程式:

from dotenv import load_dotenv
import os

load_dotenv()
print(os.getenv("API_KEY"))

輸出會是:

from-shell

這是好事,因為正式部署平台通常會把真正的環境變數注入 process,而 .env 只該是本機開發輔助。 如果你真的需要讓 .env 覆蓋目前 shell 裡的值,可以明確寫:

load_dotenv(override=True)

但拍拍君建議 production entrypoint 不要隨手加 override=True。 比較好的做法是把意圖寫清楚:

load_dotenv(override=False)

就算這是預設值,也能讓讀 code 的人知道:shell / deployment environment 優先,.env 只是補缺。

六. 指定 .env 路徑:不要被工作目錄騙
#

load_dotenv() 會嘗試尋找 .env,小專案通常沒問題。 但一旦你有 CLI、背景 worker、測試、Docker、不同啟動位置,就可能遇到工作目錄不一致。 比較穩的寫法是用 pathlib 指定路徑:

from pathlib import Path
from dotenv import load_dotenv

BASE_DIR = Path(__file__).resolve().parent
ENV_PATH = BASE_DIR / ".env"

load_dotenv(ENV_PATH, override=False)

如果你的程式在 src/ 裡,而 .env 在專案根目錄,可以往上找:

from pathlib import Path
from dotenv import load_dotenv

ROOT_DIR = Path(__file__).resolve().parents[2]
load_dotenv(ROOT_DIR / ".env", override=False)

parents[2] 這種寫法要小心,目錄結構改了就會壞。 所以拍拍君更喜歡把設定載入集中在一個地方,例如 src/my_app/config.py,不要每個模組都自己找 .env

七. 建立 .env.example:讓協作變簡單
#

.env 不該進 Git,但專案需要告訴別人「有哪些設定要填」。 這就是 .env.example 的工作。 例如:

APP_NAME=Daily Pypy Lab
DEBUG=false
API_BASE_URL=https://api.example.com
API_KEY=
DATABASE_URL=

重點是:

  • key 要完整
  • secret 值要留空或放假資料
  • 預設值要保守
  • 註解要說明必要格式

README 裡就可以寫 cp .env.example .env,然後提醒開發者填自己的值。這比在 README 裡列一大串環境變數更不容易漏。

八. 集中設定:不要在程式各處直接讀環境變數
#

很多專案一開始會在不同檔案到處呼叫 os.getenv()。 短期看起來方便,長期會讓設定規則散落各地。 拍拍君建議建立一個簡單的 config.py,集中載入、轉型和必填檢查:

from pathlib import Path
from dotenv import load_dotenv
import os

ROOT_DIR = Path(__file__).resolve().parents[1]
load_dotenv(ROOT_DIR / ".env", override=False)


def require_env(name: str) -> str:
    value = os.getenv(name)
    if value is None or value.strip() == "":
        raise RuntimeError(f"Missing required environment variable: {name}")
    return value


APP_NAME = os.getenv("APP_NAME", "my-app")
DEBUG = os.getenv("DEBUG", "false").lower() == "true"
API_KEY = require_env("API_KEY")
DATABASE_URL = require_env("DATABASE_URL")

其他地方只 import 這些設定。 這樣有幾個好處:

  • 必填設定集中在一個地方
  • 型別轉換集中在一個地方
  • 測試時比較容易替換
  • 不會每個檔案都呼叫 load_dotenv()

python-dotenv 本身很輕量,不會幫你做完整設定系統。 但只要你把讀取規則集中起來,小型專案通常就夠穩。

九. 做一點必要驗證:早點失敗比較好
#

環境變數最怕的是「看似有值,但其實不能用」。 例如:

API_KEY=replace-me
DATABASE_URL=
PORT=not-a-number

程式如果不檢查,可能啟動成功,直到真的呼叫 API 或連 DB 時才失敗。 最少可以做三件事:

  • 必填值空白就立刻失敗
  • boolean 明確接受 true/false1/0
  • port、timeout 這類數字要轉成 int

簡化版可以長這樣:

from dotenv import load_dotenv
import os

load_dotenv(override=False)

API_KEY = os.environ["API_KEY"]
DATABASE_URL = os.environ["DATABASE_URL"]
DEBUG = os.getenv("DEBUG", "false").lower() in {"1", "true", "yes"}
PORT = int(os.getenv("PORT", "8000"))

這不是最華麗的設定系統,但對小專案很實用。 重點是錯誤要清楚。 不要讓使用者只看到 KeyError: 'API_KEY',如果可以,讓錯誤訊息變成 Missing required environment variable: API_KEY

十. CLI、FastAPI、測試裡怎麼放?
#

不同專案型態,load_dotenv() 放的位置略有不同。

  • CLI 工具:放在 main entrypoint 或 config.py
  • FastAPI:放在建立 app 前,或讓 app import config.py
  • background worker:放在 worker 啟動入口
  • pytest:測試中用 monkeypatch 明確設定環境變數

測試不一定要真的讀 .env。很多時候直接用 monkeypatch.setenv() 會更穩,因為測試資料就寫在測試裡,不依賴開發者本機檔案。

十一. 部署:正式環境不要依賴 .env
#

本機開發用 .env 很舒服。 正式部署時,通常應該使用平台提供的 environment variables 或 secrets manager。 例如:

  • GitHub Actions:Repository secrets / environment secrets
  • Docker Compose:env_fileenvironment
  • Kubernetes:Secret / ConfigMap
  • Render、Railway、Fly.io、Vercel:平台後台設定

GitHub Actions 裡不要把 secret 寫進 workflow,改用 ${{ secrets.API_KEY }}

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      API_KEY: ${{ secrets.API_KEY }}
      DATABASE_URL: ${{ secrets.DATABASE_URL }}

Docker Compose 則要分清楚 .envenv_file

services:
  app:
    build: .
    env_file:
      - .env
    environment:
      DEBUG: "false"

Compose 專案根目錄的 .env 常用來替換 compose.yml 裡的變數;env_file 則是把檔案裡的 key/value 傳進 container 環境。 如果你不確定,請明確寫 env_file,並用 docker compose config 檢查最後產生的設定。 部署重點是:.env 可以用於本機,正式 secret 應該由平台注入,程式仍然用 os.environ 讀,不需要知道 secret 來自哪裡。

十二. 安全清單:上線前檢查一次
#

.env 最大的風險不是工具本身,而是使用習慣。 拍拍君的上線前清單:

  • .env 有在 .gitignore
  • .env.example 有 commit
  • .env.example 沒有真 secret
  • README 有寫 cp .env.example .env
  • 必填變數啟動時會檢查
  • 程式不會把 secret 印到 log
  • CI/CD secret 放在平台設定,不寫進 workflow
  • production 不依賴開發機 .env
  • token 不小心 commit 過就直接 rotate

如果你懷疑 secret 已經進過 Git 歷史,不要只刪掉那一行。 請直接去 provider 後台撤銷或重產 token。 Git 歷史清理很麻煩,而且你無法保證遠端、fork、CI log、快取裡沒有留下痕跡。 也不要在錯誤訊息或 debug log 裡印完整 os.environ;secret 一旦進 log,log 平台、告警系統、截圖都可能變成洩漏來源。

結語:dotenv 是入口,不是整套安全系統
#

python-dotenv 很適合解決本機開發的設定問題。 它讓你可以把 secret 和環境差異移出程式碼,用 .env 管理本機值,再用平台環境變數處理正式部署。 但它不是 secret manager,也不是完整設定框架。 拍拍君建議你記住三個原則: 第一,.env 不進 Git,.env.example 才進 Git。 第二,production 環境變數優先,不要隨便 override=True。 第三,啟動時集中檢查設定,早點失敗比晚點爆炸好。 把這些習慣養起來,你的 Python 專案會少很多「本機可以,部署不行」的謎之問題。 設定這種東西平常很安靜,一出事就很吵。 所以我們先把它收好。拍拍。

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 10: 本文

相關文章

Python pydantic-settings 實戰:型別安全管理 .env 與設定檔
·6 分鐘· loading · loading
Python Pydantic Pydantic-Settings Dotenv Configuration Developer-Tools
Python secrets 實戰:安全產生 Token、密碼與一次性連結
·7 分鐘· loading · loading
Python Secrets Security Token Standard-Library Web
Streamlit + DuckDB 實戰:本地資料查詢 Dashboard
·8 分鐘· loading · loading
Python Streamlit DuckDB SQL Dashboard Data-Analysis
Textual + SQLite 實戰:做一個終端機資料管理小工具
·8 分鐘· loading · loading
Python Textual SQLite TUI Database Developer-Tools
Python uv scripts 實戰:PEP 723、inline dependencies 與單檔工具
·6 分鐘· loading · loading
Python Uv PEP 723 Script Developer-Tools Automation
Python Alembic 實戰:資料庫 Migration、版本控管與團隊協作
·9 分鐘· loading · loading
Python Alembic SQLAlchemy Database Migration Developer-Tools