Deploy Django With CircleCI
Set up CircleCI for a Django project: a config.yml with a PostgreSQL service image, dependency caching, parallel test splitting, Docker image builds, and an SSH or registry-based deploy job gated by branch filters and approval.
Problem statement
CircleCI runs builds in containers with first-class support for service databases, caching, test splitting across parallel containers, and workflows with approval steps. A good Django configuration uses those features to keep feedback fast (cached dependencies, parallel tests) and deployments controlled (deploy only from main, after tests, optionally after a human approves). This guide builds that .circleci/config.yml.
- Create
.circleci/config.ymlwith atestjob usingcimg/pythonpluscimg/postgresas a service container. - Cache pip downloads keyed on the requirements file checksum.
- Split tests across
parallelism: 4containers withcircleci tests split --split-by=timings. - Add
buildanddeployjobs filtered tomain, with anapprovaljob before production. - Store secrets in a CircleCI context; add SSH keys in project settings for SSH deploys.
- Run migrations as part of the deploy job and finish with a smoke test.
Step-by-step solution
1. Test job with PostgreSQL
version: 2.1
jobs:
test:
docker:
- image: cimg/python:3.12
environment:
DATABASE_URL: postgres://app:app@localhost:5432/app
DJANGO_SETTINGS_MODULE: config.settings.test
SECRET_KEY: test-only
- image: cimg/postgres:16.4
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
parallelism: 4
steps:
- checkout
- restore_cache:
keys: [pip-v1-{{ checksum "requirements.txt" }}]
- run: pip install -r requirements.txt
- save_cache:
key: pip-v1-{{ checksum "requirements.txt" }}
paths: [~/.cache/pip]
- run: dockerize -wait tcp://localhost:5432 -timeout 60s
- run: python manage.py makemigrations --check --dry-run
- run:
name: Run tests (split)
command: |
TESTS=$(circleci tests glob "**/tests/test_*.py" | circleci tests split --split-by=timings)
pytest --junitxml=test-results/junit.xml $TESTS
- store_test_results: { path: test-results }
Service containers share the network namespace with the primary container, so PostgreSQL is on localhost. dockerize (available in cimg images) waits for the port. Timing-based splitting balances containers after the first run that stores results.
2. Build the image
build:
docker: [{ image: cimg/base:current }]
steps:
- checkout
- setup_remote_docker: { docker_layer_caching: true }
- run: |
echo "$REGISTRY_PASSWORD" | docker login ghcr.io -u "$REGISTRY_USER" --password-stdin
docker build -t ghcr.io/you/myapp:${CIRCLE_SHA1:0:12} .
docker push ghcr.io/you/myapp:${CIRCLE_SHA1:0:12}
3. Deploy job
deploy:
parameters:
host: { type: string }
docker: [{ image: cimg/base:current }]
steps:
- add_ssh_keys:
fingerprints: ["SHA256:abc..."]
- run: echo "$KNOWN_HOSTS" >> ~/.ssh/known_hosts
- run: |
TAG=${CIRCLE_SHA1:0:12}
ssh deploy@<< parameters.host >> "cd /srv/app && \
docker pull ghcr.io/you/myapp:$TAG && \
IMAGE_TAG=$TAG docker compose run --rm web python manage.py migrate --noinput && \
IMAGE_TAG=$TAG docker compose up -d --no-deps web worker"
- run: curl -fsS https://<< parameters.host >>/health/
4. Workflow with approval
workflows:
ci-cd:
jobs:
- test
- build:
requires: [test]
filters: { branches: { only: main } }
context: registry
- deploy:
name: deploy-staging
host: staging.example.com
requires: [build]
context: staging
- hold:
type: approval
requires: [deploy-staging]
- deploy:
name: deploy-production
host: example.com
requires: [hold]
context: production
Pull requests run only test. Merges to main build, deploy to staging, wait for approval, and deploy production with the same image tag.
Verification check: open a pull request and confirm only test runs with four containers; merge it and confirm the workflow pauses at hold after staging; approve and confirm production serves the new commit (expose the SHA on a version endpoint).
Explanation
CircleCI models CI as jobs running in executors (Docker images or VMs) and workflows that order jobs with requires and filters. Secondary images in a Docker executor run as services alongside the primary container, which is how Django tests get a real PostgreSQL. Contexts hold environment-specific secrets and can be restricted to security groups, so only approved people can trigger jobs that use production credentials. The approval job is a native pause that turns the workflow into a release process.
Edge cases / notes
- Docker layer caching is a paid feature on some plans; without it, use BuildKit cache exports to the registry.
- Test splitting needs JUnit results stored so timings are known; the first run splits by name.
- Concurrency. Two merges in quick succession can deploy out of order; serialise deploy jobs with a queueing orb or deploy markers.
- Self-hosted runners let deploy jobs run inside your private network without opening SSH to the internet.
Next steps
- Compare with Deploy Django With GitHub Actions CI/CD.
- Verify releases with the Django Post-Deploy Verification Checklist.
FAQ
How do I run PostgreSQL in CircleCI for Django tests?
Add a cimg/postgres image as a secondary container in the job; it is reachable on localhost.
How do I speed up Django tests on CircleCI?
Cache pip downloads and split tests across parallel containers by timing.
How do I require approval before deploying?
Add a job with type: approval between staging and production in the workflow.
Where should deploy secrets live?
In CircleCI contexts, restricted to the people allowed to deploy.