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

Bisesh AdhikariBisesh Adhikari
12 min read

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 app

  • TAG: Uses Jenkins build number to uniquely tag each image

  • NGINX_IMAGE: Docker image for Nginx

  • DJANGO_CONTAINER_NAME and DJANGO_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 named djangoapp. 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 named nginx. 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 it djangoapp, and exposes port 8000 on the host to port 8000 inside the container. The image biseshadk/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 it nginx, and links it to the previously started djangoapp container using --link djangoapp. This allows Nginx to communicate with the Django container. Port 80 on the host is mapped to port 80 inside the container. The Nginx image biseshadk/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:

  1. Pull Docker Images: First, the playbook pulls the latest Docker images for Django and Nginx using the docker pull command.

  2. Remove Existing Containers: Next, it ensures that any existing containers for Django and Nginx are stopped and removed using docker rm -f.

  3. Run Containers: Finally, it runs new containers for Django and Nginx, making sure the Django app runs on port 8000 and Nginx on port 80.

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"

  1. Open Jenkins (http://135.207.158.5:8080/)

  2. Click New Item

  3. Enter the name: django-jenkins

  4. Select Pipeline, click OK

  5. Under Pipeline, choose:

    • Definition: Pipeline script from SCM

    • SCM: Git

    • Repository URL: Your GitHub project URL

    • Branch: */main

    • Script Path: Jenkinsfile

  1. 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

  1. Go to Manage Jenkins > Credentials

  2. Under your Jenkins domain (e.g., (global)), click Add Credentials

  3. Choose Username with password

    • ID: dockerhub-creds

    • Username: Your DockerHub username

    • Password: Your DockerHub password or access token

  4. 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.

  1. 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.

  2. 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.

  1. 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:

  1. Go to Your GitHub Repository:

    • Open your GitHub repository where your code is hosted (e.g., ims2).
  2. Navigate to Settings:

    • On the right-hand side of the repository page, click on the Settings tab.
  3. Add a Webhook:

    • Scroll down to the Webhooks section in the left-hand menu and click on Add webhook.
  4. 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.

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.
  • 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.
  1. 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.

  2. 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:

  1. 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.

  2. 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.

  3. 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.

0
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