Docker Compose 与 .env
TaskFlow 在本机只需要装 Node 和 Docker:PostgreSQL、测试数据库、Adminer 和 Redis 都跑在 Docker Compose 里。 本页介绍 docker-compose.yml 文件、它的 profile、常用命令,以及应用读取的 .env 文件。这个文件在 J1 创建,Redis 在 J9 加入,app 服务在 J11 加入。
docker-compose.yml 文件
# docker compose up -d -> PostgreSQL + Adminer for `npm run start:dev`
# docker compose --profile queue up -d -> + Redis for the BullMQ queue (J9)
# docker compose --profile app up -d -> + the API itself, built from the Dockerfile (J11)
services:
db:
image: postgres:16-alpine
container_name: taskflow-db
environment:
POSTGRES_USER: taskflow
POSTGRES_PASSWORD: taskflow
POSTGRES_DB: taskflow
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U taskflow -d taskflow"]
interval: 5s
timeout: 3s
retries: 10
# Throwaway database for e2e tests: data lives in memory (tmpfs).
db-test:
image: postgres:16-alpine
container_name: taskflow-db-test
environment:
POSTGRES_USER: taskflow
POSTGRES_PASSWORD: taskflow
POSTGRES_DB: taskflow_test
ports:
- "5433:5432"
tmpfs:
- /var/lib/postgresql/data
adminer:
image: adminer
ports:
- "8081:8080"
depends_on:
- db
redis:
image: redis:7-alpine
profiles: [queue]
ports:
- "6379:6379"
app:
build: .
profiles: [app]
environment:
NODE_ENV: production
PORT: 3000
DB_HOST: db
DB_PORT: 5432
DB_USER: taskflow
DB_PASSWORD: taskflow
DB_NAME: taskflow
# Empty unless exported: the app then refuses to start (validateEnv), while
# the other services still work without it.
JWT_SECRET: ${JWT_SECRET:-}
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
volumes:
pgdata:服务
| 服务 | 镜像 | 本机端口 | Profile | 作用 |
|---|---|---|---|---|
db | postgres:16-alpine | 5432 | (无) | 开发数据库,数据存在 pgdata 卷里 |
db-test | postgres:16-alpine | 5433 | (无) | 端到端测试数据库,数据在内存里(tmpfs) |
adminer | adminer | 8081 | (无) | 查看数据库的 Web 界面 |
redis | redis:7-alpine | 6379 | queue | 通知用的 BullMQ 队列(可选) |
app | 由 Dockerfile 构建 | 3000 | app | 容器化的 API 本身 |
db 是 npm run start:dev 用的数据库。有了命名卷 pgdata,数据在重启后依然保留。healthcheck 每 5 秒运行一次 pg_isready:它告诉 Compose PostgreSQL 什么时候真正能接受连接,app 服务就依赖这一点。
db-test 是第二个 PostgreSQL 实例,端口 5433,数据库名 taskflow_test。它的数据目录挂载成 tmpfs,也就是放在 内存里:数据库很快,容器每次停止后都会变回空库。端到端测试先在上面执行迁移,再清空各表,然后才开始运行 (J10)。这就是计划里的“Compose 服务”方案,用来代替 Testcontainers。
adminer 是一个数据库管理 Web 界面,地址 http://localhost:8081。登录时选择 PostgreSQL,服务器填 db(Compose 网络里的服务名,不是 localhost),用户名和密码都是 taskflow,数据库填 taskflow。
redis 给 J9 的 BullMQ 队列做存储。和计划里一样,它是可选的:只有定义了 REDIS_URL,TaskFlow 才会加载队列模块。
app 用 Dockerfile(J11)构建镜像,以 NODE_ENV=production 运行,并连接 db 服务。有三个细节:
JWT_SECRET: ${JWT_SECRET:-}:值来自你的 shell(或docker-compose.yml旁边的.env文件),否则就是空字符串。 文件里不写任何密钥,其他服务也用不到它:docker compose up -d或docker compose up -d db-test什么都不用 export 就能运行。- 没有密钥时,是
app容器拒绝启动:环境变量校验(J7)会立刻报 “property JWT_SECRET has failed the following constraints: minLength, isString”并终止。启用appprofile 之前, 先 export 一个真正的值。 - 带
condition: service_healthy的depends_on会等db的healthcheck通过后,才启动 API。
镜像启动时不执行迁移:迁移是单独的一步。npm run migration:run:prod 在同一镜像的一次性容器里执行编译后的迁移 (dist/),在 API 启动之前完成(顺序见常用命令,细节见 J11)。 app 容器拿不到 REDIS_URL:它在没有队列的情况下运行,这时通知只会写进日志。
已在 TaskFlow 上验证:镜像能构建,migration:run:prod 在空库上建好了所有表,随后容器以 node 用户启动,/health 返回 ok。部署到服务器还没有验证过。
export 的密钥对 npm run start:dev 同样生效
对从这个 shell 启动的任何应用,export 的变量都优先于 .env.* 文件(读取顺序):为 app profile export 的 JWT_SECRET 会覆盖 .env.development 里的值。
与计划中文件的差异
计划给出的文件包含 db、db-test、adminer 和 redis,后者标注为“可选(J9)”,但每次 docker compose up 都会启动。TaskFlow 把 redis 放到 queue profile 后面,并加了 app 服务(app profile),这是 计划 J11 里要求的(“Dockerfile 以及 Compose 里的 app 服务”)。
Compose profile
声明了 profiles 的服务,只有在它的某个 profile 被启用(--profile)或在命令行里被显式点名时才会启动。没有 profile 的服务,在每次不点名任何服务的 docker compose up 时都会启动。
| 命令 | 启动的服务 |
|---|---|
docker compose up -d | db、db-test、adminer |
docker compose --profile queue up -d | 同上 + redis |
docker compose --profile app up -d --build | 同上 + app(重新构建镜像;需已 export JWT_SECRET 并执行过迁移) |
docker compose --profile queue --profile app up -d | 全部(app 的条件同上) |
docker compose up -d db-test | 只有 db-test(用来跑端到端测试) |
用 docker compose config --services 验证过:不带 profile 时,列表是 db、adminer、db-test;--profile queue 会加上 redis,--profile app 会加上 app。
Compose profile 和 Spring 的 profile、和 NODE_ENV 都没有关系:它决定启动哪些容器,而不是应用怎么配置。按环境区分 配置在 J7 讲。
常用命令
docker compose up -d # 在后台启动 db、db-test 和 adminer
docker compose ps # 容器状态(以及 db 的 healthcheck)
docker compose logs -f db # 持续查看 PostgreSQL 日志
docker compose exec db psql -U taskflow -d taskflow # 开发库的 SQL 控制台
docker compose stop # 停止容器,全部保留
docker compose down # 删除容器,保留 pgdata 卷跑端到端测试时,需要一个 .env.test 文件(见下文):
docker compose up -d db-test
npm run test:e2e运行容器化的 API(app profile),按这个顺序:
export JWT_SECRET=$(openssl rand -hex 32) # 只有 app 容器需要
docker compose --profile app build
docker compose --profile app run --rm app npm run migration:run:prod
docker compose --profile app up -d数据丢失
docker compose down -v 会连 pgdata 卷一起删除,也就是整个开发数据库。之后要重新运行 npm run migration:run, 再运行 npm run seed。
.env 文件
应用从环境变量读取配置,并在启动时校验(J7)。开发和测试时,这些变量 来自 .env.* 文件。纳入版本控制的模板:
# Copy to .env.development (npm run start:dev) and .env.test (npm run test:e2e).
# Only this example file is versioned.
NODE_ENV=development
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_USER=taskflow
DB_PASSWORD=taskflow
DB_NAME=taskflow
JWT_SECRET=change-me
JWT_EXPIRES_IN=15m
# typeorm (PostgreSQL) or memory (the J2-J3 in-memory store)
PROJECTS_STORE=typeorm
# Set to enable the BullMQ notifications queue (docker compose --profile queue up -d)
# REDIS_URL=redis://localhost:6379
# Comma-separated origins allowed by CORS; unset = no cross-origin access
# CORS_ORIGIN=http://localhost:5173变量
| 变量 | 作用 | 启动时的检查 |
|---|---|---|
NODE_ENV | 运行环境:决定读哪个 .env.* 文件和日志级别 | development、test 或 production(默认 development) |
PORT | API 的 HTTP 端口 | 1 到 65535 之间的整数(默认 3000) |
DB_HOST、DB_PORT | PostgreSQL 地址 | 字符串;整数(默认 localhost、5432) |
DB_USER、DB_PASSWORD、DB_NAME | 账号和数据库名 | 必填字符串 |
JWT_SECRET | token 的签名密钥 | 必填,至少 8 个字符 |
JWT_EXPIRES_IN | access token 的有效期 | 格式如 15m、1h、7d(默认 15m) |
PROJECTS_STORE | 项目 store 的实现 | typeorm 或 memory(默认 typeorm) |
REDIS_URL | 启用通知用的 BullMQ 队列 | 可选 |
CORS_ORIGIN | CORS 允许的来源,用逗号分隔 | 在 app.setup.ts 里直接读取,不在校验 schema 中;不设置 = 不允许任何跨域访问 |
种子数据脚本还会读取 SEED_ADMIN_PASSWORD(默认 admin-password-dev),它不在 .env.example 里。
有哪些文件,哪些要纳入版本控制
| 文件 | 谁在用 | 纳入版本控制 |
|---|---|---|
.env.example | 没人用:它是用来复制的模板 | 是,唯一纳入版本控制的 .env* 文件 |
.env.development | npm run start:dev、TypeORM CLI(迁移)和种子数据脚本 | 否 |
.env.test | npm run test:e2e(未定义时 Jest 会设置 NODE_ENV=test) | 否 |
.env | 可选的兜底文件,在当前环境专属文件之后读取 | 否 |
TaskFlow 的 .gitignore(examples/.gitignore)忽略 .env*,只保留 .env.example,它的 .dockerignore 也一样: 本地环境文件既不会进 git,也不会进镜像。
创建本地文件:
cp .env.example .env.development
cp .env.example .env.test在 .env.test 里指向测试库:NODE_ENV=test、DB_PORT=5433、DB_NAME=taskflow_test。
读取顺序
AppModule 先加载 .env.${NODE_ENV},再加载 .env(见配置里的 envFilePath):
- 同一个变量在两个文件里都有时,以先读的那个为准;
- 进程环境里已经定义的变量(shell、CI、Compose 的
environment)优先于文件,就像 Spring Boot 里环境变量优先于application.yml。
正因如此,TaskFlow 的 CI 和 app 服务都不需要文件:它们直接提供变量。TypeORM CLI(src/database/data-source.ts)不 启动 Nest,它用 process.loadEnvFile() 读取同一个 .env.${NODE_ENV} 文件。
密钥
JWT_SECRET=change-me 只是开发用的值。生产环境的密钥来自密钥管理服务或 CI 的受保护变量,绝不来自纳入版本控制的文件。