FastAPI Pydantic Settings Complete Guide to Multi-Environment Configuration

📂 Phase: Phase 5 - Engineering and Deployment (Practical) 🔗 Related Chapters: FastAPIdependency-injection · FastAPI安全认证

Table of contents

Environment Configuration Management Overview

Have you ever encountered such a situation: everything was normal when debugging with SQLite during local development, but after pushing to the server, because the database URL was not modified in time, the production environment was also connected to the development library, and the test data was even accidentally written into the official database? This is a classic disaster caused by "hard-coded configuration".

Dangers of hard-coded configuration:

  • It is easy to forget to modify when switching environments
  • Sensitive information such as keys and passwords may be leaked to the warehouse
  • Code is not reusable in different environments

Following the configuration principle of 12-Factor App, we should completely separate configuration from code and dynamically inject it through environment variables or configuration files. The benefits of doing this are very obvious:

  • Environment Isolation: Development, testing, and production environments do not interfere with each other
  • Security Protection: Sensitive information such as passwords and keys will not be hard-coded in the code
  • Flexible deployment: Environment switching can be completed without modifying a line of code

💡 Actual combat comparison The following example shows the problem of hard-coded configuration and how to avoid it with environment-driven configuration.

# ❌ 错误示范:硬编码配置
DATABASE_URL = "sqlite:///dev.db"   # 上线时忘记修改,生产环境用了 SQLite
SECRET_KEY = "dev-secret-123"      # 密钥暴露在代码中,安全风险极高
DEBUG = True                       # 生产环境开启调试模式,敏感信息可能泄露
# ✅ 正确做法:环境驱动的配置
# 不同环境使用不同的 .env 文件,代码完全统一
# .env.development  → 本地开发
# .env.testing      → 自动化测试
# .env.production   → 生产环境

By reading different environment files, the application no longer needs to care about which environment it is in, and the code can truly remain unchanged and the configuration can be changed.

Pydantic Settings Basics

Pydantic Settings is a configuration artifact in the FastAPI ecosystem. It is based on Pydantic's data verification capabilities, which can help you read environment variables or configuration files in a type-safe manner, and supports automatic verification.

Install

pip install pydantic-settings fastapi

Define basic configuration classes

Let's first create aBaseConfig, put the configuration items common to all environments here. passmodel_configSpecify the default environment file name and prohibit undefined extra fields to avoid configuration confusion.

# config/base.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field, field_validator
from typing import List

class BaseConfig(BaseSettings):
    # 配置文件加载规则
    model_config = SettingsConfigDict(
        env_file=".env",                 # 默认读取 .env 文件
        env_file_encoding="utf-8",
        extra="forbid"                   # 不允许出现未定义的配置项
    )

    # 应用基本信息
    app_name: str = Field(default="FastAPI App", min_length=1)
    app_version: str = "1.0.0"
    environment: str = Field(
        default="development",
        pattern=r"^(development|testing|production)$"
    )

    # 调试与服务器设置
    debug: bool = False
    host: str = "127.0.0.1"
    port: int = Field(default=8000, ge=1, le=65535)

    # 跨域资源共享(CORS)白名单
    cors_allow_origins: List[str] = Field(default_factory=list)

    # 便利的属性,用于判断当前环境
    @property
    def is_production(self) -> bool:
        return self.environment == "production"

    @property
    def is_development(self) -> bool:
        return self.environment == "development"

    # 解析逗号分隔的 CORS 来源字符串(环境变量常以字符串形式传入)
    @field_validator("cors_allow_origins", mode="before")
    @classmethod
    def parse_cors_origins(cls, v):
        if isinstance(v, str):
            return [origin.strip() for origin in v.split(",") if origin.strip()]
        return v

Environment-specific configuration classes

through inheritanceBaseConfig, we create separate configuration classes for the development environment and production environment respectively. Production environments need to turn off debugging options, use a more robust database, and even add additional validation rules.

# config/environments.py
from .base import BaseConfig
from pydantic import Field

