全栈容器化应用的环境变量管理

全栈容器化应用的环境变量管理

理解一个复杂程序的运行逻辑,一个方式是找到它的 Main 函数,观察各个对象的生命周期。这能让我们把握程序的运行途径,而不是迷失在散落各处的类文件中。理解一个系统的配置管理,我们也可以采用类似的视角。当我们构建一个前后端分离项目,并希望通过 Docker 进行容器化部署时,如何管理配置?以 FastAPI + Vue/TypeScript + PostgreSQL 为例,整理一下环境变量管理方案。

核心原则:配置与代码分离

无论是在前端、后端还是数据库层,都要遵循一个核心原则:代码中定义配置的结构,而由环境变量注入具体的值。配置管理的物理载体通常是项目根目录下的 .env 文件。

大多数现代工具库和 Docker 都遵循一套标准的优先级逻辑:

  1. 系统级环境变量(最高优先级):通常由 CI/CD 流水线或服务器配置直接注入。
  2. .env 文件(默认配置):本地开发时的配置来源。

这意味着在本地开发时,我们可以依赖 .env 文件快速启动;而在部署生产环境时,直接在服务器或容器编排工具中设置同名变量即可覆盖默认配置,无需修改任何代码。

后端管理:Pydantic 的类型安全

对于 Python 后端(尤其是 FastAPI),目前的行业标准方案是使用 pydantic-settings。不再手动解析 os.environ,而是创建一个 config.py 文件,定义一个继承自 BaseSettings 的类。这个类充当了配置的“单一事实来源”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# config.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
# 定义配置项及其类型,支持设置默认值
POSTGRES_USER: str = "postgres"
POSTGRES_PASSWORD: str
POSTGRES_DB: str = "app_db"
POSTGRES_HOST: str = "localhost"

# 自动读取 .env 文件
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")

# 实例化对象,供全局调用
settings = Settings()

在项目的其他地方,无论是在数据库连接模块还是路由逻辑中,我们只需导入这个 settings 对象即可使用配置。它自动处理了环境变量的映射,还提供了类型检查和验证功能。

前端管理:构建时与运行时

前端的配置管理比后端更为复杂,因为我们需要区分两个环境:

  1. 构建环境(Build Time):代码被 Vite/Webpack 编译打包的过程,运行在 Node.js 环境中。
  2. 运行环境(Runtime):用户打开网页后的环境,运行在浏览器的沙盒中。

构建工具的角色(Vite)

在构建阶段,vite.config.ts 负责定义项目如何打包(例如端口设置、路径别名)。由于它运行在 Node.js 中,它可以直接读取 process.env 或使用 Vite 的 loadEnv 方法来获取环境变量,从而改变构建行为。

浏览器中的环境变量

然而,浏览器完全不知道 process 是什么,我们也无法在 Vue 组件中直接访问服务器的环境变量。

为了解决这个问题,Vite 利用了现代浏览器的 import.meta 特性。它会自动读取 .env 文件,并将特定的变量注入到 import.meta.env 对象中,供前端代码在浏览器中使用。为了防止后端密钥(如 AWS Secret)意外泄露到前端代码中,Vite 实施了严格的过滤:只有以 VITE_ 开头的变量(例如 VITE_API_URL)才会被暴露给前端代码。

在 Vue 组件中,我们无需引入额外的配置类,直接使用即可:

1
2
// api.ts
const apiUrl = import.meta.env.VITE_API_URL;

容器编排:Docker Compose 的胶水作用

docker-compose.yml 是连接宿主机环境变量与容器内部环境的桥梁。

docker-compose.yml 中,我们可以使用 ${VARIABLE:-default} 的语法。它的意思是尝试读取宿主机的环境变量 VARIABLE,如果未设置,则使用 default 作为默认值。

当然,这个变量目前只提取到了 docker-compose.yml 中,需要通过 environment 字段显式注入到服务容器中,这样后端的 Python 代码或数据库进程才能读取到它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
version: '3.8'

services:
backend:
build: ./backend
environment:
# 映射宿主机变量到容器内部
- DATABASE_URL=postgresql://${POSTGRES_USER:-dalu}:${POSTGRES_PASSWORD:-dalu_password}@db:5432/${POSTGRES_DB:-dalu_farm}
# 或者分别映射,供 Pydantic 读取
- POSTGRES_USER=${POSTGRES_USER:-dalu}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-dalu_password}
- POSTGRES_HOST=db

db:
image: postgres:15
environment:
- POSTGRES_USER=${POSTGRES_USER:-dalu}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-dalu_password}
- POSTGRES_DB=${POSTGRES_DB:-dalu_farm}
volumes:
- postgres_data:/var/lib/postgresql/data

volumes:
postgres_data: