Omonbude Emmanuel
Backend Engineer
Fullstack Web Developer
Mobile Application Developer
  • Residence:
    Nigeria
  • Phone:
    +2349032841265
  • Email:
    budescode@gmail.com
HTML/CSS
JAVASCRIPT
PYTHON
NODE JS
DJANGO/DJANGO REST FRAMEWORK
EXPRESS JS
FLUTTER
DART
ANGULAR
TYPESCRIPT

Automating Django Deployments with github actions and docker

Omonbude Emmanuel | Feb. 5, 2024, 5:18 a.m.

Introduction

 

In today's fast-paced development landscape, ensuring the seamless integration and deployment of web applications is paramount.  By incorporating Continuous Integration (CI) and Continuous Deployment (CD) practices into your workflow, you can automate the testing and deployment processes, streamlining development and improving overall efficiency.
This guide will walk you through the steps to implement a CI/CD pipeline for a Dockerized Django application. From containerizing your Django project to setting up automated testing,  and deploying to production, we'll explore best practices to enhance your development workflow. 

Prerequisites

 

To successfully follow this guide, ensure that you have a VPS server with  Ubuntu (minimum of 2GB RAM) , docker and docker compose installed and with a basic firewall. You should have a github account as well.

NOTE!!
If you're interested in letsencrypt with certbot (SSL Certificate), you need to connect your domain name to your server and update the AAAA and A records as well. These are required for certbot to work.
You can go to https://dnschecker.org/ to check dns propergation.

LETS BEGIN!
 

STEP 1:


Create Dockerfile, docker-compose.yml, entrypoint.sh and .env file in your project folder
We start by creating Dockerfile, docker-compose.yml and entrypoint.sh file for our configurations.
Paste the code below in your Dockerfile

# Use an official Python runtime as a parent image
FROM python:3.8

# Set environment variables
ENV PYTHONUNBUFFERED 1
#This means that as soon as a message is generated (e.g., by a print statement), it's immediately visible, making it easier to see real-time output and diagnose issues more quickly, especially in environments like Docker where log messages may be crucial for debugging.
ENV DJANGO_SETTINGS_MODULE project.settings

# Create and set the working directory
RUN mkdir /code
WORKDIR /code

# Copy the current directory contents into the container at /code
COPY . /code/

# Install any needed packages specified in requirements.txt
RUN pip install -r requirements.txt

# Copy the entrypoint script and make it executable
COPY entrypoint.sh /code/entrypoint.sh
RUN chmod +x /code/entrypoint.sh
# Copy the entrypoint.sh script and set execute permissions

# Expose the port the application runs on
EXPOSE 8000


# Run Django development server
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

#In summary, EXPOSE is used within the Dockerfile to document which ports a container might use, 
#while port binding with -p or -P is used at runtime to actually open and map ports between the host and the container, making container services accessible from the host or external network.

 

Paste the code below in entrypoint.sh file
We want to migrate and collectstatic immediately our web service spins up.
 

#!/bin/sh

set -e

python3 manage.py migrate --no-input

python3 manage.py collectstatic --no-input

gunicorn -b 0.0.0.0:8000 project.wsgi --timeout 200 --workers=5

 

 

Next is to setup the services in our docker-compose.yml file
Paste the code below in docker-compose.yml file
 

version: '3'


services:
  # Django web application
  web:
    container_name: django-nginx-app
    build:
      context: .
      dockerfile: Dockerfile
    env_file:
      - .env
    depends_on:
      - db
    ports:
      - "8000:8000"
    volumes:
      - ./:/code
    command: sh ./entrypoint.sh

  # PostgreSQL database
  db:
    image: postgres:14.1-alpine
    container_name: my-postgres-db
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
          - "5432:5432"
    env_file:
      - .env

  nginx:
    build: ./nginx
    ports:
      - "80:80"
      - "443:443"
    restart: always
    volumes:
      - ./:/code
      - certbot:/etc/letsencrypt
      - certbot:/var/www/certbot  
    env_file:
      - .env    
    depends_on:
      - web

volumes:
  static:
  certbot:
  postgres_data:

 

 


We have web, db and nginx services.
Web service holds code for our web server, db is out database and in this case we're using postgres and finallu nginx services helps us to configure nginx with our web service.


We have three volumes,

1. static which makes our static files persistent
2. certbot which will be used to store our ssl certificate and make it persistent as well
3. postgres_data is used to store details from our postgres database in order to make it persistent
What I mean by persistent in this case is that our data is not wiped off or deleted when docker compose restarts, instead is still saved and not lost.


Create a .env file and save these details there

POSTGRES_DB=mydb
POSTGRES_USER=myuser
POSTGRES_PASSWORD=mypassword
DEBUG=0
SECRET_KEY=yoursecretkey
 

 

STEP 2: SETUP NGINX
 

Create folder called nginx and create two files, Dockerfile and nginx.conf
Paste the code below in the Dockerfile

FROM nginx:latest

COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Paste the code below in the nginx.conf file
 

upstream django {
    server web:8000;
}

server {
    listen 80;

    location / {
        proxy_pass http://django;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;        
     }
    location /static/ {
        alias /code/static/;
    }

    location /media/ {
        alias /code/media/;
      }
}

 

 

STEP 3: SETTING UP GITHUB ACTIONS

Create a folder called .github and in this folder, create another folder called workflows and finally, create a file called docker-image.yml.
Your structure should be like .github/workflows/docker-image.yml
In the docker-image.yml file, paste the code below
 

name: Docker Image CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v2

      - name: Generate .env file
        uses: SpicyPizza/create-envfile@v1.3
        with:
          # 3rd party variables.

          envkey_POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
          envkey_POSTGRES_USER: ${{ secrets.POSTGRES_USER }}      
          envkey_POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
          envkey_CERTBOT_EMAIL: ${{ secrets.CERTBOT_EMAIL }}
          envkey_CERTBOT_DOMAIN: ${{ secrets.CERTBOT_DOMAIN }}
          envkey_DEBUG: ${{ secrets.DEBUG }}
          envkey_SECRET_KEY: ${{ secrets.SECRET_KEY }}

          file_name: .env
          fail_on_empty: false

      - name: Build the Docker image
        run: | 
            docker compose build
            docker compose run --rm web python manage.py makemigrations     
            docker compose run --rm web python manage.py migrate 
            docker compose run --rm web python manage.py collectstatic --no-input

  test:
    runs-on: self-hosted
    needs:
      - build
    steps:
      - name: Run tests
        run: |
          docker compose run --rm web python manage.py test

  deploy:
    runs-on: self-hosted
    needs:
      - build
      - test
    steps:
      - name: Deploy
        run: |
          docker compose up -d --force-recreate --remove-orphans
          


 

Create a repository in github and push your code

 

STEP 4: SETTING UP ENV VARIABLE WITH GITHUB ACTIONS

In your github repo, go to settings and click on secrets and variables, under the drop down, click on Actions
 

 

Under Repository secret, add your secrets

 

Anything you want to be in your .env file should be added here

 

STEP 5: SETTING UP RUNNSER WITH GITHUB RUNNSERS ON OUR SERVER]

We have to connect github runners to our ubuntu server so that when there is a push on our main branch, deployment can be automated.

On the settings page, click on Actions on the sidebar and click on Runnners under the dropdown

Click on new self hosted runner.

SSH into your ubuntu server and follow the instructions on the runnser page.

NOTE!!!!

You might face an error when trying ./run.sh:


Must not run interactively with sudo
Exiting runner...

To fix this, you need to add RUNNER_ALLOW_RUNASROOT=true to your command

RUNNER_ALLOW_RUNASROOT=true ./config.sh...

Now we want it to take effect immediately,

Run 
source ~/.bashrc or 
source ~/.bash_profile or 
source ~/.bash_profile or 
depending on your linux system

NOTE2:
When you close your terminal, the session closes, if you want ./run.sh to keep listening, use the command below

RUNNER_ALLOW_RUNASROOT=true nohup ./run.sh &


 

STEP 6: PUSH TO GITHUB

 

 

Push your code to github and click on Actions tab and monitor your github actions

Monitor the workflow to make sure everything is successfully deployed.
Go to your web page and confirm it has been deployed.

 

STEP 7: SSL CERTIFICATE WITH CERTBOT


We want to install ssl certificate to make our website secured. We do it with certbot
Open your docker-compose.yml and add certbot service in file
 

  certbot:
    image: certbot/certbot
    volumes:
      - certbot:/etc/letsencrypt
      - certbot:/var/www/certbot      
    depends_on:
      - web
      - nginx
    env_file:
      - .env
    command: certonly --webroot -w /var/www/certbot --force-renewal --email $CERTBOT_EMAIL -d $CERTBOT_DOMAIN --agree-tos
 


Your code should be like this.
Push your code to github and make sure it deploys.

 

STEP 8: CONFIGURE NGINX TO HANDLE SSL CERTIFICATE


Add the below code to the end of your nginx/nginx.conf file
 

server {
    listen 443 ssl http2;
    # use the certificates
    ssl_certificate     /etc/letsencrypt/live/scorezone.xyz/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/scorezone.xyz/privkey.pem;
    server_name scorezone.xyz www.scorezone.xyz;


    location / {
        proxy_pass http://django;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location ~ /.well-known/acme-challenge/ {
        root /var/www/certbot; #path to webroot 
        
    }

    location /static/ {
        alias /code/static/;
    }

    location /media/ {
        alias /code/media/;
    }
}

 

My domain name here  is scorezone.xyz


STEP 9: PUSH TO GITHUB

 

Update your current code to github and it should be up. GO to https://yourdomain.com. In my case its https://scorezone.com

 

COMMON MISTAKE

 

Do not make the mistake of pushing the nginx certbot configuration and nginx configuration at the same time.
The server needs to be runnning first before certbot is added. So configure nginx first for the server and upload, then when it is running, add the ssl configuraiton block and upload push again .

 

server {listen 443 ssl http2;.....} 

 

CONCLUSION

In conclusion, automating Django deployments with GitHub Actions and Docker offers a streamlined and efficient workflow for managing and deploying Django applications. By leveraging the power of GitHub Actions, you can automate various tasks such as building Docker images, running tests, and deploying your application to production.

 

© 2023 Omonbude Emmanuel

Omonbude Emmanuel