BodySense 环境变量配置方案

BodySense 项目的环境变量分层策略:哪个服务需要哪些变量,本地 vs 生产,Droplet 内置 DB vs DigitalOcean Managed DB。

#type / howto #status / seed #tech / ops #tech / ai

[!info] related notes

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_HOST127.0.x.xprivate-db-pgsql-sgp1-58909-do-user-29522374-0.i.db.ondigitalocean.compostgres(Docker 服务名)
DB_PORT5432250605432
DB_SSLMODEdisablerequiredisable
REDIS_HOSTlocalhostprivate-db-vk-sgp1-30226-do-user-29522374-0.g.db.ondigitalocean.comredis(Docker 服务名)
REDIS_PORT6384250616379
REDIS_TLSfalsetruefalse

关键: 生产环境用 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 里硬编码了 postgresredis 作为 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

同时可以删除 postgresredis 两个 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 权限设为 600chmod 600
  • .gitignore 已排除所有 .env* 除了 .env.example
  • .env.example 只有变量名,无真实密码
  • Managed DB 只开放 VPC 访问,不开 Public(本地调试时临时开 Trusted Source)
  • Spaces 的 Access Key 权限最小化(只给需要的 bucket)
  • JWT_SECRET_KEY 是随机生成的强密钥,不是 change-me

相关 MOC

创建于 2026/7/4 更新于 2026/7/15