Deployment
CI/CD pipelines

Deploy Django With a Jenkins Pipeline

Build a Jenkins pipeline for Django: a declarative Jenkinsfile that runs tests against PostgreSQL in Docker, builds and pushes an image, deploys over SSH with migrations, uses credentials binding, and gates production behind approval.

Problem statement

Many companies run Jenkins on their own infrastructure, and the Django project must use it rather than a hosted CI service. Older Jenkins setups rely on freestyle jobs configured in the UI, which are hard to review and reproduce. A declarative Jenkinsfile in the repository makes the pipeline versioned code: tests against a real PostgreSQL, an image build, a deploy with migrations, and an approval gate before production.

Django pipeline in JenkinsCheckoutJenkinsfile in repoTestPostgres in DockerBuild + pushtagged imageDeploymigrate, then switchA Jenkinsfile keeps the pipeline reviewed alongside the code it deploys.
  1. Add a Jenkinsfile with stages: Test, Build, Deploy Staging, Approve, Deploy Production.
  2. Run tests in a Python container with a PostgreSQL sidecar using the Docker Pipeline plugin.
  3. Store registry passwords, SSH keys, and secrets in Jenkins Credentials, bound with withCredentials or sshagent.
  4. Build the image once, tag it with the commit SHA, and deploy that same tag to staging and production.
  5. Deploy: pull the image on the server, run migrate, restart, and run a smoke test.
  6. Gate production with an input step limited to specific users.

Step-by-step solution

1. Pipeline skeleton

// Jenkinsfile
pipeline {
  agent any
  options { timestamps(); disableConcurrentBuilds() }
  environment {
    IMAGE = "registry.example.com/myapp"
    TAG = "${env.GIT_COMMIT.take(12)}"
  }
  stages {
    stage('Test')    { steps { script { runTests() } } }
    stage('Build')   { steps { script { buildAndPush() } } }
    stage('Deploy staging') { when { branch 'main' } steps { deploy('staging.example.com') } }
    stage('Approve') { when { branch 'main' } steps { input message: 'Deploy to production?', submitter: 'release-managers' } }
    stage('Deploy production') { when { branch 'main' } steps { deploy('prod.example.com') } }
  }
  post { failure { echo 'Notify the team here (Slack, email)' } }
}

disableConcurrentBuilds() prevents two deploys racing.

2. Tests with PostgreSQL in Docker

def runTests() {
  docker.image('postgres:16').withRun('-e POSTGRES_USER=app -e POSTGRES_PASSWORD=app -e POSTGRES_DB=app') { db ->
    docker.image('postgres:16').inside("--link ${db.id}:db") {
      sh 'until pg_isready -h db -U app; do sleep 1; done'
    }
    docker.image('python:3.12-slim').inside("--link ${db.id}:db -e DATABASE_URL=postgres://app:app@db:5432/app -e DJANGO_SETTINGS_MODULE=config.settings.test -e SECRET_KEY=test") {
      sh 'pip install -r requirements.txt'
      sh 'python manage.py makemigrations --check --dry-run'
      sh 'python manage.py test --parallel'
    }
  }
}

The wait loop avoids the database startup race. makemigrations --check fails the build if model changes lack migrations. For JUnit reports in the Jenkins UI, run tests with pytest --junitxml=report.xml and add junit 'report.xml' in a post block.

one commit through the pipelineTestPostgres containerBuild imagetag = commit SHADeploy stagingmigrate + smoke testApproveinput step, named usersDeploy productionsame image tagThe image tested and staged is exactly the one production runsdjango-deployment.com
Never rebuild for production; promote the image that passed staging.

3. Build and push with credentials

def buildAndPush() {
  docker.withRegistry('https://registry.example.com', 'registry-creds') {
    def img = docker.build("${IMAGE}:${TAG}")
    img.push()
  }
}

registry-creds is a Username/Password credential in Jenkins. Credentials are masked in logs.

4. Deploy over SSH

def deploy(String host) {
  sshagent(credentials: ['deploy-ssh-key']) {
    sh """
      ssh -o StrictHostKeyChecking=yes deploy@${host} '
        set -e
        cd /srv/app
        docker pull ${IMAGE}:${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
      '
    """
    sh "curl -fsS https://${host}/health/"
  }
}

Add the servers' host keys to the Jenkins agent's known_hosts once (verified), rather than disabling host key checking. For non-Docker deploys, the same SSH step can run your deploy script that pulls code, installs dependencies, migrates, and reloads Gunicorn.

5. Production approval

The input step pauses the pipeline until someone in release-managers approves. Combine with a timeout so abandoned builds do not hold executors:

stage('Approve') {
  steps { timeout(time: 2, unit: 'HOURS') { input message: 'Deploy to production?', submitter: 'release-managers' } }
}

Pausing outside any agent block (use agent none at the top level with per-stage agents) frees executors while waiting.

Jenkins pipeline hygieneJenkinsfile in the repositoryno UI-only job configTests against real PostgreSQLwait for readiness firstCredentials plugin for secretsmasked, never echoedOne image promoted across envstag = commit SHAApproval with timeout for prodnamed submittersPipeline as code makes Jenkins changes reviewable like application changesdjango-deployment.com
Keep Jenkins and its plugins updated; they are part of your attack surface.

Verification check: a pull request build runs only the Test stage; a merge to main builds, deploys to staging, waits for approval, and deploys the same tag to production; the smoke test step fails the build if /health/ does not return 200.

Explanation

Declarative pipelines describe stages, conditions, and post actions in a structured syntax that Jenkins validates, while script blocks allow Groovy where needed. The Docker Pipeline plugin runs steps inside containers, giving each build a clean Python environment and disposable services such as PostgreSQL. Treating the image as the unit of deployment, built once and promoted, means production runs exactly what was tested and staged, and makes rollback a matter of redeploying an earlier tag.

Edge cases / notes

  • Agents. Run builds on agents, not the controller; Docker on the agent is required for the container steps.
  • Multibranch pipelines discover branches and pull requests automatically and apply the when conditions.
  • Shared libraries let several Django projects reuse runTests and deploy functions.
  • Secrets in logs. Avoid set -x in shell steps that use credentials.

Next steps

FAQ

Can Jenkins run Django tests against PostgreSQL?

Yes. Use the Docker Pipeline plugin to start a PostgreSQL container and run tests in a Python container linked to it.

Where should deploy secrets live in Jenkins?

In Jenkins Credentials, bound in steps with withCredentials or sshagent.

How do I require approval before production?

Use an input step with a submitter list and a timeout.

Should production use a fresh build?

No. Deploy the same image tag that passed tests and staging.

Keep going