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 ...@@ -27,13 +27,23 @@ PORT=3101
LOG_LEVEL=info 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 # staging: postgresql://pac:<staging-pwd>@<staging-pg-host>:5532/pac?schema=public
# production: postgresql://pac:<prod-pwd>@<prod-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 DATABASE_URL=postgresql://pac:pac@localhost:5532/pac?schema=public
# local: redis://localhost:6479
# staging+prod: redis://<host>:6479 (BullMQ 队列用,丢了会丢未消费的 plan-asset-generate 任务) # staging+prod: redis://<host>:6479 (BullMQ 队列用,丢了会丢未消费的 plan-asset-generate 任务)
REDIS_URL=redis://localhost:6479 REDIS_URL=redis://localhost:6479
......
...@@ -22,7 +22,7 @@ FROM deps AS dev ...@@ -22,7 +22,7 @@ FROM deps AS dev
COPY . . COPY . .
WORKDIR /app/apps/pac-service WORKDIR /app/apps/pac-service
RUN pnpm prisma generate RUN pnpm prisma generate
EXPOSE 3001 EXPOSE 3101
CMD ["pnpm", "dev"] CMD ["pnpm", "dev"]
# === build === # === build ===
...@@ -47,5 +47,5 @@ COPY --from=build /app/apps/pac-service/prisma ./apps/pac-service/prisma ...@@ -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/data ./apps/pac-service/data
COPY --from=build /app/apps/pac-service/package.json ./apps/pac-service/ COPY --from=build /app/apps/pac-service/package.json ./apps/pac-service/
WORKDIR /app/apps/pac-service WORKDIR /app/apps/pac-service
EXPOSE 3001 EXPOSE 3101
CMD ["node", "--enable-source-maps", "dist/main.js"] CMD ["node", "--enable-source-maps", "dist/main.js"]
...@@ -18,10 +18,13 @@ RUN pnpm install --frozen-lockfile ...@@ -18,10 +18,13 @@ RUN pnpm install --frozen-lockfile
FROM deps AS dev FROM deps AS dev
COPY . . COPY . .
WORKDIR /app/apps/pac-web WORKDIR /app/apps/pac-web
EXPOSE 3000 EXPOSE 3100
CMD ["pnpm", "dev"] CMD ["pnpm", "dev"]
FROM deps AS build 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 . . COPY . .
RUN pnpm --filter @pac/types build RUN pnpm --filter @pac/types build
WORKDIR /app/apps/pac-web WORKDIR /app/apps/pac-web
...@@ -34,5 +37,5 @@ ENV NODE_ENV=production ...@@ -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/standalone ./
COPY --from=build /app/apps/pac-web/.next/static ./apps/pac-web/.next/static 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 COPY --from=build /app/apps/pac-web/public ./apps/pac-web/public
EXPOSE 3000 EXPOSE 3100
CMD ["node", "apps/pac-web/server.js"] CMD ["node", "apps/pac-web/server.js"]
...@@ -2,7 +2,54 @@ ...@@ -2,7 +2,54 @@
> W4 末:试部署阶段手动 ssh + 跑脚本;W5+ 接 CI/CD 自动化。 > 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 @@ ...@@ -14,7 +61,7 @@
--- ---
## 首次部署 SOP(staging / production 通用) ## systemd 模式 SOP(staging / production 通用)
### 1. 服务器基础环境 ### 1. 服务器基础环境
......
# Requires a root `.env` next to this file. Keys it needs: # PAC 生产 compose — 单一真相在 apps/*/.env(跟 systemd 模式共用)
# POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB #
# DATABASE_URL / REDIS_URL # 启动:
# JWT_SECRET / JWT_REFRESH_SECRET / JWT_EXPIRES_IN / JWT_REFRESH_EXPIRES_IN # 1. cp apps/pac-service/.env.example apps/pac-service/.env 并填好(POSTGRES_*/JWT_*/DEEPSEEK_*)
# AI_GATEWAY_URL / AI_GATEWAY_API_KEY # 2. cp apps/pac-web/.env.example apps/pac-web/.env 并填好(NEXT_PUBLIC_API_BASE_URL)
# CORS_ORIGINS / EXCHANGE_CODE_TTL_SECONDS # 3. docker compose -f docker-compose.prod.yml \
# NEXT_PUBLIC_API_BASE_URL (build-time, change → rebuild web) # --env-file apps/pac-service/.env \
# Per-key meaning: see apps/pac-service/.env.example (most overlap). # --env-file apps/pac-web/.env \
# Run: docker compose -f docker-compose.prod.yml up -d --build # 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: services:
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
restart: always restart: always
environment: env_file: ./apps/pac-service/.env # 读 POSTGRES_USER/PASSWORD/DB
POSTGRES_USER: ${POSTGRES_USER:-pac}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?postgres password required}
POSTGRES_DB: ${POSTGRES_DB:-pac}
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
ports:
- "127.0.0.1:5532:5432" # 宿主仅本地访问(psql 进容器调试用)
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-pac}"] test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-pac}"]
interval: 10s interval: 10s
retries: 5 retries: 5
...@@ -28,6 +37,8 @@ services: ...@@ -28,6 +37,8 @@ services:
command: ["redis-server", "--appendonly", "yes"] command: ["redis-server", "--appendonly", "yes"]
volumes: volumes:
- redis_data:/data - redis_data:/data
ports:
- "127.0.0.1:6479:6379"
# One-shot migrator: applies pending Prisma migrations and exits. # One-shot migrator: applies pending Prisma migrations and exits.
# Runs before pac-service so the schema is current before any traffic # Runs before pac-service so the schema is current before any traffic
...@@ -41,8 +52,10 @@ services: ...@@ -41,8 +52,10 @@ services:
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
env_file: ./apps/pac-service/.env
environment: 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 working_dir: /app/apps/pac-service
command: ["npx", "prisma", "migrate", "deploy"] command: ["npx", "prisma", "migrate", "deploy"]
...@@ -59,36 +72,34 @@ services: ...@@ -59,36 +72,34 @@ services:
condition: service_started condition: service_started
pac-migrate: pac-migrate:
condition: service_completed_successfully condition: service_completed_successfully
env_file: ./apps/pac-service/.env
environment: environment:
NODE_ENV: production NODE_ENV: production
PORT: 3101 PORT: 3101
DATABASE_URL: ${DATABASE_URL} # 覆盖 .env 的 localhost URL → 走 docker 内部网络
REDIS_URL: ${REDIS_URL} DATABASE_URL: postgresql://${POSTGRES_USER:-pac}:${POSTGRES_PASSWORD:-pac}@postgres:5432/${POSTGRES_DB:-pac}?schema=public
JWT_SECRET: ${JWT_SECRET} REDIS_URL: redis://redis:6379
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}
ports: ports:
- "3101:3101" - "127.0.0.1:3101:3101"
pac-web: pac-web:
build: build:
context: . context: .
dockerfile: apps/pac-web/Dockerfile dockerfile: apps/pac-web/Dockerfile
target: prod 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 restart: always
depends_on: depends_on:
- pac-service - pac-service
env_file: ./apps/pac-web/.env
environment: environment:
NODE_ENV: production NODE_ENV: production
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
PORT: 3100 PORT: 3100
ports: ports:
- "3100:3100" - "127.0.0.1:3100:3100"
volumes: volumes:
postgres_data: 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