Skip to main content

pytest-testcontainers-django

CI PyPI Python versions License: MIT

Bridge between pytest-testcontainers and pytest-django: starts a Postgres (and optionally Redis) container before Django imports its settings, so your tests run against a real, ephemeral DB without any docker-compose orchestration — and without "Connection refused" against port 5432 because Django read os.environ too early.

Why this package exists

Django evaluates DATABASES at module-import time. pytest-django imports settings during its pytest_load_initial_conftests hook. Any fixture-based testcontainer setup runs after that — so by the time the container has a port, Django has already opened a connection (or failed to) against whatever your .env had at pytest startup.

The only correct hook for "start a container, write its port to os.environ, before Django imports settings" is pytest_load_initial_conftests itself, registered with @pytest.hookimpl(tryfirst=True). That single detail is the core IP of this package; the rest is plumbing — xdist worker propagation, dotenv suppression, init-script mounting, TEST TEMPLATE wiring, cleanup ordering.

See SPEC.md for the full design rationale (especially §6 on the timing dance).

Features

  • pytest_load_initial_conftests(tryfirst=True) hook that runs before pytest-django imports your settings.py.
  • Zero-config defaults — works out of the box for projects whose settings.py reads DJANGO_DB_HOST / DJANGO_DB_PORT / etc. from os.environ.
  • Declarative configuration in [tool.pytest-testcontainers-django] or programmatic configuration via register(DjangoContainerConfig(...)) from conftest.py.
  • Postgres init-script mounting (/docker-entrypoint-initdb.d/) with automatic DATABASES['TEST']['TEMPLATE'] defaulting — so pytest --create-db finishes in seconds.
  • Optional Redis container with the same timing-safe injection pattern.
  • pytest-xdist support — workers inherit the controller's environment, no port-fight.
  • --no-testcontainers / PYTEST_TESTCONTAINERS_DISABLE=1 to delegate to docker-compose; PYTEST_TESTCONTAINERS_REUSE=1 for fast local iteration.
  • atexit safety net for abrupt-exit paths that skip pytest_unconfigure.
  • Optional integration with django-pg-baseline for managed baseline SQL artifacts.

Supported versions

Python

Python 3.10 3.11 3.12 3.13

Django

Authoritative upstream: https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django

Django 3.10 3.11 3.12 3.13 Status
4.2 LTS EOL Apr 2026 (still works)
5.2 LTS Active LTS
6.0 Requires Python >=3.12
6.1 Tested in CI (Python 3.13)

Django 6.0 and 6.1 require Python 3.12 or newer, so they are unavailable on the 3.10/3.11 rows.

CI's test matrix is a pytest matrix (this is a pytest plugin, and Django is only a dev dependency), plus one dedicated job that pins Django~=6.1.0 on Python 3.13 — see the django61 tox env. EOL Django releases (4.2, 5.0, 5.1) are not actively tested but should still work — this package only consumes pytest-django's hook surface, and touches just a couple of long-stable Django internals. Open an issue if you need an LTS-only reassurance.

Install

Using uv (recommended)

uv add pytest-testcontainers-django

Using pip

pip install pytest-testcontainers-django

You also need a working Docker daemon on the host running pytest. No extra system libraries are required — the package is pure Python.

Quick start

For most projects, configuration lives in pyproject.toml. Zero conftest.py needed:

[tool.pytest-testcontainers-django]
postgres_image = "postgres:16"
postgres_user = "myapp"
postgres_password = "myapp"
postgres_database = "myapp"

# Env-var names this plugin writes into os.environ.
# These are the same names your settings.py reads.
db_host_env = "DJANGO_DB_HOST"
db_port_env = "DJANGO_DB_PORT"
db_name_env = "DJANGO_DB_NAME"
db_user_env = "DJANGO_DB_USER"
db_password_env = "DJANGO_DB_PASSWORD"
db_test_template_env = "DJANGO_DB_TEST_TEMPLATE"
skip_dotenv_env = "DJANGO_SKIP_DOTENV"

Your settings.py reads these env vars exactly as you'd expect:

import environ
import os

env = environ.Env()

