Automating Django Deployments with github actions and docker
1004
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/[email protected]
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.