本节摘要:开发与测试环境模板解决"环境不一致"这个老问题:开发环境用绑定挂载加热重载、mock 外部依赖、Mailpit 本地邮箱;测试环境用测试容器加一次性数据库、健康检查前置、隔离网络。核心思想是把环境差异消灭在 Docker 层,让新同事入职当天就能跑起项目,让 CI 与本地跑同一份配置。
开发环境的第一诉求是"改代码立刻生效"。实现手段是绑定挂载加开发模式启动命令,模板如下:
services: web: build: context: ./app dockerfile: Dockerfile.dev ports: - "5000:5000" volumes: - ./app:/app environment: - FLASK_ENV=development depends_on: - db restart: always db: image: postgres:14 ports: - "5432:5432" environment: - POSTGRES_USER=dev - POSTGRES_PASSWORD=dev - POSTGRES_DB=devdb volumes: - db_data:/var/lib/postgresql/data volumes: db_data:
配套的 Dockerfile.dev 与生产 Dockerfile 分开维护:
FROM python:3.9-slim-buster WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . EXPOSE 5000 CMD ["flask", "run", "--host=0.0.0.0"]
volumes: - ./app:/app:宿主机代码目录盖住容器内 /app。注意镜像里 COPY 进的文件会被挂载遮住,启动命令用的是挂载后的版本——这正是我们要的效果。FLASK_ENV=development:Flask 的开发模式开关。flask run 在该模式下自带 reloader 和 debugger,保存 .py 文件后进程自动重载,浏览器刷新即见新代码。netstat -ano | findstr 5000 查占用,再改左侧端口即可。pip install 的依赖装在镜像层里,改 requirements.txt 后要重新 docker compose -f docker-compose.dev.yml up --build;纯代码改动不用 build。Dockerfile.dev 与生产 Dockerfile 分离:开发镜像可以多装调试工具、开着 debug,生产镜像做精简。两个 Dockerfile 的差异不要靠注释约定,直接分成两个文件,构建时用 dockerfile 参数指定。Node.js 版本的热重载:Express 进程默认不会自动重启,要加 nodemon 之类的工具,package.json 的 dev 脚本写成 nodemon index.js,Dockerfile.dev 的 CMD 指向它。热重载的机制因语言而异,但挂载的做法完全一致。
变体:VS Code Remote 开发容器:配合 devcontainer.json 可以把整个开发环境收进编辑器,配置里指定 dockerComposeFile、要进入的 service、工作目录 workspaceFolder 和要转发的端口 forwardPorts。好处是格式化工具、语言服务器、调试器全在容器里,换机器零迁移成本;代价是第一次构建镜像要等几分钟。
依赖锁文件必须提交。热重载解决了代码同步,依赖漂移是另一个污染源:两个人分别 build 镜像,pip 或 npm 解析出不同版本,行为就不一致。规矩是锁文件进仓库:Python 用 pip-tools 或 uv 生成 requirements.lock,Node.js 提交 package-lock.json,Dockerfile 里 pip install -r requirements.lock 或 npm ci 按锁文件安装。npm ci 和 npm install 的区别就在这里:ci 严格按锁文件装,install 会重新解析,开发容器里用错命令,锁文件等于没锁。
开发环境最怕"依赖别人的服务才能启动"。第三方支付、短信网关、外部 API,这些服务在开发期要么没有测试环境,要么调用要钱。我们的做法是在 compose 里起一个替身容器,应用代码通过环境变量切换地址:
services: web: build: ./app ports: - "5000:5000" environment: - PAYMENT_API_URL=http://mock-payment:8080 - SMS_API_URL=http://mock-sms:8080 mock-payment: image: wiremock/wiremock:3.5.0 volumes: - ./mocks/payment:/home/wiremock mock-sms: image: wiremock/wiremock:3.5.0 volumes: - ./mocks/sms:/home/wiremock
WireMock 是常用的 API 模拟器,把预定义的响应 JSON 放进挂载目录,它就按路径返回固定数据。替身容器的价值在于:接口契约被写成了文件,随代码仓库走,新同事 clone 下来就能跑,不会出现"我这边连不上测试环境"的扯皮。
⚠️ mock 的边界要划清楚:只 mock 外部依赖,不要 mock 自己的数据库和消息队列。自己家的组件用真容器跑,才能在开发期暴露真实的 schema 变更和消息格式问题。
应用要发邮件时,开发期绝不能真发。Mailpit 是本地 SMTP 测试工具,接收所有邮件并提供一个 Web 界面查看:
services: mailpit: image: axllent/mailpit:latest ports: - "1025:1025" - "8025:8025" web: build: ./app environment: - SMTP_HOST=mailpit - SMTP_PORT=1025 - SMTP_FROM=dev@example.com
开发库和测试库混用是测试结果不可靠的头号原因。模板里给数据库单独建测试库,用初始化脚本在首次启动时生成:
services: db: image: postgres:14 environment: - POSTGRES_USER=dev - POSTGRES_PASSWORD=dev - POSTGRES_DB=devdb volumes: - db_data:/var/lib/postgresql/data - ./initdb:/docker-entrypoint-initdb.d:ro
initdb 目录里放两个脚本:01_create_testdb.sql 建 testdb 库,02_seed.sql 灌基础数据。应用连接串按环境切换:开发连 devdb,测试连 testdb。测试跑完要清理数据时,直接 docker compose exec db dropdb -U dev testdb 再重建,比逐表 DELETE 干净利落。
表结构变更走迁移工具。initdb 脚本只负责"首次建库",上线后的表结构变更要靠迁移工具:Python 生态用 Alembic、Java 生态用 Flyway 或 Liquibase,Node.js 生态各有对应方案。迁移文件按版本号递增,应用启动时自动检查并执行未应用的迁移。这个习惯和容器化的配合点在于:测试环境每次重建库,迁移工具从头跑到最新版本,正好验证了迁移脚本的完整性;如果哪天重建后迁移报错,说明迁移链上有缺口,趁早修比上线后修便宜得多。
测试数据用工厂不用手写 SQL。灌测试数据最忌在用例里硬编码 INSERT,表结构一变全挂。业界通行做法是数据工厂:代码里定义对象构造器,字段默认值集中管理,用例里只覆盖关心的字段。数据库模板只管"库存在、表结构对",数据怎么造是测试代码的事,两层职责分开,测试环境才稳定。
自动化测试环境的核心形态是:被测服务加依赖服务加一个跑测试的容器,测试容器跑完即退出,退出码决定成败:
services: api: build: context: ./api dockerfile: Dockerfile ports: - "3000:3000" environment: - NODE_ENV=test depends_on: - db db: image: mongo:latest volumes: - db_data:/data/db test: build: context: ./test dockerfile: Dockerfile environment: - API_URL=http://api:3000 depends_on: - api command: npm run test restart: on-failure volumes: db_data:
command: npm run test:测试容器的主进程就是测试命令,进程退出码就是测试结果。0 代表全部通过,非 0 代表有失败——这个约定让 compose 能直接当 CI 用。restart: on-failure:测试失败时自动重启重跑。本地调试时这个策略很方便;CI 里要改成不重启,让失败现场保留在日志里。node:16 基础镜像里 COPY 测试代码,CMD 写成 npm test。执行方式:docker compose -f docker-compose.test.yml up --build,全程日志输出到终端;只跑一次并回收用 docker compose run --rm test,跑完自动删容器,不会残留。
测试容器最怕的不是测试失败,而是"数据库还没就绪测试就开跑",然后报一堆连接超时,看起来像代码问题。正确的做法是让 depends_on 带上条件:
services: api: build: ./api depends_on: db: condition: service_healthy db: image: postgres:14 healthcheck: test: ["CMD-SHELL", "pg_isready -U dev -d devdb"] interval: 5s timeout: 5s retries: 10 test: build: ./test depends_on: api: condition: service_healthy
condition: service_healthy 的含义:等依赖服务的健康检查通过后才启动本服务。它把"睡 10 秒再连"这种赌博式写法彻底干掉——机器慢就多等,快就少等,永远等的是就绪信号而不是拍脑袋的时长。注意 api 服务自己也要配 healthcheck,否则条件永远不满足,compose 会卡在等待里。
开发和测试环境跑在同一台机器时,网络隔离是硬要求。compose 默认每个项目一个独立网络,但同一项目里混跑开发与测试就要显式分开:
services: api: networks: - testnet db: networks: - testnet test: networks: - testnet networks: testnet: driver: bridge
三个要点:一是测试环境的服务只进 testnet,不给它访问开发网络的能力;二是网络名带项目前缀或环境后缀,project_testnet 与 project_devnet 一眼可辨;三是测试容器不映射任何端口,它只在内部网络里访问 API——端口映射是给宿主机用的,测试不需要。
💡 一份配置多用:把 dev 和 test 的差异收敛到环境变量里,用 -f docker-compose.yml -f docker-compose.override.yml 叠加配置,公共部分只写一份。第 4 章还会用同样的思路叠加生产配置。
开发与测试环境共用一套模板时,最大的隐患是配置串味:测试连了开发库、开发用了测试的 mock 地址,跑出来的结果谁都不信。拆分靠的是环境文件:
# 开发环境用 docker compose --env-file .env.dev -f docker-compose.yml up -d # 测试环境用 docker compose --env-file .env.test -f docker-compose.yml up
compose 里所有 ${变量} 引用都从环境文件取值:.env.dev 里写开发库地址、mock 开关、调试级别;.env.test 里写测试库地址、测试报告路径。两个文件都进版本仓库,新同事 clone 下来就能跑。唯一的纪律是:密码和密钥不进环境文件,它们该走 CI 的密钥仓库或本机未提交的 .env.local,避免把生产凭据提交进 git 历史。
测试报告的落盘。测试容器退出后,容器内的报告文件就没了。把报告目录挂出来:
test: build: ./test volumes: - ./test-results:/app/reports
挂载后不管是 pytest 的 html 报告还是 Jest 的覆盖率目录,都会留在宿主机 test-results 里,CI 能直接收集成产物,本地也能打开看。注意挂载目录的属主:测试容器以 root 跑没问题,非 root 镜像要先把宿主机目录的写权限放开。
down 与 -v 的杀伤力。docker compose down 默认不删卷,数据还在;加上 -v 会连命名卷一起删。测试环境里 down -v 是"彻底重置"的利器,一个命令把测试库、缓存全部清空回到出厂状态。但手滑在开发环境执行,开发数据库就没了。我们的规矩:测试环境允许用 -v,开发环境永远只写 down,重置开发库用初始化脚本而不是删卷。
CI 里的用法。流水线里跑测试时,环境变量的来源是流水线注入而不是 .env 文件:docker compose --env-file <(echo "$ENV_CONTENT") up --exit-code-from test。--exit-code-from 让 compose 以指定容器的退出码收尾,测试失败时流水线立即标红,不用再解析日志。这套写法在 GitHub Actions 和 Jenkins 里通用,模板本身不用改。
| 维度 | 开发环境 | 测试环境 |
|---|---|---|
| 代码来源 | 绑定挂载实时同步 | 镜像内固定版本 |
| 数据 | 可持久化保留 | 一次性或可重建 |
| 外部依赖 | mock 容器替身 | 同样 mock 或真容器 |
| 启动方式 | up 常驻 | up 跑完退出 |
| 结果判定 | 人工观察 | 退出码自动化判定 |
| 端口 | 按需映射 | 一律不映射 |
| 网络 | 开发专用网络 | 隔离测试网络 |
下一节给部署栈加上眼睛——监控与日志栈模板,让每个服务的健康状态都变成看得见的数字。