
一个很常见的场景本地跑得好好的一提交代码CI 却报数据库连接失败。很多开发者第一次把项目接入 GitHub Actions 时都会在这类问题上卡住。原因是本地开发环境里数据库是“常驻服务”程序启动就能连上而 GitHub Actions 的 runner 是一台全新临时机器任务是跑完即销毁的。你不能默认数据库“总在那里”。这篇文章要讲清楚的核心问题是如何在 GitHub Actions 里获得一个干净、可重复、按需销毁的数据库环境。我会从概念原理讲起给出 MySQL、PostgreSQL、Redis 的实际接入示例再列出常见的连接失败场景和排查思路。读完你会得到一个判断GitHub Actions 中的数据库不是传统意义上的“数据库运维”而是一个临时的、隔离的、服务于代码验证的测试资源。1. 这篇文章真正要解决的问题GitHub Actions 里使用数据库最常见的诉求有三个。第一类是单元测试或集成测试需要真实数据库。很多项目在本地开发时用 SQLite 或者 Docker 容器跑数据库只要把代码推到 GitHub 上就需要在 CI 中还原一个可用的数据库否则测试会跳过、报错或无法覆盖 SQL 逻辑。第二类是数据库迁移和脚本验证。比如你写了一个schema.sql、一套 Flyway 或 Liquibase 迁移脚本或者一个存储过程你需要确认它在全新数据库上能正常执行。本地数据库可能已经“脏”了数据残留会让脚本跑过头而 CI 每次给的是一个全新库恰好能暴露迁移脚本不幂等、依赖历史数据的问题。第三类是验证应用与数据库的兼容性。比如 ORM 的实体映射、方言配置、字符集、事务隔离级别、数据库驱动版本是否匹配。这些问题往往在本地开发时被忽略等上了测试环境或者生产环境才炸出来。我的核心判断是GitHub Actions 里的数据库核心价值不是“部署数据库”而是“提供可重复的数据库验证环境”。它要解决的是三个矛盾本地数据库状态不可控、生产数据库不能被 CI 随便碰、多任务并行时互相污染。理解这一点后你选择哪种接入方式、怎么配置服务、怎么处理数据都会有更清晰的思路。这篇文章适合正在搭建 CI 的前后端开发者、把老项目迁移到 GitHub Actions 的团队以及想用自动化测试提升代码质量的个人开发者。不需要你精通数据库运维但希望你已经了解 GitHub Actions 的基本语法比如workflow、job、step这些概念。2. GitHub Actions 与数据库的核心逻辑要理解 GitHub Actions 中的数据库先要理解它的执行模型。一个 GitHub Actions 工作流由若干job组成每个job在一个全新的 runner 上执行。只要用的是官方托管的 runner这台机器在任务结束后就会被回收。所以你在 job 里安装的软件、写入的文件、启动的进程都不会保留到下一次运行。这是它和传统 Jenkins 等常驻 CI 服务器最大的区别。数据库也一样。如果你在 job 的 step 里手动执行apt-get install mysql-server再启动 MySQL 服务这个数据库只存在于当前 job 的生命周期内。下一次运行又是全新机器一切重来。GitHub Actions 针对这类依赖服务提供了更优雅的写法services服务容器。你可以在 job 定义里声明一个或多个 Docker 容器比如 MySQL、PostgreSQL、Redis。GitHub Actions 会为这个 job 启动一个专用的虚拟网络把服务容器和应用步骤放在同一个网络中让它们通过服务名互相访问。这里有一个很容易踩的坑服务容器内部的端口默认不会映射到 runner 宿主机的localhost。如果在 step 里写mysql -h 127.0.0.1可能连不上因为 3306 只在服务容器里监听。正确做法是用服务名比如mysql这个服务名本身就代表容器的网络地址。当然你也可以显式声明ports把端口映射到宿主机这样就能用localhost访问了。两种写法我会在后面的示例里分别演示。还有一个重要概念是“临时性”。GitHub Actions 的数据库每次都是全新初始化的数据不会跨 job 保留。这看起来像个限制但其实是优点测试结果是可重复的不会因为上一次运行残留数据而失败。多分支并行跑互不干扰每个 job 有独立数据库。测试数据不会污染生产环境。因此你不需要编写复杂的清理逻辑也不用担心“CI 数据库垃圾越来越多”。每次跑完一切归零这正是自动化测试追求的效果。3. 环境准备与前置条件开始实操之前先确认几项基础条件。第一你需要一个 GitHub 账号和一个 Git 仓库。仓库的 Actions 功能默认是开启的一般不需要额外配置。如果你的仓库是企业私有仓库确认管理员没有禁用 Actions 即可。第二代码仓库中需要创建.github/workflows目录。GitHub 会自动识别这个目录下的 YAML 文件作为工作流配置。文件路径大概是.github/workflows/mysql-test.yml第三确认你的项目需要哪种数据库。选择标准很简单尽量与生产环境保持一致。生产用 MySQL 8就不要在 CI 里用 MySQL 5.7生产用 PostgreSQL就不要为了“省事”换成 SQLite。因为很多 SQL 特性和行为在数据库之间差异极大名字相同的功能实际语义可能完全不同。第四了解 GitHub 托管 runner 的软件环境。官方托管的ubuntu-latest镜像里预装了很多软件但是版本会随着镜像更新而变化。不要依赖“runner 自带 MySQL”这种假设更可靠的方式是通过services声明一个指定版本的数据库镜像或者通过setup-*动作安装你需要的版本。本文示例中会用mysql:8.0和postgres:16作为演示镜像具体版本请以你的项目实际依赖为准。第五如果你需要连接 GitHub Actions 之外的资源比如云数据库、私有制品库要提前把访问凭证配置为仓库的Secrets。工作流中通过${{ secrets.XXX }}引用不要把明文密码写进 YAML 文件。4. 数据库接入 CI 的三种方式GitHub Actions 中接入数据库主流方式有三种。它们的适用场景、隔离性和运维成本差很多。4.1 方式一Service Container推荐在jobs.job_id.services下定义一个服务容器这是最接近 GitHub Actions 设计意图的方式。写法简洁隔离性最好容器只服务于当前 job。jobs: test: runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: root ports: - 3306:3306 steps: # 这里可以访问数据库这种方式的好处是数据库镜像版本可控、启动参数可控、健康检查可控。缺点是需要了解 Docker 镜像的基本配置方式比如 MySQL 的MYSQL_ROOT_PASSWORD、PostgreSQL 的POSTGRES_PASSWORD等环境变量。4.2 方式二在 Runner 上直接安装也可以在 step 里通过sudo apt-get install安装数据库并手动启动服务。这样可以访问 runner 上完整的系统能力和传统 CI 服务器更像。但问题也很明显安装时间更长、版本取决于 runner 镜像源、每次都要重新安装、服务启动方式会比较绕。一般情况下只有找不到官方 Docker 镜像或者需要和 runner 系统上某个组件深度集时时才推荐这种方式。4.3 方式三连接外部数据库第三种方式是在 CI 中直接连接一个外部数据库比如云数据库、公司内网数据库或已有的测试环境。这种方式省去了安装和初始化的时间但风险很高CI 每次运行都会向共享数据库写入数据可能导致数据污染并发跑多个 job 时可能互相冲突数据库凭证一旦进入 secrets管理不善很容易泄露。我的建议是默认不要用外部数据库。如果你的场景真的需要比如验证某个数据库驱动和云数据库的兼容性那一定要使用独立的测试库、隔离的账号并且保证执行的操作可回滚、可幂等。不要把生产数据库或其他团队共用的数据库作为 GitHub Actions 的目标库。三种方式的对比方式隔离性启动速度版本可控性风险推荐度Service Container高中高低推荐Runner 直接安装中慢中中部分场景连接外部数据库低快低高不推荐5. 完整示例MySQL 数据库工作流下面用一个最小但完整的示例演示如何在 GitHub Actions 中启动 MySQL执行 SQL 脚本并验证数据库可用。先创建文件.github/workflows/mysql-test.ymlname: MySQL Integration Test on: push: branches: [ main ] pull_request: jobs: mysql-test: runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: demo MYSQL_USER: demo MYSQL_PASSWORD: demo ports: - 3306:3306 options: - --health-cmdmysqladmin ping -h 127.0.0.1 -u root -proot --health-interval10s --health-timeout5s --health-retries10 steps: - name: Checkout repository uses: actions/checkoutv4 - name: Wait for MySQL to be ready run: | for i in $(seq 1 30); do if mysqladmin ping -h 127.0.0.1 -u root -proot --silent; then echo MySQL is ready exit 0 fi echo Waiting for MySQL... sleep 2 done echo MySQL did not become ready in time 2 exit 1 - name: Run schema script run: mysql -h 127.0.0.1 -u demo -pdemo demo scripts/schema.sql - name: Run smoke query run: | mysql -h 127.0.0.1 -u demo -pdemo demo -e \ SELECT COUNT(*) AS table_count FROM information_schema.tables WHERE table_schemademo;这段配置里有几个关键点。services.mysql.env中的环境变量是 MySQL Docker 镜像初始化的依据包括 root 密码、默认数据库、默认用户。GitHub Actions 会等容器启动后再执行下面的 steps但“启动”不等于“数据库可接受连接”所以需要健康检查。options中的health-cmd是 Docker 的健康检查指令health-interval和health-retries控制检查频率和重试次数。ports将容器的 3306 端口映射到 runner 宿主机因此 step 中可以用127.0.0.1访问。如果你去掉ports配置那么 step 里应该使用服务名mysql而不是127.0.0.1例如mysql -h mysql -u demo -pdemo demo -e SELECT 1这里真正容易踩坑的地方是“端口映射”和“服务名访问”的混用。很多初学者明明配置了services却仍然在代码里写localhost导致应用连不上。一个简单经验配了ports用127.0.0.1没配ports用服务名。scripts/schema.sql是项目中的一个 SQL 文件比如-- 文件路径scripts/schema.sql CREATE TABLE users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(64) NOT NULL UNIQUE, email VARCHAR(128) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );在 CI 中这条 SQL 会在一台全新 MySQL 实例上执行。如果脚本中存在“先删除表再建表”之类的逻辑也要保证它在新库上能正确执行因为新库里本来就没有表。运行这个工作流后你会看到控制台输出类似MySQL is ready smoke query ------------- | table_count | ------------- | 1 | -------------table_count为 1说明users表创建成功。如果输出 0说明 schema 脚本没有执行或者执行到了别的数据库需要检查-d参数指定的库名是否正确。6. 完整示例PostgreSQL 应用测试与迁移接下来演示一个更贴近实际项目的场景Node.js 应用连接 PostgreSQL先执行迁移文件再运行一个检查数据库状态的脚本。先看工作流文件.github/workflows/node-postgres.ymlname: Node.js PostgreSQL Test on: push: branches: [ main ] jobs: test: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_USER: app POSTGRES_PASSWORD: app POSTGRES_DB: appdb ports: - 5432:5432 options: - --health-cmdpg_isready -U app -d appdb --health-interval10s --health-timeout5s --health-retries5 steps: - name: Checkout repository uses: actions/checkoutv4 - name: Set up Node.js uses: actions/setup-nodev4 with: node-version: 20 cache: npm - name: Install dependencies run: npm ci - name: Run database migration run: | PGPASSWORDapp psql -h 127.0.0.1 -U app -d appdb \ -f db/migrations/001_create_users.sql - name: Run database check script run: node src/check-db.js env: DATABASE_URL: postgres://app:app127.0.0.1:5432/appdb这里PGPASSWORDapp是给 psql 客户端提供的密码环境变量。如果你不想在命令里写密码也可以在 step 的env中配置- name: Run database migration env: PGPASSWORD: app run: | psql -h 127.0.0.1 -U app -d appdb -f db/migrations/001_create_users.sql迁移文件内容-- 文件路径db/migrations/001_create_users.sql CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(64) NOT NULL UNIQUE, email VARCHAR(128) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );数据库检查脚本文件路径src/check-db.jsconst { Client } require(pg); async function main() { const client new Client({ connectionString: process.env.DATABASE_URL, }); await client.connect(); const res await client.query( SELECT COUNT(*) AS total FROM users ); console.log(users count:, res.rows[0].total); await client.end(); } main().catch((err) { console.error(database check failed:, err); process.exit(1); });项目依赖中需要包含pg驱动package.json中至少要有{ name: github-actions-db-demo, version: 1.0.0, private: true, dependencies: { pg: ^8.11.3 } }这个示例的核心是应用代码通过DATABASE_URL环境变量拿到连接串连接的是 CI 中临时启动的 PostgreSQL。迁移脚本先创建表应用脚本再查询表。如果迁移文件、驱动版本、SQL 语法有任何问题这个工作流都会报错。预期输出是users count: 0如果users表不存在node src/check-db.js会抛出类似relation users does not exist的错误说明迁移没有执行成功或者迁移执行到了错误的数据库。6.1 顺便接入 Redis 服务如果项目还依赖 Redis只需要在同一个 job 的services里再加一个配置redis: image: redis:7-alpine ports: - 6379:6379 options: - --health-cmdredis-cli ping --health-interval10s --health-timeout5s --health-retries5然后在应用测试脚本中设置env: REDIS_URL: redis://127.0.0.1:6379/0需要注意的是Redis 默认没有密码GitHub Actions 的临时网络环境里这样使用是可行的但在外部网络环境或自托管 runner 上一定要开启认证并限制访问。7. 常见问题与排查思路GitHub Actions 里使用数据库报错场景高度集中。我把常见问题和排查方式整理成表格你可以直接对照定位。问题现象可能原因排查方式解决方案连接被拒绝报ECONNREFUSED数据库容器尚未就绪或端口映射不对查看 step 日志、用健康检查命令探测配置 health check等待服务 ready 后再跑访问命令报Access denied for user数据库账号密码、主机或权限配置错误检查 services 中的 env 和连接命令中的用户统一账号密码使用 root 账号调试确认 MySQL 8 认证插件兼容性PostgreSQL 报database appdb does not existPOSTGRES_DB未设置或连接串中的库名拼写错误查看容器环境变量和连接串显式设置POSTGRES_DB仔细核对连接串数据库启动很慢应用连接超时容器健康检查未生效或镜像冷启动较慢观察 job 启动耗时、查看容器日志增加健康检查重试次数在应用层增加连接重试本地能连CI 连不上本地用了 localhost但 CI 中的服务容器没映射端口检查 YAML 中是否有ports配置没有ports时改用服务名有ports时才能使用127.0.0.1某些框架报无法识别数据库类型如couldnt deduct database type应用层的数据库方言或类型配置缺失只给了 JDBC/连接串还不够查看应用启动日志确认数据库类型配置项在环境变量中补充数据库类型/方言参数SQL Server 相关报错如wait on the database engine recovery handle failedSQL Server 容器初始化失败或系统资源不足查看容器日志、检查内存和磁盘避免在低配 runner 上跑重型 SQL Server增加健康检查并等待恢复Redis 可视化工具连不上 Actions 里的 Redis临时服务容器生命周期短且网络对外不可达不要在 CI 中依赖外部可视化工具CI 中直接用 redis-cli 验证本地复现用本地 Redis每次运行都要重新拉取镜像耗时很长托管 runner 是全新机器镜像无缓存查看日志中镜像拉取时间使用自托管 runner 并预热镜像或使用更小的镜像减少拉取耗时还有一个常见误区很多人喜欢在 step 里写sleep 30来等数据库就绪。这其实并不可靠——机器负载高时 30 秒可能不够空闲时又白白浪费时间。更稳妥的做法是用健康检查命令写一个轮询脚本比如前面 MySQL 示例中那样或者直接信任 Docker 的health-cmd配合health-retries。如果容器一直不能进入 healthy再考虑用docker logs查看容器内部日志。8. 安全、成本与最佳实践8.1 安全边界在 GitHub Actions 中使用数据库安全的第一原则是临时数据库永远不要和生产环境混为一谈。临时库的密码写在 YAML 里问题不大因为整个环境是隔离的、用完即毁的但如果是连接外部数据库就必须把所有凭证放进secrets并设置最小权限账号。举几个必须遵守的底线不要把生产数据库的连接字符串写进 workflow 文件。不要在 CI 中对共享数据库执行DROP DATABASE、TRUNCATE等破坏性操作。如果需要把部分真实数据导入临时库做测试必须先脱敏禁止直接导出生产数据到 CI 环境。使用第三方 Marketplace Action 时要评估供应链风险尽量锁定版本或 commit SHA不要直接引用一个不定期更新的master分支。8.2 成本控制GitHub Actions 的托管 runner 按分钟计费数据库相关的镜像拉取和容器启动都会计入 job 时长。以下几点可以帮助控制成本在允许的情况下使用较小的数据库镜像比如alpine变体。尽量在一个 job 中复用一个数据库服务而不是拆成多个 job 各自启动数据库。不要为了“图方便”在 CI 中跑需要大量资源的分析型数据库任务。自托管 runner 虽然需要自己维护但可以预拉镜像、缓存依赖长期看更省钱。8.3 工程实践建议从项目工程质量角度看数据库接入 GitHub Actions 后有几点建议值得长期坚持。数据库镜像版本要与生产环境一致。CI 的价值在于提前发现兼容性问题如果 CI 用 MySQL 5.7、生产用 MySQL 8.0那很多问题只能在更晚的阶段暴露。一致性越高CI 越有价值。数据库迁移脚本要保证幂等。GitHub Actions 每次使用全新数据库看起来“幂等”不是必须的但一旦你需要把 CI 测试数据扩展到预发环境或者同一个 job 重试多次非幂等脚本会立刻变成麻烦。CREATE TABLE IF NOT EXISTS、INSERT ... ON CONFLICT DO NOTHING这类写法更安全。每个 job 使用独立的数据库。如果同一个 workflow 里有多个 job 需要跑测试不要共享同一个服务容器或外部数据库。GitHub Actions 的 job 默认互相隔离你只需要在每个 job 下分别定义services即可。这样并行执行时不会因为数据竞争导致随机失败。日志和错误信息要保留。当 CI 数据库报错时第一步永远是看日志。GitHub Actions 的网页控制台中可以看到每个 step 的完整输出。把关键查询的日志打出来把数据库容器状态打印出来能显著缩短排错时间。8.4 更合理的自动化策略在项目早期就把数据库纳入 CI是成本最低的选择。很多团队第一个 CI 只跑编译和单元测试数据库相关的集成测试留到人工阶段结果每次发版前都要靠人工点一遍。更好的做法是新建仓库时就把数据库服务写进标准 workflow 模板让每次 PR 都自动跑一轮真实数据库集成测试。如果你用的是 ORM 或数据库迁移工具比如 Flyway、Liquibase、Prisma、TypeORM建议在 CI 中直接执行项目自己的迁移命令而不是手动复制 SQL 执行。例如前一个示例中如果项目用 TypeORM可以直接在 step 中运行npm run typeorm:migration:run这样验证的是开发者真实使用的迁移路径而不是“CI 里另写的那份 SQL”。配置数据库连接时把连接串统一收敛到环境变量。你的应用代码不应该知道数据库是临时容器还是外部库它只需要读取DATABASE_URL或类似配置。这样本地开发、CI、预发、生产四个环境之间切换时改动都在配置层代码层没有任何差异。9. 总结与后续学习方向GitHub Actions 与数据库的结合本质上是把“数据库环境”从隐式依赖变成显式配置。你在 workflow 中明确声明数据库版本、初始化参数、端口和健康检查然后让测试代码在干净环境中运行。这种模式看起来只是 CI 配置的细节实际影响的是整个团队的发布节奏和代码质量。本文中我们从概念开始了解了 Service Container 的工作方式然后通过 MySQL、PostgreSQL 两个完整示例跑通了“启动数据库、执行迁移、验证数据”这条链路最后整理了常见的连接失败原因和安全实践。按这个思路你可以把任意一个依赖数据库的项目接入 GitHub Actions并让它稳定运行。如果你是从零开始建议先在自己的项目中加入一个最小的 MySQL 或 PostgreSQL workflow只跑一条 SQL 查询确认服务能正常起停。再逐步加入迁移、种子数据、ORM 映射、事务测试。每次扩展都保持“跑得通、看得见输出、失败了能查日志”这三个标准。后续值得深入学习的方向包括自托管 runner 上的数据库镜像缓存、不同数据库镜像的健康检查参数差异、数据库迁移工具在 CI 中的标准接入方式以及如何用混沌测试验证数据库高可用逻辑。当然这些都是在基础链路稳定之后再考虑的事。建议先收藏本文动手跑通一个示例再回头对照自己的项目需求做调整。