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.
- Add a
Jenkinsfilewith stages: Test, Build, Deploy Staging, Approve, Deploy Production. - Run tests in a Python container with a PostgreSQL sidecar using the Docker Pipeline plugin.
- Store registry passwords, SSH keys, and secrets in Jenkins Credentials, bound with
withCredentialsorsshagent. - Build the image once, tag it with the commit SHA, and deploy that same tag to staging and production.
- Deploy: pull the image on the server, run
migrate, restart, and run a smoke test. - Gate production with an
inputstep 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.
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.
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
whenconditions. - Shared libraries let several Django projects reuse
runTestsanddeployfunctions. - Secrets in logs. Avoid
set -xin shell steps that use credentials.
Next steps
- Hosted alternatives: Deploy Django With GitHub Actions CI/CD and Deploy Django With GitLab CI/CD.
- SSH failures in the deploy stage: Fix Permission Denied (publickey) in Django CI Deploys.
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.