DEV Community

DoriDoro
DoriDoro

Posted on • Edited on

Make a Django project production-ready, create a Docker Image and use GitHub CI/CD to automate the push of the Docker image

Introduction:

In this article, we will walk through a detailed explanation of

for building a containerized Django web application.

Dockerfile for a Django project (Django version 5.0), which is saved on GitHub and will be deployed on Render with help of a CI/CD pipeline of GitHub actions.

Making Your Django Project Production-Ready with WhiteNoise and Gunicorn

When it comes to deploying a Django application in a production environment, two important tools can significantly improve your setup: WhiteNoise and Gunicorn. Both play crucial roles in ensuring that your Django application runs efficiently and securely. This article will delve into what these tools are, their purposes, and why they are essential for a production-ready Django project.

WhiteNoise: Serving Static Files Efficiently

WhiteNoise is a Python package for serving static files directly from your web application. While Django has built-in static file handling capabilities, they are primarily geared toward development and are not suitable for production environments. Here’s why WhiteNoise is a valuable addition:

  1. Ease of Use: WhiteNoise is straightforward to set up and integrates seamlessly with Django. It allows you to serve static files without requiring an external web server like Nginx or Apache.

  2. Performance: WhiteNoise achieves efficient file serving by using a robust static file caching mechanism. This means it can distribute static files with the appropriate caching headers, ensuring that the files are stored in client-side caches, reducing load times and server strain.

  3. Security: WhiteNoise provides sensible security defaults. It handles Content Security Policy (CSP) headers and ensures that cache-busting works correctly, which is crucial for deploying updates reliably.

  4. Compression: It also comes with out-of-the-box support for Gzip and Brotli compression. These compression techniques reduce the size of the static files sent to clients, resulting in faster load times.

Why WhiteNoise?

  • Simplifies Deployment: You don’t need to set up and manage an additional server or CDN for serving static files.
  • Improves Performance: Efficient caching and compression mechanisms enhance the overall performance of your web application.
  • Enhances Security: Security features and sensible defaults help in serving static files securely.

Gunicorn: Efficient Application Server

Gunicorn, short for "Green Unicorn", is a Python HTTP server for WSGI applications. It is a pre-fork worker model which allows handling many requests in parallel. Here’s why Gunicorn is indispensable for running Django applications in production:

  1. Speed and Efficiency: Gunicorn is designed to be fast and lightweight, making it an excellent choice for performance-critical applications. It efficiently handles incoming HTTP requests and delegates them to the appropriate application code.

  2. Scalability: With its ability to run multiple worker processes, Gunicorn can scale to handle a large number of concurrent requests. You can adjust the number of workers and threads according to your application's load.

  3. Compatibility: Gunicorn is WSGI-compliant, meaning it can work with any WSGI-compatible web framework, including Django. This makes it a versatile choice for various Python web applications.

  4. Simplicity and Flexibility: It comes with a simple command-line interface and a plethora of configuration options, allowing you to tailor it to your specific needs effortlessly.

Why Gunicorn?

  • Robust Request Handling: An optimized request handling mechanism ensures that your application can handle high traffic efficiently.
  • Concurrency: Multi-threading and multi-processing capabilities allow your application to handle many users concurrently without performance degradation.
  • Flexibility: Easily configurable to meet the different requirements of various deployments and environments.

Preparing Django for Production with WhiteNoise and Gunicorn

Integrating WhiteNoise and Gunicorn into your Django project ensures a smoother transition from development to production. Here's how:

Step-by-Step Implementation:

  1. Install WhiteNoise and Gunicorn:

    pip install whitenoise gunicorn
    
  2. Configure WhiteNoise in Django:
    In your settings.py file, add the following configurations:

    # settings.py
    
    MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    # add the whitenoise middleware here
    "whitenoise.middleware.WhiteNoiseMiddleware",  
    ]
    
    STORAGES = {
    # ...
    "staticfiles": {
        "BACKEND": "whitenoise.storage.CompressedStaticFilesStorage",
    },
    }
    
  3. Collect static files:
    Command in terminal to collect all static files:

    python manage.py collectstatic
    
  4. Use Gunicorn to Run Your Application:
    Command in terminal to use Gunicorn as the application server:

    gunicorn my_project.wsgi:application --bind 0.0.0.0:8000
    

