BodySense 环境变量配置方案
BodySense 项目的环境变量分层策略:哪个服务需要哪些变量,本地 vs 生产,Droplet 内置 DB vs DigitalOcean Managed DB。
#type / howto
#status / seed
#tech / ops
#tech / ai
[!info] related notes
- 所属 MOC: BodySense 云原生实践 MOC
- 理论: 12-Factor App
- 基础设施: bodysense-cloud-native-infrastructure
BodySense 环境变量配置方案
一句话定义
不是”所有资源都塞进所有服务的 env”,而是哪个服务需要哪个资源,就给哪个服务配置对应 env。前端只能放公开配置,后端放连接信息,生产用 VPC 地址,本地用 Public 地址。
文件分层
.env.example # 提交 Git,只有变量名,无真实密码
.env # 本地开发,不提交
.env.production # 生产非敏感配置,可提交
.env.production.local # 生产敏感配置,不提交
加载优先级: .env → .env.production → .env.production.local → OS 环境变量
.gitignore 规则:
.env
.env.*
!.env.example
哪个服务需要哪些变量
Go API(apps/api)
API 是业务核心,需要最多变量:
# ── 应用 ──────────────────────────────────
API_PORT=8080
API_HOST=0.0.x.x
APP_ENV=production # production | development
# ── PostgreSQL ────────────────────────────
DB_HOST=<见下方环境对比>
DB_PORT=5432
DB_NAME=bodysense
DB_USER=doadmin
DB_PASSWORD=<密码>
DB_SSLMODE=require # Managed DB 必须 require
# ── Valkey/Redis ──────────────────────────
REDIS_HOST=<见下方环境对比>
REDIS_PORT=25061
REDIS_PASSWORD=<密码>
REDIS_TLS=true # Managed Redis 需要 TLS
# ── JWT ───────────────────────────────────
JWT_SECRET_KEY=<openssl rand -base64 32>
JWT_ACCESS_TTL_HOURS=168
JWT_REFRESH_TTL_HOURS=720
# ── CORS ──────────────────────────────────
CORS_ORIGINS=https://body.bakersean.top
# ── Object Storage (Spaces) ──────────────
SPACES_ENDPOINT=https://sgp1.digitaloceanspaces.com
SPACES_REGION=sgp1
SPACES_BUCKET=bodysense-storage
SPACES_ACCESS_KEY_ID=<Spaces Key>
SPACES_SECRET_ACCESS_KEY=<Spaces Secret>
# ── AI Service 代理 ───────────────────────
AI_SERVICE_URL=http://ai-service:8100
Python AI Service(apps/ai-service)
AI 服务负责推理、RAG、OCR,需要 LLM 和向量库:
# ── 应用 ──────────────────────────────────
AI_SERVICE_PORT=8100
# ── PostgreSQL (LangGraph checkpoint + pgvector) ──
DATABASE_URL=postgresql://doadmin:<密码>@<host>:25060/defaultdb?sslmode=require
# ── LLM ───────────────────────────────────
LLM_PROVIDER=openrouter
LLM_API_KEY=sk-or-v1-xxx
LLM_MODEL=openai/gpt-oss-120b:free
LLM_BASE_URL=https://openrouter.ai/api/v1
OPENROUTER_API_KEY=sk-or-v1-xxx
# ── Embedding ─────────────────────────────
EMBEDDING_PROVIDER=openrouter # hashing | openrouter | local_transformer
EMBEDDING_API_KEY=sk-or-v1-xxx
EMBEDDING_MODEL=openai/text-embedding-ada-002
EMBEDDING_BASE_URL=https://openrouter.ai/api/v1
EMBEDDING_DIMENSIONS=1536
# ── ASR (语音识别) ────────────────────────
ASR_PROVIDER=asr_api # whisper.cpp | funasr_sensevoice | asr_api
ASR_API_KEY=<asr key>
ASR_API_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1
ASR_API_MODEL=mimo-v2.5-asr
# ── 其他模型 ──────────────────────────────
MIMO_BASE_URL=https://token-plan-cn.xiaomimimo.com/anthropic
MIMO_API_KEY=<mimo key>
# ── Object Storage (RAG 知识库文件) ──────
SPACES_ENDPOINT=https://sgp1.digitaloceanspaces.com
SPACES_REGION=sgp1
SPACES_BUCKET=bodysense-storage
SPACES_ACCESS_KEY_ID=<Spaces Key>
SPACES_SECRET_ACCESS_KEY=<Spaces Secret>
# ── 功能开关 ──────────────────────────────
ASK_USER_ENABLED=true
React 前端(apps/web)
前端只能放公开配置,绝对不能放密钥:
# 唯二需要的变量
VITE_API_BASE_URL=/api # 生产用相对路径,nginx 反代
VITE_WS_URL=wss://body.bakersean.top/ws
绝对不能放前端的变量:
DATABASE_URL ← 用户可以看到打包后的 JS
REDIS_PASSWORD
JWT_SECRET_KEY
SPACES_SECRET_ACCESS_KEY
LLM_API_KEY
Caddy(反向代理)
APP_DOMAIN=body.bakersean.top
ACME_EMAIL=admin@bakersean.top
Watchtower(自动更新)
WATCHTOWER_POLL_INTERVAL=300
本地 vs 生产:数据库连接地址
| 变量 | 本地开发 | 生产(Managed DB) | 生产(Droplet 内容器) |
|---|---|---|---|
DB_HOST | 127.0.x.x | private-db-pgsql-sgp1-58909-do-user-29522374-0.i.db.ondigitalocean.com | postgres(Docker 服务名) |
DB_PORT | 5432 | 25060 | 5432 |
DB_SSLMODE | disable | require | disable |
REDIS_HOST | localhost | private-db-vk-sgp1-30226-do-user-29522374-0.g.db.ondigitalocean.com | redis(Docker 服务名) |
REDIS_PORT | 6384 | 25061 | 6379 |
REDIS_TLS | false | true | false |
关键: 生产环境用 VPC/private 地址(内网通信,免费、低延迟),本地开发用 Public 地址。
你的 DigitalOcean 资源清单
PostgreSQL 18(Managed)
Host: private-db-pgsql-sgp1-58909-do-user-29522374-0.i.db.ondigitalocean.com
Port: 25060
User: doadmin
Pass: <YOUR_PG_PASSWORD>
DB: defaultdb
SSL: require
Valkey 8(Managed Redis 兼容)
Host: private-db-vk-sgp1-30226-do-user-29522374-0.g.db.ondigitalocean.com
Port: 25061
User: default
Pass: <YOUR_VALKEY_PASSWORD>
TLS: required
Spaces Object Storage
Endpoint: https://sgp1.digitaloceanspaces.com
Bucket: bodysense-storage
Region: sgp1
URL: https://bodysense-storage.sgp1.digitaloceanspaces.com
Droplet
IP: <4C/8G 服务器 IP>
SSH: ssh DO-bodysense
生产环境配置文件示例
.env.production(非敏感,可提交 Git)
# ── 应用基础 ──────────────────────────────
NODE_ENV=production
TZ=Asia/Shanghai
# ── Docker 镜像 ───────────────────────────
REGISTRY=crpi-cv97phwhms6wy4as.cn-hangzhou.personal.cr.aliyuncs.com
ACR_NAMESPACE=bodysense
WEB_TAG=prod-latest
API_TAG=prod-latest
AI_TAG=prod-latest
# ── Caddy ─────────────────────────────────
APP_DOMAIN=body.bakersean.top
ACME_EMAIL=admin@bakersean.top
# ── Watchtower ────────────────────────────
WATCHTOWER_POLL_INTERVAL=300
# ── PostgreSQL(Managed,VPC 地址)────────
DB_HOST=private-db-pgsql-sgp1-58909-do-user-29522374-0.i.db.ondigitalocean.com
DB_PORT=25060
DB_NAME=defaultdb
DB_USER=doadmin
DB_SSLMODE=require
# ── Valkey/Redis(Managed,VPC 地址)──────
REDIS_HOST=private-db-vk-sgp1-30226-do-user-29522374-0.g.db.ondigitalocean.com
REDIS_PORT=25061
# ── Go API ────────────────────────────────
API_PORT=8080
API_HOST=0.0.x.x
JWT_ACCESS_TTL_HOURS=168
JWT_REFRESH_TTL_HOURS=720
CORS_ORIGINS=https://body.bakersean.top
# ── AI Service ────────────────────────────
AI_SERVICE_PORT=8100
AI_SERVICE_URL=http://ai-service:8100
EMBEDDING_PROVIDER=openrouter
EMBEDDING_DIMENSIONS=1536
LLM_PROVIDER=openrouter
LLM_MODEL=openai/gpt-oss-120b:free
LLM_BASE_URL=https://openrouter.ai/api/v1
EMBEDDING_BASE_URL=https://openrouter.ai/api/v1
# ── Spaces ────────────────────────────────
SPACES_ENDPOINT=https://sgp1.digitaloceanspaces.com
SPACES_REGION=sgp1
SPACES_BUCKET=bodysense-storage
# ── 前端 ──────────────────────────────────
VITE_API_BASE_URL=/api
VITE_WS_URL=wss://body.bakersean.top/ws
.env.production.local(敏感,不提交 Git)
# ── 数据库密码 ────────────────────────────
DB_PASSWORD=<YOUR_PG_PASSWORD>
# ── Redis 密码 ────────────────────────────
REDIS_PASSWORD=<YOUR_VALKEY_PASSWORD>
# ── JWT ───────────────────────────────────
JWT_SECRET_KEY=<openssl rand -base64 32 生成>
# ── LLM API Keys ─────────────────────────
OPENROUTER_API_KEY=sk-or-v1-xxx
LLM_API_KEY=sk-or-v1-xxx
EMBEDDING_API_KEY=sk-or-v1-xxx
# ── 其他模型 ──────────────────────────────
MIMO_API_KEY=xxx
ASR_API_KEY=xxx
# ── Spaces 密钥 ──────────────────────────
SPACES_ACCESS_KEY_ID=xxx
SPACES_SECRET_ACCESS_KEY=xxx
docker-compose.prod.yml 改造要点
当前 docker-compose.prod.yml 里硬编码了 postgres 和 redis 作为 Docker 服务。切换到 Managed DB 后需要改:
改之前(Droplet 内容器)
environment:
DB_HOST: postgres # Docker 服务名
DB_PORT: 5432
REDIS_HOST: redis # Docker 服务名
REDIS_PORT: 6379
改之后(Managed DB)
environment:
DB_HOST: ${DB_HOST} # 从 .env.production 读取 VPC 地址
DB_PORT: ${DB_PORT} # 25060
DB_SSLMODE: ${DB_SSLMODE} # require
REDIS_HOST: ${REDIS_HOST} # VPC 地址
REDIS_PORT: ${REDIS_PORT} # 25061
同时可以删除 postgres 和 redis 两个 service 定义,以及对应的 volumes。
双模式兼容(推荐过渡方案)
不想一次性切换?可以用环境变量控制:
# docker-compose.prod.yml 中,api 的 environment:
environment:
DB_HOST: ${DB_HOST:-postgres}
DB_PORT: ${DB_PORT:-5432}
DB_SSLMODE: ${DB_SSLMODE:-disable}
REDIS_HOST: ${REDIS_HOST:-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
- 用 Managed DB 时:
.env.production里填 VPC 地址 - 用 Droplet 内容器时:不填这两个变量,走默认值
启动命令
本地开发(Droplet 内容器 + .env)
cd /opt/bodysense
docker compose -f docker/docker-compose.yml --profile dev up -d
生产环境(Managed DB + .env.production + .env.production.local)
cd /opt/bodysense
docker compose -f docker/docker-compose.prod.yml \
--env-file .env.production \
--env-file .env.production.local \
up -d
安全检查清单
-
.env.production.local权限设为600(chmod 600) -
.gitignore已排除所有.env*除了.env.example -
.env.example只有变量名,无真实密码 - Managed DB 只开放 VPC 访问,不开 Public(本地调试时临时开 Trusted Source)
- Spaces 的 Access Key 权限最小化(只给需要的 bucket)
- JWT_SECRET_KEY 是随机生成的强密钥,不是
change-me