class DevelopmentConfig(BaseConfig):
    debug: bool = True
    database_url: str = "sqlite+aiosqlite:///dev.db"
    cors_allow_origins: List[str] = ["http://localhost:3000"]

class ProductionConfig(BaseConfig):
    debug: bool = False
    database_url: str = Field(description="生产数据库URL", min_length=10)
    cors_allow_origins: List[str] = ["https://yourapp.com"]

    # 强制检查:生产环境绝不能使用 SQLite
    @field_validator("database_url")
    @classmethod
    def validate_prod_db(cls, v):
        if "sqlite" in v.lower():
            raise ValueError("生产环境不能使用SQLite")
        return v

Configuration factory: automatically loaded according to environment

We write a factory function based onENVIRONMENTThe value of the environment variable loads the corresponding configuration class. In order to improve performance, also uselru_cacheCache configuration instances to avoid repeated initialization.

# config/__init__.py
import os
from functools import lru_cache
from .environments import DevelopmentConfig, ProductionConfig, BaseConfig

def get_config() -> BaseConfig:
    """根据 ENVIRONMENT 环境变量动态选择配置类"""
    env = os.getenv("ENVIRONMENT", "development").lower()
    config_map = {
        "development": DevelopmentConfig,
        "production": ProductionConfig
    }
    return config_map.get(env, DevelopmentConfig)()

@lru_cache()
def get_cached_config() -> BaseConfig:
    """带缓存的配置获取函数,避免重复创建实例"""
    return get_config()

In a FastAPI application, you can easily use dependency injection to obtain configuration:

from fastapi import FastAPI, Depends
from config import get_cached_config, BaseConfig

app = FastAPI()

@app.get("/info")
def info(config: BaseConfig = Depends(get_cached_config)):
    return {
        "app_name": config.app_name,
        "environment": config.environment,
        "debug": config.debug
    }

Multi-environment configuration strategy

Pydantic Settings follows an explicit priority when reading configuration. This feature allows us to override settings in a more flexible way.

