Commit bcb406a0 by luoqi

feat(compose): per-app .env as single source of truth for prod compose

- docker-compose.prod.yml 重构:
  - env_file: 指令读 apps/pac-service/.env + apps/pac-web/.env
  - environment: 段覆盖 DATABASE_URL/REDIS_URL 走 docker 内部网络
  - pac-web build.args.NEXT_PUBLIC_API_BASE_URL 用 ${} 插值
  - 启动需双 --env-file(给 CLI 插值,跟 env_file 注入容器是两套作用域)
- apps/pac-service/.env.example 加 POSTGRES_USER/PASSWORD/DB 段(compose 模式 postgres 容器读)
- apps/pac-web/Dockerfile 加 ARG NEXT_PUBLIC_API_BASE_URL,build 时 inline
- Dockerfile EXPOSE 端口对齐(3101/3100)
- deploy/README.md 加 compose 模式启动 SOP
parent dd53c6c2
......@@ -27,13 +27,23 @@ PORT=3101
LOG_LEVEL=info
# ─── Postgres 初始化(仅 compose 模式用 — postgres 容器启动时读这三个)──
# systemd 模式可忽略(你自己起 postgres 时指定)
POSTGRES_USER=pac
POSTGRES_PASSWORD=pac-change-me-in-prod
POSTGRES_DB=pac
# ─── 数据库 / 缓存 ────────────────────────────────────────────────────
# local: postgresql://pac:pac@localhost:5532/pac?schema=public
# DATABASE_URL / REDIS_URL 的 host 部分按部署模式不同:
# systemd 模式: host=localhost port=5532/6479 (走宿主端口映射)
# compose 模式: host=postgres/redis port=5432/6379 (走 docker 内部网络)
# → compose 会在 environment: 段自动覆盖此处的 URL,这里写 localhost 即可
#
# staging: postgresql://pac:<staging-pwd>@<staging-pg-host>:5532/pac?schema=public
# production: postgresql://pac:<prod-pwd>@<prod-pg-host>:5532/pac?schema=public
DATABASE_URL=postgresql://pac:pac@localhost:5532/pac?schema=public
# local: redis://localhost:6479
# staging+prod: redis://<host>:6479 (BullMQ 队列用,丢了会丢未消费的 plan-asset-generate 任务)
REDIS_URL=redis://localhost:6479
......
......@@ -22,7 +22,7 @@ FROM deps AS dev
COPY . .
WORKDIR /app/apps/pac-service
RUN pnpm prisma generate
EXPOSE 3001
EXPOSE 3101
CMD ["pnpm", "dev"]
# === build ===
......@@ -47,5 +47,5 @@ COPY --from=build /app/apps/pac-service/prisma ./apps/pac-service/prisma
COPY --from=build /app/apps/pac-service/data ./apps/pac-service/data
COPY --from=build /app/apps/pac-service/package.json ./apps/pac-service/
WORKDIR /app/apps/pac-service
EXPOSE 3001
EXPOSE 3101
CMD ["node", "--enable-source-maps", "dist/main.js"]
......@@ -18,10 +18,13 @@ RUN pnpm install --frozen-lockfile
FROM deps AS dev
COPY . .
WORKDIR /app/apps/pac-web
EXPOSE 3000
EXPOSE 3100
CMD ["pnpm", "dev"]
FROM deps AS build
# NEXT_PUBLIC_* 必须 build-time 注入(Next.js 把它们 inline 进客户端 bundle)
ARG NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL}
COPY . .
RUN pnpm --filter @pac/types build
WORKDIR /app/apps/pac-web
......@@ -34,5 +37,5 @@ ENV NODE_ENV=production
COPY --from=build /app/apps/pac-web/.next/standalone ./
COPY --from=build /app/apps/pac-web/.next/static ./apps/pac-web/.next/static
COPY --from=build /app/apps/pac-web/public ./apps/pac-web/public
EXPOSE 3000
EXPOSE 3100
CMD ["node", "apps/pac-web/server.js"]
......@@ -2,7 +2,54 @@
> W4 末:试部署阶段手动 ssh + 跑脚本;W5+ 接 CI/CD 自动化。
## 文件清单
## 两种部署模式
| 模式 | 适用 | 文件 |
|---|---|---|
| **systemd 裸跑** | 宿主有 node/pnpm,想直接 systemctl 管理 | `deploy.sh` + `systemd/*.service` |
| **docker-compose** | 宿主只装 docker,想完全隔离 | `docker-compose.prod.yml`(项目根) |
两种模式共用同一份 `apps/pac-service/.env` + `apps/pac-web/.env`(单一真相)。
### compose 模式启动(推荐共享服务器)
```bash
# 1. 填配置
cp apps/pac-service/.env.example apps/pac-service/.env # POSTGRES_*/JWT_*/DEEPSEEK_*/DW_*
cp apps/pac-web/.env.example apps/pac-web/.env # NEXT_PUBLIC_API_BASE_URL
# 2. 起所有容器
docker compose -f docker-compose.prod.yml \
--env-file apps/pac-service/.env \
--env-file apps/pac-web/.env \
up -d --build
# 3. 验证
docker compose -f docker-compose.prod.yml ps
curl http://127.0.0.1:3101/health
curl http://127.0.0.1:3100
```
日常操作:
```bash
# 更新代码 + 重建
git pull && docker compose -f docker-compose.prod.yml --env-file apps/pac-service/.env --env-file apps/pac-web/.env up -d --build
# 看日志
docker compose -f docker-compose.prod.yml logs -f pac-service
# 只重启某个服务(不重建 image)
docker compose -f docker-compose.prod.yml restart pac-service
# 进容器调试
docker compose -f docker-compose.prod.yml exec pac-service sh
```
⚠️ 改了 `NEXT_PUBLIC_*` 必须 `up -d --build`(build-time inline);改后端 .env 只要 `restart pac-service`
---
## systemd 模式文件清单
| 文件 | 用途 |
|---|---|
......@@ -14,7 +61,7 @@
---
## 首次部署 SOP(staging / production 通用)
## systemd 模式 SOP(staging / production 通用)
### 1. 服务器基础环境
......
# Requires a root `.env` next to this file. Keys it needs:
# POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB
# DATABASE_URL / REDIS_URL
# JWT_SECRET / JWT_REFRESH_SECRET / JWT_EXPIRES_IN / JWT_REFRESH_EXPIRES_IN
# AI_GATEWAY_URL / AI_GATEWAY_API_KEY
# CORS_ORIGINS / EXCHANGE_CODE_TTL_SECONDS
# NEXT_PUBLIC_API_BASE_URL (build-time, change → rebuild web)
# Per-key meaning: see apps/pac-service/.env.example (most overlap).
# Run: docker compose -f docker-compose.prod.yml up -d --build
# PAC 生产 compose — 单一真相在 apps/*/.env(跟 systemd 模式共用)
#
# 启动:
# 1. cp apps/pac-service/.env.example apps/pac-service/.env 并填好(POSTGRES_*/JWT_*/DEEPSEEK_*)
# 2. cp apps/pac-web/.env.example apps/pac-web/.env 并填好(NEXT_PUBLIC_API_BASE_URL)
# 3. docker compose -f docker-compose.prod.yml \
# --env-file apps/pac-service/.env \
# --env-file apps/pac-web/.env \
# up -d --build
#
# 说明:--env-file 给 compose CLI 做 ${VAR} 插值(POSTGRES_* / NEXT_PUBLIC_*);
# env_file: 指令则把同一份文件注入容器内 process.env(JWT_* / DEEPSEEK_* 等)。
# 两者作用域不同,都要配。多个 --env-file 后者覆盖前者(本例无冲突 key)。
#
# 端口(宿主机):
# postgres 5532 / redis 6479 / pac-service 3101 / pac-web 3100
#
# 内部网络:容器之间用服务名访问(postgres:5432 / redis:6379),容器内 port 用 image 默认
services:
postgres:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER:-pac}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?postgres password required}
POSTGRES_DB: ${POSTGRES_DB:-pac}
env_file: ./apps/pac-service/.env # 读 POSTGRES_USER/PASSWORD/DB
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "127.0.0.1:5532:5432" # 宿主仅本地访问(psql 进容器调试用)
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-pac}"]
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-pac}"]
interval: 10s
retries: 5
......@@ -28,6 +37,8 @@ services:
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis_data:/data
ports:
- "127.0.0.1:6479:6379"
# One-shot migrator: applies pending Prisma migrations and exits.
# Runs before pac-service so the schema is current before any traffic
......@@ -41,8 +52,10 @@ services:
depends_on:
postgres:
condition: service_healthy
env_file: ./apps/pac-service/.env
environment:
DATABASE_URL: ${DATABASE_URL}
# 覆盖 .env 里 host=localhost 的连接串 → 走 docker 内部网络
DATABASE_URL: postgresql://${POSTGRES_USER:-pac}:${POSTGRES_PASSWORD:-pac}@postgres:5432/${POSTGRES_DB:-pac}?schema=public
working_dir: /app/apps/pac-service
command: ["npx", "prisma", "migrate", "deploy"]
......@@ -59,36 +72,34 @@ services:
condition: service_started
pac-migrate:
condition: service_completed_successfully
env_file: ./apps/pac-service/.env
environment:
NODE_ENV: production
PORT: 3101
DATABASE_URL: ${DATABASE_URL}
REDIS_URL: ${REDIS_URL}
JWT_SECRET: ${JWT_SECRET}
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-2h}
JWT_REFRESH_EXPIRES_IN: ${JWT_REFRESH_EXPIRES_IN:-7d}
AI_GATEWAY_URL: ${AI_GATEWAY_URL}
AI_GATEWAY_API_KEY: ${AI_GATEWAY_API_KEY}
CORS_ORIGINS: ${CORS_ORIGINS}
EXCHANGE_CODE_TTL_SECONDS: ${EXCHANGE_CODE_TTL_SECONDS:-60}
# 覆盖 .env 的 localhost URL → 走 docker 内部网络
DATABASE_URL: postgresql://${POSTGRES_USER:-pac}:${POSTGRES_PASSWORD:-pac}@postgres:5432/${POSTGRES_DB:-pac}?schema=public
REDIS_URL: redis://redis:6379
ports:
- "3101:3101"
- "127.0.0.1:3101:3101"
pac-web:
build:
context: .
dockerfile: apps/pac-web/Dockerfile
target: prod
args:
# build-time inline 进客户端 bundle,改了必须 --build
# 来自 apps/pac-web/.env(启动时用 --env-file 指定那份)
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
restart: always
depends_on:
- pac-service
env_file: ./apps/pac-web/.env
environment:
NODE_ENV: production
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
PORT: 3100
ports:
- "3100:3100"
- "127.0.0.1:3100:3100"
volumes:
postgres_data:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment