先说结论:GitHub Actions 里的 Database,不是让你把数据库长期跑在 CI 机器上,而是在自动化流程里临时拉起一个真实数据库,用来跑迁移、接口测试、数据校验和集成测试。这东西最大的价值是让每次提交代码之后,代码都能在一个干净、可复现的数据库环境里被验证一遍,而不是全靠本地“我这边能跑”来兜底。
这篇文章适合正在写 GitHub Actions 工作流、需要做自动化测试或数据迁移验证的开发者看。最值得关注的点有三个:怎么用服务容器拉起数据库、怎么让应用真正连上它、出问题时按什么顺序排查。后面还会涉及 MySQL、PostgreSQL、Redis、SQL Server 这几类常见数据库的配置差异和坑点。
我会按实际落地顺序拆开讲,先讲清楚为什么需要数据库,再讲最小配置,然后是单任务到批量的推进方式,最后是排错和边界。
1. 先理解 GitHub Actions 里的数据库到底解决什么问题
1.1 为什么 CI 里要有一个真正的数据库
很多项目的自动化测试并不是每次都需要数据库,单元测试可以用内存数据库或者 mock 掉 DAO 层。但一旦涉及以下场景,假数据库就不够用了:
- ORM 实体映射发生了变化,需要跑真实迁移来验证表结构是否正确。
- 查询语句对数据库方言敏感,比如用了特定索引写法、JSON 函数、分页语法。
- 接口测试要校验数据落库后的返回结果。
- 数据迁移脚本要从旧版本结构升级到新版本结构。
- 批量任务处理完后,需要确认影响行数和数据一致性。
这些场景如果不在真实数据库里跑,等到上线才暴露问题,排查成本要高好几倍。GitHub Actions 里临时拉一个数据库,目的就是把这些验证提前到每次提交。
1.2 本地环境与 CI 数据库的核心差异
本地开发时,你用的是自己机器上的 MySQL、PostgreSQL 或者 Docker 容器,数据是长期存在的,网络、权限、字符集都是自己配置好的。CI 环境则完全不一样:
- 每次运行都是全新的虚拟机或容器,数据库不会保留上次的数据。
- 默认端口、账号密码、字符集、时区都需要显式配置。
- 数据库服务启动需要时间,而 CI 任务不会等你,所以必须有健康检查。
- 跑完任务后环境直接销毁,不会有人帮你维护数据。
这就决定了你在 CI 里连接数据库的方式,不能照抄本地开发。最典型的就是 localhost 和容器网络的区别,以及密码和端口必须通过环境变量注入而不是写死在配置文件里。
1.3 适合哪些人、哪些任务
如果你属于下面这几类情况,这个方案就很值得用:
- 后端项目用 Spring Boot、Django、Rails、Node.js 等框架,测试需要真实数据源。
- 项目里有 Flyway、Liquibase、Prisma Migrate、Alembic 这类迁移工具。
- 团队要求每次合并代码前必须跑完整集成测试。
- 你正在做数据管道、定时任务、报表服务,需要验证数据写入逻辑。
反过来说,如果项目完全没有数据库依赖,或者所有数据访问都被 mock 掉了,那就不需要为 CI 特意配数据库,省下的运行时间更划算。
2. 服务容器是跑数据库最省事的方案
2.1 什么是 GitHub Actions 服务容器
GitHub Actions 的 workflow 里可以声明一个services字段,用来在任务运行期间启动一个或多个独立的容器。这个容器和主任务运行在同一台虚拟机上,主任务可以通过 localhost 或容器网络访问它。
这就是“GitHub Actions Database”最常见的实现方式。你不需要在 workflow 里手动装数据库软件,也不需要维护 Docker Compose 文件,而是直接用官方镜像把数据库拉起来。
服务容器的核心优势是隔离和可重复。每次运行都用同一个镜像、同一套环境变量、同一个启动命令,不会因为开发机器上的残留配置导致结果不一致。
2.2 最小配置:一个 MySQL 8 的 workflow
下面是一个最简示例,用 MySQL 8 作为测试数据库,跑一个 Python 项目的迁移和测试:
name: database-test on: push: branches: [ main ] jobs: test-with-mysql: runs-on: ubuntu-latest services: mysql: image: mysql:8.4 env: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: app_test ports: - 3306:3306 options: >- --health-cmd "mysqladmin ping --silent" --health-interval 5s --health-timeout 5s --health-retries 10 steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install dependencies run: pip install -r requirements.txt - name: Run migrations run: alembic upgrade head env: DATABASE_URL: mysql+pymysql://root:root@127.0.0.1:3306/app_test - name: Run tests run: pytest env: DATABASE_URL: mysql+pymysql://root:root@127.0.0.1:3306/app_test这个文件看起来不多,但每一段都有讲究。下面拆开解释。
2.3 健康检查为什么要单独配置
服务容器启动后,主任务不会自动等数据库真正可用。MySQL 容器从开始启动到能接受连接,通常需要几秒到十几秒。如果主任务立刻跑迁移,大概率会报Can't connect to MySQL server或者Connection refused。
options里的--health-cmd就是用来解决这个问题的。GitHub Actions 会按--health-interval的间隔执行健康检查命令,直到返回成功或者超过--health-retries次数。只有健康状态变为 healthy 之后,主任务里的步骤才会继续执行。
这里有两个容易踩的坑:
- 健康检查命令必须能在容器内部运行。
mysqladmin ping在 MySQL 镜像里可用,但换到其他镜像就要换成对应命令。 - 健康检查不设置超时和重试次数的话,默认值可能导致任务启动后很快超时。数据库镜像体积大、首次拉取慢时尤其明显。
2.4 端口、账号、超时怎么设计
ports把容器内的 3306 映射到虚拟机上的 3306。主任务用127.0.0.1:3306访问。实际项目中需要注意:
- 如果同时启动多个数据库服务,端口要避免冲突。比如 MySQL 用 3306,PostgreSQL 用 5432,Redis 用 6379。
- 账号密码通过
env注入。MySQL 镜像里MYSQL_ROOT_PASSWORD是必须的,否则容器可能启动失败。 - 连接字符串里的主机名,在同一个 job 里用
127.0.0.1或localhost都可以。如果你拆成了多个 job,那就要考虑 job 之间的数据传递,通常需要一个 artifact 或者重新初始化数据库。
注意:服务容器的生命周期和当前 job 绑定,job 跑完容器就销毁。不要想着在这个环境里保存长期数据。
3. 单条流程跑通,从提交代码到测试完成
3.1 先写最小 workflow
很多人第一次配置数据库服务时,喜欢直接把自己项目的完整测试套件全部放进去,结果一报错根本分不清是数据库问题还是业务代码问题。我建议先跑一个最小 workflow,只验证三件事:服务起来了、数据库能连接、能执行一条最简单的 SQL。
可以直接在 workflow 里加一个独占的验证步骤:
- name: Verify database connection run: | mysql --host=127.0.0.1 --port=3306 --user=root --password=root -e "SELECT VERSION();" env: MYSQL_PWD: root这个步骤能通过,说明网络、端口、账号、健康检查都没问题。之后再加入迁移和测试步骤,排查范围就小很多。
3.2 迁移脚本在哪里执行
迁移脚本的执行时机很关键,应该在测试开始之前,而且顺序不能乱:
- 先确认数据库连接正常。
- 再执行迁移,让表结构变成最新版本。
- 然后插入必要的种子数据。
- 最后启动测试套件。
如果你的项目用 Flyway 或 Liquibase,执行方式通常是 Maven 或 Gradle 插件。用 Prisma 的话就是prisma migrate deploy。用 Alembic 就是alembic upgrade head。关键是这些命令必须拿到同一个DATABASE_URL,并且迁移工具能识别数据库方言。
如果迁移脚本里包含存储过程、索引、触发器等复杂对象,建议先在本地相同版本的数据库镜像里验证一遍。CI 环境报错时最常看到的不是 SQL 语法错误,而是权限不足、字符集不匹配、时间戳默认值不一致。
3.3 验证数据库真的连上了
除了在 workflow 里执行 SQL 验证,还要注意应用侧是否真的读到了 CI 环境变量。很多项目里存在多个配置文件,比如application.yml、application-test.yml、.env,如果环境变量的优先级低于配置文件,数据库地址就会被覆盖。
验证方法很简单:在测试日志里打印数据库地址和连接参数,看是不是 CI 环境的值。不要只在本地验证,因为本地可能根本没有覆盖逻辑的问题。
3.4 单任务跑通之后再扩大范围
单条 workflow 跑通之后,再考虑扩展。比如:
- 把数据库版本换成项目实际使用的版本。
- 加入缓存依赖,减少镜像拉取时间。
- 用矩阵策略同时测试多个数据库版本。
- 在需要时加入批量数据初始化脚本。
每一步都保持“先最小可用,再逐步加复杂度”的节奏。不要一上来就在一个 workflow 里塞数据库、缓存、队列、外部 API 模拟服务,那样出了错很难定位。
4. 常见数据库的配置差异,按需选型
4.1 主流数据库服务容器参数对比
不同数据库镜像的初始化方式不一样,直接套用 MySQL 的写法会踩坑。下面这张表是我日常使用时的基础参数参考:
| 数据库 | 镜像示例 | 默认端口 | 必要环境变量 | 健康检查命令 |
|---|---|---|---|---|
| MySQL | mysql:8.4 | 3306 | MYSQL_ROOT_PASSWORD, MYSQL_DATABASE | mysqladmin ping --silent |
| PostgreSQL | postgres:16 | 5432 | POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB | pg_isready -U postgres |
| Redis | redis:7 | 6379 | 无(可选 REDIS_PASSWORD) | redis-cli ping |
| SQL Server | mcr.microsoft.com/mssql/server:2022-latest | 1433 | ACCEPT_EULA=Y, MSSQL_SA_PASSWORD | /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $MSSQL_SA_PASSWORD -C -Q "SELECT 1" |
注意,这张表的镜像版本、端口和变量名需要以你的实际环境为准。原始材料没有给出明确版本,落地时先确认依赖版本是更稳妥的做法。
4.2 MySQL / PostgreSQL 常见坑
MySQL 最常见的坑是认证方式。新版 MySQL 8 默认使用 caching_sha2_password,如果应用驱动版本太旧,会报认证失败。解决方案是换新驱动,或者在创建用户时指定mysql_native_password,但后者是临时手段,不建议长期依赖。
PostgreSQL 的常见坑是编码和时区。POSTGRES_DB创建出来的数据库默认使用的是模板库的编码,如果应用需要 UTF8MB4 或特定时区,要在初始化脚本里处理。另一个坑是pg_isready只检查进程是否可连接,不检查数据库是否已经完成初始化,所以在复杂场景下还要结合重试逻辑。
4.3 Redis 作为缓存或队列怎么配
Redis 在 CI 里通常不需要像关系型数据库那样做迁移,但连接方式要注意:
- Redis 默认没有密码,本地开发可能无所谓,CI 里一般也不用配。
- 如果应用配置里强制要求密码,可以给服务容器加
REDIS_PASSWORD环境变量,但不同镜像的密码参数名不一样。 - 健康检查用
redis-cli ping,返回 PONG 才算 ready。 - Redis 在 CI 里适合用来验证缓存逻辑、队列消费、限流和分布式锁,但不适合做持久化数据验证。
有人问 Redis Insight 客户端连接 CI 里的数据库怎么连。本地场景下,Redis Insight 连的是你自己机器的 Redis 实例,CI 里的服务容器是临时的,不会暴露到公网,所以也不存在从 Insight 连接 CI 中 Redis 的需要。如果你要在本地复现 CI 环境,直接本地起一个同样的 Redis 容器,用同样的端口和密码参数来连接就行。
4.4 SQL Server 的启动时间问题
SQL Server 在 GitHub Actions 里相对特殊,因为镜像体积大、启动时间明显更长。很多人第一次跑会遇到wait on the database engine recovery handle failed这类错误。这类错误的主因通常是两个:一个是容器启动后数据库引擎还在恢复阶段,主任务就开始连接了;另一个是 SA 密码不符合 SQL Server 的复杂度要求,导致引擎无法初始化。
处理思路是:
- 把健康检查的重试次数调大,间隔适当缩短,比如 10 次重试、10 秒间隔。
- 密码必须包含大小写字母、数字和符号,不能用太简单的弱密码。
- 连接时使用正确的工具路径,新版镜像里的 sqlcmd 路径已经改成了
/opt/mssql-tools18/bin/sqlcmd,并且需要加-C参数跳过加密证书校验。 - 如果步骤里使用 JDBC 连接,连接字符串里要加
encrypt=false或trustServerCertificate=true,否则会报证书错误。
5. 数据准备、迁移和测试隔离
5.1 迁移流程怎么放进 pipeline
迁移脚本是项目的一部分,应该跟着代码一起提交,而不是临时从某个环境导出。在 pipeline 里执行迁移时,要确保以下几点:
- 迁移命令只能在一个 job 里执行一次,避免并发重复执行。
- 迁移工具的配置文件和 CI 环境变量分离,不能用写死的数据库地址。
- 迁移脚本应支持幂等执行。不是指所有操作必须可重放,但至少要能处理“已经执行过”的情况。
如果你的迁移脚本在本地能跑通,但在 CI 里失败,优先检查数据库版本差异和初始化脚本的执行顺序。很多迁移语句在 MySQL 8.0 和 5.7 里的表现不一样,PostgreSQL 14 和 16 也有函数行为变化。
5.2 种子数据和测试数据分开
测试数据分为两类:一类是业务默认数据,比如字典表、配置表,这类数据可以通过初始化脚本在每个测试库创建时导入;另一类是测试用例特有的数据,比如某个用户、某个订单,这类数据应该在测试框架里创建,而不是依赖全局种子数据。
混在一起会导致测试之间互相影响。比如一个测试删除了所有订单,另一个测试假设订单存在,那整个测试套件就会出现随机失败。
5.3 清理和重建策略
CI 数据库每次都是全新启动,所以通常不需要额外清理。但如果你在同一个 job 里执行多轮测试,或者用脚本反复插入数据,就要在关键测试之间做清理。
清理策略可以很简单:
- 每个测试类运行前删除并重建 schema。
- 使用事务回滚来隔离测试数据。
- 在测试结束后删除测试创建的数据。
不建议在 CI 里做“全量数据导入再全量清理”的操作,那会显著拉长构建时间。数据库的验证目标是逻辑正确性,不是数据完整性演练。
6. 常见报错与排查顺序
6.1 连接不上:先看服务状态和端口
连接不上是最常见的报错,但原因可能有好几种。排查顺序应该是:
- 先看服务容器是否健康。在 workflow 日志里找
Container service相关日志,看健康检查是否通过。 - 再看端口映射。
ports写的是3306:3306,那主任务用127.0.0.1:3306;如果只写了3306,端口会被随机映射,连接信息要从 job 日志里找。 - 然后看账号密码。
MYSQL_ROOT_PASSWORD和连接字符串里的密码必须一致。 - 最后看驱动和连接参数。MySQL 8 的认证方式、SQL Server 的加密参数、PostgreSQL 的 SSL 参数都可能影响连接。
不要一上来就怀疑应用代码。先手动执行一条命令验证数据库本身,再逐步扩大范围。
6.2 健康检查一直失败
健康检查失败通常表现为任务卡在启动阶段,或者提示Container ... is unhealthy。常见原因有:
- 健康检查命令写错,镜像里根本没有这个命令。
- 环境变量没有传给健康检查。注意
options里的--health-cmd是在容器内执行的,不一定能读到 workflow 里设置的env。 - 数据库初始化需要的时间超过了
--health-retries能覆盖的总时长。解决办法是增加重试次数或加长间隔。 - 端口映射和健康检查无关,健康检查是在容器内部进行的,即使映射不对,检查也可能通过。
6.3 数据库引擎恢复类错误
这一类错误在 SQL Server 上尤其常见,比如wait on the database engine recovery handle failed. check the sql server error log。看到这个信息时,第一反应不应该是改应用代码,而是去确认数据库引擎到底有没有正常启动。
具体做法:
- 拉长健康检查重试次数。
- 在 workflow 中增加一步,先确认 SQL Server 的错误日志路径和状态。
- 检查 SA 密码是否满足复杂度要求。
- 确认 SQL Server 镜像版本和宿主机的 CPU 架构是否匹配。
这类错误有时候是镜像首次启动时需要初始化系统数据库,耗时较长,重试次数不足导致的。所以先确认启动时间,再调整参数。
6.4 应用侧无法识别数据库类型
有些框架会通过 JDBC URL 自动推断底层数据库类型。如果应用报了类似couldn't deduct database type from data source的错,通常不是服务容器的问题,而是连接串没有被正确传递,或者数据库类型检查逻辑读取到了空值。
排查思路:
- 打印最终使用的
DATABASE_URL或SPRING_DATASOURCE_URL,确认不是空值。 - 检查环境变量名称是否和框架的配置键一致。
- 检查
.env文件或者配置文件是否覆盖了环境变量。 - 如果是多模块项目,确认连接串是否传到了真正执行迁移的那个模块。
这个问题经常发生在环境变量拼写不一致,或者嵌套 workflow 时变量没有透传的情况。
6.5 排查的顺序和日志怎么看
我把通用排查顺序总结成下面这几步,遇到任何数据库报错都可以按这个顺序走:
| 序号 | 排查项 | 看什么 |
|---|---|---|
| 1 | 现象 | 完全连不上、超时、迁移失败、测试数据异常 |
| 2 | 服务状态 | 健康检查是否通过、容器是否正常退出 |
| 3 | 输入 | 连接串、账号密码、端口号、数据库名是否正确 |
| 4 | 环境 | 镜像版本、驱动版本、系统架构、资源限制 |
| 5 | 参数 | 健康检查间隔、超时、重试次数、加密选项 |
| 6 | 日志 | workflow 日志、数据库容器日志、应用日志、迁移工具日志 |
实际操作中,数据库容器日志往往比应用日志更早暴露问题。比如 MySQL 会直接告诉你 authentication 失败还是 character set 配置错误,PostgreSQL 会告诉你哪些初始化语句失败。
7. 本地与生产化边界,别把 CI 数据库当正式库
7.1 本地开发怎么保持一致
CI 里用服务容器跑数据库,最大的好处是本地也可以复用同一套配置。如果你本地装了 Docker,可以直接用同样的镜像和环境变量启动一个本地数据库,做到“本地和 CI 用同一个数据库版本”。
比较实用的做法是写一个 compose 文件,把 CI 里用到的数据库镜像、端口、密码、健康检查都整理进去。本地开发时docker compose up -d,CI 时用 workflow 里的services字段。两边配置尽量保持一致,可以大幅减少“本地能跑,CI 挂了”的情况。
7.2 批量、矩阵和缓存
当项目需要同时验证多个数据库版本时,可以用 GitHub Actions 的矩阵策略:
strategy: matrix: mysql-version: [8.0, 8.4]在矩阵任务里,每个版本都会启动一个独立的数据库服务容器,测试互不干扰。但要注意运行时间和资源消耗会成倍增加。如果项目预算有限,优先只测生产环境使用的数据库版本。
镜像拉取是 CI 数据库使用中最耗时的一环。actions/cache可以缓存 Docker 镜像层,但 GitHub Actions 的 service 容器会优先在本地查找镜像。如果你的 workflow 经常重复使用同一个数据库镜像,可以考虑用 Docker 层缓存加快启动速度。
7.3 什么时候该用托管数据库服务
服务容器适合单元测试、集成测试和短暂的数据验证。但不适合以下场景:
- 压测需要大量数据集和长时间运行。
- 需要数据库备份、恢复、监控等运维能力。
- 测试需要使用固定的公网连接地址。
- 团队需要多个流程共享同一个数据库状态。
这些场景应该考虑使用云数据库或内网托管的数据库实例,而不是在 workflow 里临时拉起容器。CI 服务容器的定位是“快速、可丢弃、只服务于当前任务”,不要强行扩展成持久化存储。
7.4 最后留几个实用的检查习惯
做完这一整套配置之后,真正要长期保持的其实不是某个具体命令,而是几个习惯:
- 每次改动数据库相关配置时,先跑最小连接验证,再跑完整测试。
- 数据库镜像版本和应用连接驱动版本一起升级,避免驱动太旧导致认证协议不兼容。
- 遇到随机失败,先查是不是测试数据互相污染,再查数据库配置。
- 不要把 CI 数据库的地址写进代码仓库的默认配置文件里,一律通过环境变量注入。
- 定期清理不再使用的 workflow 版本和缓存,避免磁盘空间和缓存策略影响任务启动。
踩过几次之后我发现,很多问题不是工具能力不够,而是前置环境和输入材料没有处理干净。GitHub Actions 里的数据库也是一样,配置写清楚、健康检查给足时间、连接参数和实际环境一致,大部分报错都能在前面几步排查掉。如果只是学习,默认配置通常够用;如果要长期使用,就要把日志、输出目录和任务队列提前整理好。