Conclusion

Using WhiteNoise and Gunicorn greatly improves the efficiency, performance, and security of your Django application in a production environment. While WhiteNoise simplifies and secures static file serving, Gunicorn ensures your application can handle large volumes of traffic with minimal latency. Together, they provide a robust foundation for deploying a Django application that is ready to meet the demands of real-world usage.


Create a Dockerfile:

In this part of the article, a detailed explanation of a Dockerfile used for building a containerized Django web application is displayed. This Dockerfile is tailored for a Django application, but the principles can be applied to other frameworks and types of applications. The Dockerfile has to be created in the root directory of your Django project.

# Dockerfile

# Use the official Python image from Docker Hub as the base image
FROM python:3.11-slim

# Set the working directory in the container
WORKDIR /app

# Environment variables to prevent Python from writing .pyc files and to buffer stdout/stderr
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Copy the requirements.txt file into the container
COPY ./requirements.txt .

# Install the Python dependencies
RUN pip install --upgrade pip && pip install -r requirements.txt

# Copy the rest of the application code into the container
COPY . .

# Collect static files for the Django application
RUN python manage.py collectstatic --noinput

# Expose port 8000 to allow traffic to the application
EXPOSE 8000

# Define the command to run the application
CMD ["gunicorn", "portfolio.wsgi:application", "--bind", "0.0.0.0:8000"]
Enter fullscreen mode Exit fullscreen mode

Dockerfile Breakdown

FROM python:3.11-slim
Enter fullscreen mode Exit fullscreen mode
  • Base Image: The python:3.11-slim image is a minimal Python environment tailored to run lightweight applications. It's based on Debian with only the essential packages, making it a good choice for maintaining a small image size.
WORKDIR /app
Enter fullscreen mode Exit fullscreen mode
  • Setting Working Directory: The WORKDIR instruction sets the working directory inside the container to /app. Any subsequent instructions will be executed in this directory.
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
Enter fullscreen mode Exit fullscreen mode
  • Environment Variables:
    • PYTHONDONTWRITEBYTECODE=1: Prevents Python from writing .pyc files to disk, which can save space and reduce I/O operations.
    • PYTHONUNBUFFERED=1: Ensures that the Python output is sent straight to the terminal (or log) without being buffered, which is useful for debugging and real-time logging.
COPY ./requirements.txt .
Enter fullscreen mode Exit fullscreen mode
  • Copying Dependencies File: This instruction copies the requirements.txt file from the host machine to the current working directory inside the container (/app).
RUN pip install --upgrade pip && pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode
  • Installing Dependencies:
    • pip install --upgrade pip: Updates pip to the latest version.
    • pip install -r requirements.txt: Installs the dependencies listed in requirements.txt.
COPY . .
Enter fullscreen mode Exit fullscreen mode
  • Copying Application Code: Copies all remaining files from the current directory on the host machine to the working directory in the container.
RUN python manage.py collectstatic --noinput
Enter fullscreen mode Exit fullscreen mode
  • Collecting Static Files: The collectstatic command (specific to Django) moves all static files (CSS, JavaScript, images) into a single location for easy serving. The --noinput flag makes sure the command runs non-interactively.
EXPOSE 8000
Enter fullscreen mode Exit fullscreen mode
  • Exposing Ports: Informs Docker that the container listens on port 8000. This makes it possible to map the container port to a port on the host machine for network access.