# Skip .env loading when the plugin already populated os.environ —
# otherwise read_env(overwrite=True) would clobber our injected port.
if not os.environ.get("DJANGO_SKIP_DOTENV"):
    environ.Env.read_env(".env", overwrite=True)

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": env("DJANGO_DB_NAME"),
        "USER": env("DJANGO_DB_USER"),
        "PASSWORD": env("DJANGO_DB_PASSWORD"),
        "HOST": env("DJANGO_DB_HOST"),
        "PORT": env("DJANGO_DB_PORT"),
    },
}

# Wire the TEST TEMPLATE env var (optional but recommended when you
# load init scripts — see "Init scripts / baseline" below).
_test_template = env("DJANGO_DB_TEST_TEMPLATE", default="")
if _test_template:
    DATABASES["default"]["TEST"] = {"TEMPLATE": _test_template}

That's it — pytest will start a Postgres container, inject the port, let pytest-django import settings, and tear the container down at exit.

Init scripts / baseline

Mount SQL files into the Postgres container's /docker-entrypoint-initdb.d/ so they're replayed once on cluster init — significantly faster than running psql -f from the host:

[tool.pytest-testcontainers-django]
postgres_database = "myapp"
postgres_init_scripts = [
    "tests/fixtures/baseline.sql",
    "tests/fixtures/extensions.sql",
]
# postgres_template defaults to postgres_database when init_scripts is
# set, so the test DB will be created via fast in-server CREATE DATABASE
# test_<X> WITH TEMPLATE myapp instead of replaying migrations.

Combine with the DATABASES['default']['TEST']['TEMPLATE'] snippet above to make pytest --create-db finish in seconds.

Optional Redis

testcontainers's RedisContainer imports the redis Python client at module load, so install it alongside this package when you enable Redis:

uv add 'pytest-testcontainers-django[redis]'
# or
pip install 'pytest-testcontainers-django[redis]'
[tool.pytest-testcontainers-django]
redis_enabled = true
redis_image = "redis:7-alpine"
redis_host_env = "DJANGO_REDIS_HOST"
redis_port_env = "DJANGO_REDIS_PORT"

Your settings reads DJANGO_REDIS_HOST / DJANGO_REDIS_PORT and folds them into a redis://... URL however your stack prefers.

Programmatic configuration

For projects that need conditional configuration or want to wire in django-pg-baseline for baseline-managed seed data, register from conftest.py:

# conftest.py at the project root
from pathlib import Path

from pytest_testcontainers_django import (
    DjangoContainerConfig,
    PostgresService,
    RedisService,
    register,
)

register(
    DjangoContainerConfig(
        postgres=PostgresService(
            image="postgres:16",
            user="myapp",
            password="myapp",
            database="myapp",
            init_scripts=[Path("tests/fixtures/baseline.sql")],
            template="myapp",
        ),
        redis=RedisService(),
    )
)

register() overrides any pyproject.toml table. This works because the plugin force-imports the rootdir conftest.py from inside its tryfirst hook, so top-level register(...) calls run before configuration is read.

Disable / reuse

# Delegate to docker-compose / pre-existing services:
pytest --no-testcontainers
PYTEST_TESTCONTAINERS_DISABLE=1 pytest

# Keep containers alive between runs for fast local iteration:
PYTEST_TESTCONTAINERS_REUSE=1 pytest

