Skip to content
Latchkey

CI/CD for a REST API with Postgres using GitHub Actions

A REST API backed by Postgres should run its integration tests against a real database, not a mock.

This recipe is a framework-agnostic pipeline for a REST API with Postgres. It applies migrations, runs integration tests against a service container, and deploys on main. Swap the language steps for your stack.

What the pipeline does

  • Install deps
  • Apply migrations
  • Run integration tests against Postgres
  • Build
  • Deploy on main

The workflow

The core pattern is a Postgres service with a health check and a DATABASE_URL pointed at it. This example uses Node, but the service block is identical for any language.

.github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: api_test
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    env:
      DATABASE_URL: postgres://postgres:postgres@localhost:5432/api_test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: npm
      - run: npm ci
      - run: npm run migrate
      - run: npm test
  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: npm
      - run: npm ci --omit=dev
      - run: npm run deploy

Testing against a database

The postgres service runs as a sidecar and is reachable at localhost:5432 from the job. The health check (pg_isready) gates the steps until the database accepts connections, so migrations and tests never race a not-yet-ready server. Run migrations first, then point your test suite at DATABASE_URL. Wrap each test in a transaction and roll back, or truncate between tests, for isolation.

Deploying

The deploy job is gated on main. Run production migrations against your real database as part of the deploy (a dedicated migrate step before swapping traffic), then roll out the new build. Managed runners that auto-retry transient registry and network flakes keep the pipeline green without manual re-runs, and Latchkey runs it at about 69% lower cost.

Key takeaways

  • A health check on the Postgres service prevents tests racing a cold database.
  • Apply migrations before tests, and again against prod during deploy.
  • The service block is language-agnostic - only the steps change.

Related guides

Run this faster and cheaper on Latchkey managed runners. Start free →