CMD ["gunicorn", "portfolio.wsgi:application", "--bind", "0.0.0.0:8000"]
Enter fullscreen mode Exit fullscreen mode
  • Starting the Application: Defines the default command to run when the container starts. It uses gunicorn (a Python WSGI HTTP Server) to serve the Django application.
    • "portfolio.wsgi:application": Specifies the WSGI application callable to use.
    • "--bind", "0.0.0.0:8000": Binds gunicorn to all network interfaces on port 8000.

Conclusion

This Dockerfile encapsulates the necessary steps to containerize a Python web application efficiently. By leveraging Docker, one can ensure application consistency across different environments, simplify dependencies management, and streamline deployment processes. Below are a few key takeaways from this Dockerfile:

  1. Lean Base Image: Using a slim version of the Python image keeps the container lightweight.
  2. Working Directory: Structuring the container’s file system for clarity and organization.
  3. Dependency Management: Ensuring all necessary packages are installed.
  4. Environment Variables: Optimizing Python behavior for containerized environments.
  5. Static Files Handling: Preparing static assets for production environments.
  6. Port Exposure: Making the application accessible via a specific port.
  7. Command Specification: Defining the service entry point cleanly and efficiently.

Build, check, run, tag and push the Docker image:

Build the Docker image:

docker build -t portfolio .
Enter fullscreen mode Exit fullscreen mode

portfolio is the name of the image, you can chose it however you like.

Verify/check the image(s) on your machine:

docker images
Enter fullscreen mode Exit fullscreen mode

Start/run the container of the local Docker image:

docker run -p 8000:8000 -e SECRET_KEY=secret -e ALLOWED_HOSTS='*' -e DEBUG=True portfolio
Enter fullscreen mode Exit fullscreen mode
[2024-07-18 16:05:26 +0000] [1] [INFO] Starting gunicorn 22.0.0
[2024-07-18 16:05:26 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
[2024-07-18 16:05:26 +0000] [1] [INFO] Using worker: sync
[2024-07-18 16:05:26 +0000] [7] [INFO] Booting worker with pid: 7
Enter fullscreen mode Exit fullscreen mode

go to url: http://0.0.0.0:8000 in your browser and you should see your website.

My website example looks like this:

Image description

Verify running container:

docker ps
Enter fullscreen mode Exit fullscreen mode

Verify all container, even already stopped:

docker ps -a
Enter fullscreen mode Exit fullscreen mode

-a --all (displays all container)

Removes all stopped/unused containers:

docker container prune -f
Enter fullscreen mode Exit fullscreen mode

-f (--force)

If your local Docker image is working fine then the next steps are:

Tag your local image with the Docker Hub repository name:

docker tag portfolio your_username/portfolio:latest
Enter fullscreen mode Exit fullscreen mode

portfolio: is the name of the local repository
your_username/portfolio: your_username is your username of Docker Hub. and "/portfolio" is the name of your Docker Hub repository.

Login into Docker Hub:

docker login -u your_username
Enter fullscreen mode Exit fullscreen mode

Push the tagged image to Docker Hub:

docker push your_username/portfolio:latest
Enter fullscreen mode Exit fullscreen mode

latest: is the tagname, the tag version


Automating CI/CD with GitHub Actions for a Docker-Based Django Application

In the ever-evolving world of software development, Continuous Integration and Continuous Delivery (CI/CD) are pivotal for ensuring rapid, reliable, and resilient application development and deployment processes. GitHub Actions provides an awesome platform to automate these workflows. This article will break down a GitHub Actions configuration file designed to build and push Docker images to Docker Hub whenever changes are pushed to the main branch or pull requests are made against it.

Preparation:

Setting up secrets on GitHub is a straightforward process and is essential for securely storing sensitive information like API keys, tokens, or other credentials.

  1. Navigate to your repository on GitHub.
  2. Click on the "Settings" tab.
  3. In the left sidebar, click on "Secrets and variables" > "Actions".
  4. Click the "New repository secret" button.
  5. Add the SECRET_KEY secret:
    • Name: SECRET_KEY
    • Value: (Your secret key value)
    • Click "Add secret".

Repeat the steps to add ALLOWED_HOSTS, DEBUG, DOCKERHUB_TOKEN and USERNAME secrets. (If you have different usernames for Docker Hub and GitHub, then create several secrets)

Overview of the GitHub Actions Workflow

Let's take a look at the provided config.yml file, which is located in .github/workflows/ directory:

# .github/workflows/config.yml

name: Continuous Integration and Delivery

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  docker_actions:
    name: Docker
    runs-on: ubuntu-latest

    steps:
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: your_username/portfolio:latest
Enter fullscreen mode Exit fullscreen mode

Breakdown of the Workflow

1. Workflow Name and Triggers

name: Continuous Integration and Delivery

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
Enter fullscreen mode Exit fullscreen mode
  • Workflow Name: This is set to "Continuous Integration and Delivery".

  • Triggers: The workflow triggers on two events:

    • push to the main branch.
    • pull_request events targeting the main branch.

This ensures that the workflow runs whenever code is pushed to the main branch or when a pull request is created or updated against the main branch.

2. Job Definition

jobs:
  docker_actions:
    name: Docker
    runs-on: ubuntu-latest
Enter fullscreen mode Exit fullscreen mode
  • Job Name: docker_actions, with a display name of Docker.
  • Runs-On: The workflow runs on the latest version of Ubuntu (ubuntu-latest), ensuring a consistent and up-to-date environment.

3. Steps of the Job

The job comprises a series of steps crucial for setting up the Docker environment, building, and pushing the Docker image.

Step 1: Set up QEMU

steps:
  - name: Set up QEMU
    uses: docker/setup-qemu-action@v3
Enter fullscreen mode Exit fullscreen mode
  • QEMU Setup: This action sets up QEMU, a generic and open-source machine emulator and virtualizer. It's essential for building multi-platform Docker images.

Step 2: Set up Docker Buildx

  - name: Set up Docker Buildx
    uses: docker/setup-buildx-action@v3
Enter fullscreen mode Exit fullscreen mode
  • Buildx Setup: Docker Buildx is a CLI plugin that extends Docker with the full support of the features provided by Moby BuildKit builder toolkit. This step is essential for building multi-architecture images and leveraging advanced features such as caching.

Step 3: Login to Docker Hub

  - name: Login to Docker Hub
    uses: docker/login-action@v3
    with:
      username: ${{ secrets.USERNAME }}
      password: ${{ secrets.DOCKERHUB_TOKEN }}
Enter fullscreen mode Exit fullscreen mode
  • Docker Hub Login: This step uses the docker/login-action to log into Docker Hub using credentials stored in GitHub Secrets. secrets.USERNAME and secrets.DOCKERHUB_TOKEN are environment variables that securely store your Docker Hub username and access token, respectively.

Step 4: Build and Push Docker Image

  - name: Build and push
    uses: docker/build-push-action@v5
    with:
      push: true
      tags: your_username/portfolio:latest
Enter fullscreen mode Exit fullscreen mode
  • Build and Push:
    • docker/build-push-action is used to build and push Docker images.
    • push: true indicates that the image should be pushed to Docker Hub if the build is successful.
    • tags specifies the name and tag of the Docker image to be pushed, in this case, your_username/portfolio:latest.

Conclusion

This GitHub Actions configuration automates the CI/CD pipeline for a Django project containerized with Docker. The process ensures that the Docker image is built and pushed to Docker Hub every time code is pushed or updated on the main branch, making sure that your deployments are up-to-date and reliable.

  • Automated Builds: You no longer need to manually build and push Docker images, ensuring consistency and saving time.
  • Secure and Efficient: Using encrypted secrets for Docker Hub credentials ensures security, while automated workflows enhance efficiency and reliability.
  • Multi-platform Support: The setup steps including QEMU and Buildx ensure that you can build images for multiple architectures if necessary.

By integrating this workflow into your project, you bring the power of continuous integration and delivery to your development pipeline, fostering a healthier, faster, and more reliable deployment cycle.


Resources:

Top comments (0)