Reuse names encode the git branch, so every branch and worktree parks its own Postgres + Redis pair — and reuse mode disables Ryuk, so nothing ever expires them. Sweep them up with pytest --testcontainers-clean (see pytest-testcontainers' README for the shared-host caveat).

Teardown

Containers are named <project>-tc-<service>-… whether or not reuse is on, so they are greppable in docker ps and reachable by pytest --testcontainers-clean.

Ephemeral (non-reuse) containers are removed by pytest_unconfigure, by a SIGTERM/SIGHUP handler, and by atexit — whichever fires first. The Ryuk reaper is shut down explicitly at the end of the session instead of being left to idle out. Both mechanisms live in pytest-testcontainers; see its README for the details and for what still falls through on SIGKILL.

pytest-xdist

Workers inherit the controller's environment on fork, so they don't start new containers — they only re-set the *_SKIP_DOTENV flag so django-environ doesn't re-read .env on settings re-import.

Coexistence with other testcontainers

Django projects that need additional services (Elasticsearch, MinIO, Kafka, etc.) declare plain pytest fixtures using pytest-testcontainers's maker functions directly — no special integration with this package needed:

# conftest.py
import pytest
from pytest_testcontainers import make_container

@pytest.fixture(scope="session")
def minio():
    with make_container("minio/minio:latest", ports={"9000/tcp": None}) as c:
        yield c

Late resolution is fine for non-DB services — their host:port is read at connection time, not import time. Only Django's DATABASES has the import-time-read race that this package solves.

Configuration reference

Pyproject key Default Purpose
postgres_image postgres:16 Image used for the DB container
postgres_user postgres POSTGRES_USER
postgres_password postgres POSTGRES_PASSWORD
postgres_database postgres POSTGRES_DB
postgres_internal_port 5432 Image's internal port
postgres_template (= postgres_database when init scripts set, else unset) Value injected as DATABASES['TEST']['TEMPLATE']
postgres_init_scripts [] Paths mounted into /docker-entrypoint-initdb.d/
postgres_env {} Image-specific env (e.g. tuning flags)
db_host_env DJANGO_DB_HOST Env var written with the resolved host
db_port_env DJANGO_DB_PORT Env var written with the resolved port
db_name_env DJANGO_DB_NAME Env var written with postgres_database
db_user_env DJANGO_DB_USER Env var written with postgres_user
db_password_env DJANGO_DB_PASSWORD Env var written with postgres_password
db_test_template_env DJANGO_DB_TEST_TEMPLATE Env var written with postgres_template
skip_dotenv_env DJANGO_SKIP_DOTENV Env var your settings checks before reading .env
disable_env PYTEST_TESTCONTAINERS_DISABLE Env var that disables the plugin
reuse_env PYTEST_TESTCONTAINERS_REUSE Env var that enables reuse mode
redis_enabled false Enable Redis
redis_image redis:7-alpine Image used for Redis
redis_internal_port 6379
redis_host_env DJANGO_REDIS_HOST
redis_port_env DJANGO_REDIS_PORT
use_django_pg_baseline false Auto-prepend django-pg-baseline's artifact

Troubleshooting

Docker daemon is not reachable but docker ps works

You're almost certainly on a non-default Docker context — OrbStack, colima, or a Docker Desktop install where /var/run/docker.sock is missing or a dangling symlink. docker.from_env() (used by docker-py and by testcontainers internally) honors DOCKER_HOST but, unlike the docker CLI, ignores the active docker context. The plugin now resolves the active context's endpoint for you and exports DOCKER_HOST before any container is started, so it talks to the same daemon the CLI does.

If it still fails, point it at the daemon explicitly — an explicit DOCKER_HOST always wins:

docker context inspect -f '{{.Endpoints.docker.Host}}'   # see the endpoint
export DOCKER_HOST=unix://$HOME/.orbstack/run/docker.sock # e.g. OrbStack
export DOCKER_HOST=unix://$HOME/.colima/default/docker.sock # e.g. colima

License

MIT — see LICENSE.

Release files for pytest-testcontainers-django 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pytest-testcontainers-django 0.3.0
File Size Uploaded
pytest_testcontainers_django-0.3.0.tar.gz 33.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pytest-testcontainers-django 0.3.0
File Interpreter ABI Platform
pytest_testcontainers_django-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 58.7 kB

Release files / pytest_testcontainers_django-0.3.0.tar.gz

Download URL pytest_testcontainers_django-0.3.0.tar.gz
Size 33.9 kB
Tags Source
SHA-256 checksum
How to use checksums
de519cdf2f43b833e56a60937d05f511e33fe8420cd24b0bf5aaf1e7779de6c9
BLAKE2b-256 checksum
How to use checksums
247bb0b1ae7c0660294dc48a031df9893201ba408bd44a1b1d8ed9af5bc76122
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.2

Release files / pytest_testcontainers_django-0.3.0-py3-none-any.whl

Download URL pytest_testcontainers_django-0.3.0-py3-none-any.whl
Size 24.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
09b1aeb3a61eabf182653b0546b92c95411d33450cdd26bf3f0474635f65ec8e
BLAKE2b-256 checksum
How to use checksums
0ce4209c45238fc2f396774cef0a12a11bd87d3dfef5836c00b3b0a5a3c38576
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.2

Release history Release notifications | RSS feed

0.3.2

2 release files

0.3.1

2 release files

This release

0.3.0 This release

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page