Environment variable priority (from high to low)

  1. Directly pass parameters to the configuration class (such asConfig(key="value")
  2. System environment variables
  3. .envdocument
  4. Field default value

This means: even if.envset in the fileDEBUG=true, if you pass in the startup commandexport DEBUG=falseOverride, the program will use the value of the system environment variable first.

Use different.envdocument

Have a dedicated one for each environment.envDocumentation is a good practice. Example of file content:

# .env.development
ENVIRONMENT=development
DEBUG=true
DATABASE_URL=sqlite+aiosqlite:///dev.db
CORS_ALLOW_ORIGINS=http://localhost:3000
# .env.production
ENVIRONMENT=production
DEBUG=false
DATABASE_URL=postgresql+asyncpg://user:pass@prod-db:5432/app
CORS_ALLOW_ORIGINS=https://yourapp.com

SECURITY WARNING: ALL.envAll files should be added.gitignore, to prevent sensitive information from being submitted to version control. But you can create aenv.exampleTemplate for team members to refer to.

# .gitignore
.env*
!env.example

Docker multi-environment deployment

In Docker or Docker Compose, you can doenv_fileSpecify an environment file or directly inject environment variables. Here is a typical development/production configuration:

# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    environment:
      - ENVIRONMENT=${ENVIRONMENT:-development}
    env_file:
      - .env.${ENVIRONMENT:-development}
    ports:
      - "8000:8000"
    depends_on:
      - db

  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=app
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass

You can switch by specifying environment variables at startup:

# 开发环境
docker-compose up

# 生产环境
export ENVIRONMENT=production
docker-compose up

Configuration validation and type safety

One of Pydantic’s trump cards is its powerful type validation. Not only can we limit field types, we can also use enumerations and literal types to make the meaning of the configuration clearer and avoid spelling errors.

# config/validated.py
from .base import BaseConfig
from enum import Enum
from typing import Literal

class LogLevel(str, Enum):
    DEBUG = "DEBUG"
    INFO = "INFO"
    WARNING = "WARNING"
    ERROR = "ERROR"

class ValidatedConfig(BaseConfig):
    log_level: LogLevel = LogLevel.INFO
    cache_strategy: Literal["memory", "redis"] = "memory"

    @field_validator("cache_strategy")
    @classmethod
    def check_redis_url(cls, v, info):
        if v == "redis":
            # 如果选择 redis,必须提供 REDIS_URL
            redis_url = info.data.get("redis_url")
            if not redis_url:
                raise ValueError("使用 redis 缓存模式时必须设置 redis_url")
        return v

In this way, incorrect configuration values ​​will throw an exception immediately when the application starts, truly achieving "early detection and early repair".

Sensitive information security management

Sensitive information (database passwords, JWT keys, third-party API keys) must be kept secure. Pydantic Settings fully supports reading from external Secret stores.

Best Practices for Key Management

  1. Local Development: Use.envfile and exclude the file from version control
  2. Production environment: Use Docker Secrets, Kubernetes Secrets or cloud platform key management services (such as AWS Secrets Manager)
  3. Never Hardcode: Clear text keys should not appear anywhere in the code

Secure key reading function

You can write a helper function that first reads the secret from the directory where Docker Secrets is mounted, and then obtains it from environment variables.

# config/security.py
import os
from typing import Optional

def get_secret(key: str, default: Optional[str] = None) -> Optional[str]:
    """
    优先从 /run/secrets/<key> 读取(Docker Secrets 标准路径),
    如果不存在则回退到环境变量。
    """
    secret_path = f"/run/secrets/{key}"
    if os.path.exists(secret_path):
        with open(secret_path, "r") as f:
            return f.read().strip()
    return os.getenv(key, default)

In a configuration class you can use it like this:

from config.security import get_secret

class ProductionConfig(BaseConfig):
    secret_key: str = Field(default_factory=lambda: get_secret("APP_SECRET_KEY", "change_me"))

Production deployment best practices

Security checklist before going live

Be sure to confirm each item before deployment:

  • DEBUG=False, never enable debugging mode in the production environment
  • Use a strong key of at least 64 characters (available viaopenssl rand -hex 32generate)
  • CORS whitelist is limited to specific production domain names, no wildcards are used
  • Database uses PostgreSQL/MySQL and enables SSL connection
  • log level set toWARNINGor higher to avoid outputting sensitive information
  • All sensitive information is injected through Secrets, not hard-coded in images or environment files

Automated deployment script example

The following script will first check the necessary environment variables, then build and start the service based on Docker Compose, and perform a simple health check.

#!/bin/bash
set -e

# 列出所有运行时必须提供的环境变量
REQUIRED_VARS=("PROD_DATABASE_URL" "PROD_JWT_SECRET")
for var in "${REQUIRED_VARS[@]}"; do
  if [ -z "${!var}" ]; then
    echo "❌ 缺失必需环境变量: $var"
    exit 1
  fi
done

# 构建镜像
docker build -t my-fastapi-app .

# 使用指定的生产配置文件启动
docker-compose -f docker-compose.prod.yml up -d

# 简单健康检查
sleep 10
if curl -f http://localhost/health >/dev/null 2>&1; then
  echo "✅ 部署成功"
else
  echo "❌ 部署失败,请检查日志"
  exit 1
fi

Summarize

FastAPI and Pydantic Settings provide a rigorous and easy-to-use configuration management solution. The core advantages include:

  • Environmental Isolation: Passed.envSwitch files or environment variables to keep your code pure
  • Type Safety: Complete type hints and verification mechanisms to eliminate runtime configuration errors
  • Worry-free security: Sensitive information can be completely separated from the code, matching cloud native security practices
  • Cloud-native friendly: Perfectly adapted to containerized platforms such as Docker and Kubernetes

💡 Key Points: Establishing a good configuration management system at the beginning of the project can pave the way for subsequent continuous integration, multi-environment deployment, and security compliance. Every minute you spend on configuration management will save you a lot of maintenance time later.


🔗 Extended reading