Skip to content
compiler.dev

Django GitHub Actions Workflow Template

Last updated

This is a working .github/workflows/ci.yml for a Django project that tests against a real Postgres database. It starts Postgres as a service container, caches pip, checks for missing migrations and runs your tests.

The workflow

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

concurrency:
  group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: app_test
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 5s
          --health-timeout 5s
          --health-retries 10
    env:
      DATABASE_URL: postgres://postgres:postgres@localhost:5432/app_test
      DJANGO_SETTINGS_MODULE: config.settings
      SECRET_KEY: ci-only-not-a-secret
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip
          cache-dependency-path: requirements*.txt

      - run: pip install -r requirements.txt
      - run: python manage.py makemigrations --check --dry-run
      - run: python manage.py test --parallel

What the non-obvious parts do

  • services.postgres starts a Postgres container beside the job. Because the job runs directly on the runner, it reaches the database at localhost:5432, and ports publishes it.
  • The options health check makes GitHub wait until pg_isready succeeds before running steps. Without it, the first step that touches the database can fail at random, a common cause of flaky tests.
  • DATABASE_URL assumes you parse it with dj-database-url or similar. If your settings read DB_HOST, DB_NAME and so on, set those instead.
  • DJANGO_SETTINGS_MODULE and SECRET_KEY are placeholders. Change the module path to yours. A throwaway key is fine in CI; never reuse a production secret here.
  • makemigrations --check --dry-run fails the build if a model change is missing its migration file.
  • test --parallel runs test processes in parallel and clones the test database for each one. Postgres is fine with this. If your tests use pytest, run pytest -n auto instead (needs pytest-django and pytest-xdist).
  • cache: pip caches downloads, keyed on the requirements files. See caching pip.

Variants

Python version matrix

Add strategy.matrix.python: ["3.11", "3.12", "3.13"] and use ${{ matrix.python }}. Keep the quotes. See matrix builds.

Redis or Celery

Add a redis service the same way, with redis:7 and --health-cmd "redis-cli ping".

Static files and linting

Add ruff check . before the tests and python manage.py collectstatic --noinput if your tests render pages that need the manifest.

Deploy job

Add a second job with needs: test that runs only on main. Use OIDC for cloud credentials.

Run it on faster machines

Django suites that hit the database are CPU and disk bound. Change one line:

    runs-on: [self-hosted, Linux, compiler-ci]

The job runs on a fresh 8 vCPU machine from compiler.dev, with fallback to your original runner. The service container works the same way. Tuning tips are in speed up Django on GitHub Actions; see also pricing.

FAQ

Do I need a service container, or can I use SQLite?

SQLite is quicker but hides Postgres-specific behaviour. If production runs Postgres, test on Postgres.

Why does the test database connection fail intermittently?

Usually a missing health check. Keep the options block.

How do I split a slow Django test suite?

Use --parallel for cores and test sharding across jobs, with a separate database per shard.

Can I run this on Windows or macOS?

Service containers only work on Linux runners. Stay on Linux for database tests.