Complete Guide to Setting Up CI/CD for Django with Jenkins, Ansible, Docker, and GitHub Webhooks


Continuous Integration and Continuous Deployment (CI/CD) are key practices for modern development teams. In this guide, we'll walk through the process of building a CI/CD pipeline for a Django web application using Jenkins for automation and Ansible for deployment. We'll also use Docker to containerize the Django and Nginx services.
This tutorial is ideal for developers and DevOps enthusiasts who want to automate building, testing, pushing, and deploying Dockerized Django applications.
Prerequisites
A Linux-based machine with Docker installed
Jenkins installed on your system (not in a container)
GitHub repository containing your Django project
Basic understanding of Docker, Django, and Linux shell
Ansible installed on your local machine
Step 1: Install Jenkins on Your Machine
To install Jenkins, follow these steps:
On Ubuntu:
sudo apt update
sudo apt install openjdk-17-jdk -y
wget -q -O - https://pkg.jenkins.io/debian-stable/jenkins.io.key | sudo tee /usr/share/keyrings/jenkins-keyring.asc
echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/ | sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt update
sudo apt install jenkins -y
Start Jenkins:
sudo systemctl start jenkins
sudo systemctl enable jenkins
Now go to http://localhost:8080
in your browser and follow the setup instructions. You’ll find the initial admin password at:
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
Once you install the suggested plugins and create an admin user, you're ready to create a pipeline.
Step 2: Install and Configure Ansible
Install Ansible on your local machine:
sudo apt update
sudo apt install ansible -y
Verify installation:
ansible --version
Step 3: Create Dockerfiles
Django Dockerfile (at the root of your project)
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
WORKDIR /app
RUN apt-get update \
&& apt-get install -y libpq-dev gcc \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["gunicorn", "project.wsgi:application", "--bind", "0.0.0.0:8000"]
Nginx Dockerfile (inside nginx/
folder)
FROM nginx:alpine
COPY default.conf /etc/nginx/conf.d/default.conf
default.conf (inside nginx/
folder)
server {
listen 80;
location / {
proxy_pass http://djangoapp:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Step 4: Write Your Jenkinsfile
Place this Jenkinsfile
in the root of your GitHub repository:
pipeline {
agent any
environment {
IMAGE = 'biseshadk/djangoappjenkins'
TAG = "${BUILD_NUMBER}"
NGINX_IMAGE = 'biseshadk/nginx'
NGINX_TAG = "${BUILD_NUMBER}"
DJANGO_CONTAINER_NAME = 'django-test-container'
DJANGO_TEST_PORT = '8001'
}
stages {
stage('Clone') {
steps {
git branch: 'main', url: 'https://github.com/Biseshadhikari/ims2'
}
}
stage('Build') {
steps {
sh "docker build -t $IMAGE:$TAG ."
sh "docker build -t $NGINX_IMAGE:$TAG ./nginx"
}
}
stage('Test') {
steps {
script {
sh '''
docker rm -f $DJANGO_CONTAINER_NAME || true
docker run -d --name $DJANGO_CONTAINER_NAME -p $DJANGO_TEST_PORT:8000 $IMAGE:$TAG
sleep 10
if curl -s -L --fail http://localhost:$DJANGO_TEST_PORT/; then
echo 'Test passed'
else
echo 'Test failed'
docker logs $DJANGO_CONTAINER_NAME
docker rm -f $DJANGO_CONTAINER_NAME
exit 1
fi
'''
}
}
}
stage('Push') {
steps {
withCredentials([usernamePassword(credentialsId: 'dockerhub-creds', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
sh '''
echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin
docker push $IMAGE:$TAG
docker push $NGINX_IMAGE:$TAG
'''
}
}
}
stage('Deploy') {
steps {
sh "ansible-playbook ansible-deploy/deploy.yaml --extra-vars \"tag=$TAG\""
}
}
}
post {
always {
sh "docker rm -f $DJANGO_CONTAINER_NAME || true"
}
}
}
Explanation:
agent any
Tells Jenkins to run this pipeline on any available agent (node).
environment
Defines global environment variables used throughout the pipeline.
IMAGE
: Docker image name for Django appTAG
: Uses Jenkins build number to uniquely tag each imageNGINX_IMAGE
: Docker image for NginxDJANGO_CONTAINER_NAME
andDJANGO_TEST_PORT
: Used during test stage
Stage: Clone
stage('Clone') {
steps {
git branch: 'main', url: 'https://github.com/Biseshadhikari/ims2'
}
}
This clones the main
branch from your GitHub repository.
Stage: Build
sh "docker build -t $IMAGE:$TAG ."
sh "docker build -t $NGINX_IMAGE:$TAG ./nginx"
Builds the Django Docker image using the Dockerfile at the root.
Builds the Nginx Docker image using the Dockerfile inside the
nginx/
directory.Tags them with the Jenkins build number.
Stage: Test
docker run -d --name $DJANGO_CONTAINER_NAME -p $DJANGO_TEST_PORT:8000 $IMAGE:$TAG
Runs a temporary container of the new Django image.
Waits 10 seconds for it to start.
Uses
curl
to test if the home page responds.If it fails, prints logs and exits the build with an error.
Stage: Push
withCredentials([...]) {
docker login ...
docker push ...
}
Logs into DockerHub using Jenkins-stored credentials (
dockerhub-creds
)Pushes both Django and Nginx images with the current tag
Stage: Deploy
sh "ansible-playbook ansible-deploy/deploy.yaml --extra-vars \"tag=$TAG\""
- Triggers Ansible to deploy the application using the tag of the just-pushed Docker image.
Post Step: Clean Up
post {
always {
sh "docker rm -f $DJANGO_CONTAINER_NAME || true"
}
}
Cleans up the test container, whether the build succeeds or fails.
Step 5: Create Ansible Playbook
Create a directory named ansible-deploy/
and add deploy.yaml
inside it:
- name: Deploy Django and Nginx containers
hosts: localhost
connection: local
tasks:
- name: Pull latest Django image
shell: docker pull biseshadk/djangoappjenkins:{{ tag }}
args:
executable: /bin/bash
- name: Pull latest Nginx image
shell: docker pull biseshadk/nginx:{{ tag }}
args:
executable: /bin/bash
- name: Stop and remove existing Django container
shell: docker rm -f djangoapp || true
args:
executable: /bin/bash
- name: Stop and remove existing Nginx container
shell: docker rm -f nginx || true
args:
executable: /bin/bash
- name: Run Django container
shell: |
docker run -d --name djangoapp \
-p 8000:8000 \
biseshadk/djangoappjenkins:{{ tag }}
args:
executable: /bin/bash
- name: Run Nginx container
shell: |
docker run -d --name nginx \
--link djangoapp \
-p 80:80 \
biseshadk/nginx:{{ tag }}
args:
executable: /bin/bash
1. Play Definition:
- name: Deploy Django and Nginx containers
hosts: localhost
connection: local
name: This is the description of the playbook. It indicates that the playbook will deploy Django and Nginx containers.
hosts: This specifies the target for the playbook. In this case, it is
localhost
, meaning the playbook will run locally.connection: The connection type is set to
local
, indicating that Ansible will directly interact with the local machine (where this playbook is executed).
2. Pull Latest Django Image:
- name: Pull latest Django image
shell: docker pull biseshadk/djangoappjenkins:{{ tag }}
args:
executable: /bin/bash
name: This task pulls the latest Docker image for the Django application.
shell: The
docker pull
command is used to fetch the specified Docker image from Docker Hub. The image is tagged with{{ tag }}
, which means the actual tag value will be passed dynamically at runtime (for example, the Jenkins build number or version).args:
executable: /bin/bash
ensures the shell used for running the command is Bash.
3. Pull Latest Nginx Image:
- name: Pull latest Nginx image
shell: docker pull biseshadk/nginx:{{ tag }}
args:
executable: /bin/bash
name: This task pulls the latest Docker image for Nginx.
shell: Similar to the previous task, it uses the
docker pull
command to fetch the Nginx image from Docker Hub.args: Again, it specifies using
/bin/bash
for the shell.
4. Stop and Remove Existing Django Container:
- name: Stop and remove existing Django container
shell: docker rm -f djangoapp || true
args:
executable: /bin/bash
name: This task stops and removes any existing Django container named
djangoapp
.shell: The
docker rm -f djangoapp
command forcefully removes the container nameddjangoapp
. If the container doesn't exist, the|| true
ensures that the task doesn't fail, as it will return a true value in case of an error.args: Uses
/bin/bash
for running the command.
5. Stop and Remove Existing Nginx Container:
- name: Stop and remove existing Nginx container
shell: docker rm -f nginx || true
args:
executable: /bin/bash
name: This task stops and removes any existing Nginx container named
nginx
.shell: The
docker rm -f nginx
command forcefully removes the container namednginx
. Again,|| true
ensures that it won't fail if the container doesn't exist.args: Uses
/bin/bash
for running the command.
6. Run Django Container:
- name: Run Django container
shell: |
docker run -d --name djangoapp \
-p 8000:8000 \
biseshadk/djangoappjenkins:{{ tag }}
args:
executable: /bin/bash
name: This task starts a new Django container.
shell: The
docker run
command starts a container in detached mode (-d
), names itdjangoapp
, and exposes port8000
on the host to port8000
inside the container. The imagebiseshadk/djangoappjenkins:{{ tag }}
(with the dynamic tag) is used to create the container.args: Specifies the use of
/bin/bash
for executing the command.
7. Run Nginx Container:
- name: Run Nginx container
shell: |
docker run -d --name nginx \
--link djangoapp \
-p 80:80 \
biseshadk/nginx:{{ tag }}
args:
executable: /bin/bash
name: This task starts a new Nginx container.
shell: The
docker run
command starts a new container in detached mode (-d
), names itnginx
, and links it to the previously starteddjangoapp
container using--link djangoapp
. This allows Nginx to communicate with the Django container. Port80
on the host is mapped to port80
inside the container. The Nginx imagebiseshadk/nginx:{{ tag }}
is used to create the container.args: As with previous tasks,
/bin/bash
is used to execute the command.
Summary of the Playbook Flow:
Pull Docker Images: First, the playbook pulls the latest Docker images for Django and Nginx using the
docker pull
command.Remove Existing Containers: Next, it ensures that any existing containers for Django and Nginx are stopped and removed using
docker rm -f
.Run Containers: Finally, it runs new containers for Django and Nginx, making sure the Django app runs on port
8000
and Nginx on port80
.
This Ansible playbook automates the process of setting up and updating the Django and Nginx containers on your local machine. It ensures that the latest versions of both containers are used, and old containers are properly removed before the new ones are deployed.
Step 6: Create a Jenkins Pipeline using "Pipeline Script from SCM"
Open Jenkins (http://135.207.158.5:8080/)
Click New Item
Enter the name:
django-jenkins
Select Pipeline, click OK
Under Pipeline, choose:
Definition: Pipeline script from SCM
SCM: Git
Repository URL: Your GitHub project URL
Branch:
*/main
Script Path:
Jenkinsfile
- Click Save
Now, Jenkins will fetch your Jenkinsfile
from GitHub and run your defined pipeline when you click Build Now.
Step 7: Configure DockerHub Credentials in Jenkins
Go to Manage Jenkins > Credentials
Under your Jenkins domain (e.g.,
(global)
), click Add CredentialsChoose Username with password
ID:
dockerhub-creds
Username: Your DockerHub username
Password: Your DockerHub password or access token
Click OK
This will allow Jenkins to authenticate and push images to your DockerHub account.
Step 8: Configure Jenkins to Use GitHub Webhooks as a Trigger
After you've created your Jenkins pipeline job, the next step is to set up Jenkins to listen for changes from GitHub automatically, using the GitHub Webhook.
Navigate to Your Jenkins Job Configuration:
In the Jenkins dashboard, find your job (e.g.,
django-jenkins
) and click on it.Select Configure from the left-hand side.
Enable the GitHub Webhook Trigger:
Scroll down to the Build Triggers section in the job configuration.
Check the option labeled GitHub hook trigger for GITScm polling. This will instruct Jenkins to listen for incoming GitHub webhook notifications instead of polling the repository at regular intervals.
Save the Jenkins Configuration:
- After enabling the GitHub trigger, make sure to click Save at the bottom of the page to apply the changes.
Now Jenkins is ready to trigger a build automatically whenever a change happens in your GitHub repository. Next, we need to configure GitHub to notify Jenkins when such changes occur.
Step 9: Set Up the GitHub Webhook
Once Jenkins is configured to listen for GitHub webhooks, you need to tell GitHub to send notifications to Jenkins whenever there is a push to your repository.
Steps to Set Up a Webhook in GitHub:
Go to Your GitHub Repository:
- Open your GitHub repository where your code is hosted (e.g.,
ims2
).
- Open your GitHub repository where your code is hosted (e.g.,
Navigate to Settings:
- On the right-hand side of the repository page, click on the Settings tab.
Add a Webhook:
- Scroll down to the Webhooks section in the left-hand menu and click on Add webhook.
Fill in the Webhook Details:
Payload URL:
- Enter your Jenkins server’s URL, followed by
/github-webhook/
at the end. This is the endpoint that GitHub will call to notify Jenkins.
- Enter your Jenkins server’s URL, followed by
Example:
http://<your-jenkins-url>:8080/github-webhook/
Content type:
- Set this to
application/json
, as this is the format GitHub will send the payload in.
- Set this to
Secret: (Optional)
- You can leave this blank or set a secret key for security purposes, but for now, let’s leave it empty for simplicity.
Which events would you like to trigger this webhook?
- Choose Just the push event. This ensures that Jenkins will only trigger a build when code is pushed to the repository.
Test the Webhook:
Before finalizing, you can click on the Test the webhook button to send a test payload to Jenkins and see if everything works correctly.
If your Jenkins instance is properly configured, you should see a green success message.
Click Add Webhook:
- After filling out the form and testing the webhook, click on the Add webhook button.
Once the webhook is set up, every time a push event occurs in your GitHub repository, GitHub will notify Jenkins via this webhook, which will trigger the Jenkins pipeline.
Step 6: Test the Webhook Integration
Now that you've configured both Jenkins and GitHub, it’s time to test everything:
Push Some Code:
Make a small change to your code (e.g., modify a README file or add a comment in one of your files).
Commit and push the changes to GitHub.
Check Jenkins:
Go to your Jenkins dashboard and check if the build starts automatically after pushing the code.
You should see the new build number appear in your Jenkins job, indicating that the webhook successfully triggered the Jenkins pipeline.
Verify the Build:
- Watch the console output of your Jenkins job to ensure everything is running as expected.
If the webhook is set up correctly, Jenkins will run the pipeline every time you push code, making your CI/CD pipeline fully automated.
This tutorial walks you through the basic steps of setting up a CI/CD pipeline for your Django application using Jenkins, Ansible, Docker, and GitHub webhooks. While these steps provide a solid foundation to get started with automated deployments, there are many other optimized and secure ways to refine and enhance this setup. As your application grows, you may need to incorporate additional tools like monitoring, automated testing, security best practices, and more sophisticated infrastructure management to ensure a smooth, scalable, and secure deployment process. However, the concepts and methods discussed here will serve as a strong base for building a robust continuous integration and deployment workflow.
Subscribe to my newsletter
Read articles from Bisesh Adhikari directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Bisesh Adhikari
Bisesh Adhikari
Student | Tech enthusiast | Aspiring software developer