Deployment
CI/CD pipelines

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.

Django on CircleCItest jobPython + Postgresbuild jobimage tagged by SHAholdapproval for proddeploy jobmigrate, restartWorkflows chain jobs so deploys only run after green tests on the main branch.
  1. Create .circleci/config.yml with a test job using cimg/python plus cimg/postgres as a service container.
  2. Cache pip downloads keyed on the requirements file checksum.
  3. Split tests across parallelism: 4 containers with circleci tests split --split-by=timings.
  4. Add build and deploy jobs filtered to main, with an approval job before production.
  5. Store secrets in a CircleCI context; add SSH keys in project settings for SSH deploys.
  6. 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}
feature to benefitUse forBenefitService containersPostgres, Redis in testsreal database behaviourDependency cachepip download cachefaster installsTest splittingparallelism + timingsโœ“shorter test runsContextsshared secrets per envrestricted to groupsApproval jobshold before productionโœ“human gateFast feedback on branches, controlled releases on maindjango-deployment.com
Restrict production contexts to a security group so only approved users can deploy.

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.

the ci-cd workflow1test (parallel x4)every branch and PR2buildmain only, push image3deploy-stagingmigrate, smoke test4hold (approval)click to continue5deploy-productionsame image tagrequires and filters encode the release rules in configurationdjango-deployment.com
The approval click is recorded with the user, which helps audits.

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

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.

Keep going