Skip to content

Docker Compose 与 .env ​

TaskFlow 在本机只需要装 Node 和 Docker:PostgreSQL、测试数据库、Adminer 和 Redis 都跑在 Docker Compose 里。 本页介绍 docker-compose.yml 文件、它的 profile、常用命令,以及应用读取的 .env 文件。这个文件在 J1 创建,Redis 在 J9 加入,app 服务在 J11 加入。

docker-compose.yml 文件 ​

yaml
# 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作用
dbpostgres:16-alpine5432(无)开发数据库,数据存在 pgdata 卷里
db-testpostgres:16-alpine5433(无)端到端测试数据库,数据在内存里(tmpfs)
admineradminer8081(无)查看数据库的 Web 界面
redisredis:7-alpine6379queue通知用的 BullMQ 队列(可选)
app由 Dockerfile 构建3000app容器化的 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”并终止。启用 app profile 之前, 先 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 -ddb、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 讲。

常用命令 ​

bash
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 文件(见下文):

bash
docker compose up -d db-test
npm run test:e2e

运行容器化的 API(app profile),按这个顺序:

bash
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.* 文件。纳入版本控制的模板:

dotenv
# 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)
PORTAPI 的 HTTP 端口1 到 65535 之间的整数(默认 3000)
DB_HOST、DB_PORTPostgreSQL 地址字符串;整数(默认 localhost、5432)
DB_USER、DB_PASSWORD、DB_NAME账号和数据库名必填字符串
JWT_SECRETtoken 的签名密钥必填,至少 8 个字符
JWT_EXPIRES_INaccess token 的有效期格式如 15m、1h、7d(默认 15m)
PROJECTS_STORE项目 store 的实现typeorm 或 memory(默认 typeorm)
REDIS_URL启用通知用的 BullMQ 队列可选
CORS_ORIGINCORS 允许的来源,用逗号分隔在 app.setup.ts 里直接读取,不在校验 schema 中;不设置 = 不允许任何跨域访问

种子数据脚本还会读取 SEED_ADMIN_PASSWORD(默认 admin-password-dev),它不在 .env.example 里。

有哪些文件,哪些要纳入版本控制 ​

文件谁在用纳入版本控制
.env.example没人用:它是用来复制的模板是,唯一纳入版本控制的 .env* 文件
.env.developmentnpm run start:dev、TypeORM CLI(迁移)和种子数据脚本否
.env.testnpm run test:e2e(未定义时 Jest 会设置 NODE_ENV=test)否
.env可选的兜底文件,在当前环境专属文件之后读取否

TaskFlow 的 .gitignore(examples/.gitignore)忽略 .env*,只保留 .env.example,它的 .dockerignore 也一样: 本地环境文件既不会进 git,也不会进镜像。

创建本地文件:

bash
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 的受保护变量,绝不来自纳入版本控制的文件。