Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 61

Deploying Django with Gunicorn

Recommended resources

Welcome to Module 11. In our last lesson, you successfully implemented a robust and secure file handling system using private S3 buckets and pre-signed URLs. Your application is now deployed on AWS, but it's still relying on a critical component meant only for development: the built-in Django server (manage.py runserver). This is a significant bottleneck and security risk in a production environment.

This lesson marks our transition to building a truly production-ready backend. We will replace the development server with Gunicorn, a powerful and widely-used Web Server Gateway Interface (WSGI) application server. Understanding and implementing Gunicorn is a fundamental step toward building the "scalable backend/cloud applications" required by the Infilect role.

By the end of this 60-minute lesson, you will be able to serve your Django application in a production environment using Gunicorn, preparing it to handle real-world traffic efficiently.

1. From Development Server to Production Stack

The Django documentation is very clear: the development server is not for production. It's single-threaded (can only handle one request at a time), has not undergone security audits, and is not optimized for performance. For a production application to be stable, secure, and scalable, it needs a dedicated application server.

This is where Gunicorn comes in. It sits between the outside world and your Django application, managing multiple worker processes to handle many concurrent user requests.

This diagram illustrates a typical production stack. Gunicorn acts as the interface between a web server like Nginx and the Django application itself. We will add Nginx in a future lesson.

To understand Gunicorn's role, we first need to grasp the standard it's built upon: WSGI.


2. The "Why": Understanding WSGI and Application Servers

WSGI (Web Server Gateway Interface) is the standard that defines how a web server communicates with a Python web application. Think of it as a universal adapter.

In the early days of the web, a technology called CGI (Common Gateway Interface) was used. It worked, but it was incredibly inefficient because it had to start a brand new process for your entire application for every single incoming request. This created a massive performance bottleneck.

Modern application servers like Gunicorn solve this problem using a "pre-forking" model. When Gunicorn starts, it creates (pre-forks) several worker processes, each a ready-to-go instance of your Django application. When requests come in, Gunicorn efficiently distributes them to these waiting workers. This is orders of magnitude faster than the old CGI approach.

The following video from the Django Lessons channel provides an excellent conceptual overview of this evolution from CGI to WSGI and explains exactly where Gunicorn fits.

All You Need to Know about WSGI

This video explains the problem WSGI solves and the role of application servers like Gunicorn.

Watch the following segments to understand the core concepts: CGI's performance problem: Understand why starting a new process for every request is a major bottleneck. Pre-forking solution: See how creating worker processes in advance solves the performance issue. Gunicorn's role: This part explicitly connects Gunicorn to the pre-forking model, clarifying its function as a minimal web server that can run your WSGI app. The beauty of WSGI: This wraps up why the standard is so powerful, allowing any compliant server to run any compliant framework.

Now that you understand the "why," let's get our hands dirty and replace Django's development server with Gunicorn in our containerized deployment.


3. Implementing Gunicorn in Your Docker Setup

So far, your docker-compose.yml file likely has a command like python manage.py runserver 0.0.0.0:8000. We will now replace this with a Gunicorn command. This will make our web service production-ready.

Your Task: Integrate Gunicorn into the Infilytics App

We'll follow a few simple steps to add Gunicorn to your project and configure Docker to use it.

  1. SSH into your EC2 Instance:
    Connect to the EC2 instance where your project is deployed.

  2. Stop Your Running Application:
    Navigate to your project directory and stop the currently running Docker containers.

    cd ~/infilytics-project 
    docker-compose down
    
  3. Add Gunicorn to Your Dependencies:
    Gunicorn is a Python package, so you need to add it to your requirements.txt file.

    echo "gunicorn" >> requirements.txt
    

    You can verify it was added with cat requirements.txt.

  4. Update docker-compose.yml to Use Gunicorn:
    Now, you'll edit your docker-compose.yml file to replace the runserver command with a call to gunicorn.
    Open the file with nano docker-compose.yml. Find the web service definition. It probably looks something like this:

    services:
      web:
        build: .
        command: python manage.py runserver 0.0.0.0:8000
    
    
    
        # ... other configurations
    

    Change the command to the following:

    services:
      web:
        build: .
    
    
    
        # Replace the runserver command with gunicorn
        command: gunicorn infilytics.wsgi:application --bind 0.0.0.0:8000
    
    
    
        # ... other configurations
    

    Let's break down this new command:

    • gunicorn: The executable.
    • infilytics.wsgi:application: This tells Gunicorn where to find the WSGI entry point. It follows the pattern [project_name].wsgi:application. The application object is defined inside your wsgi.py file.
    • --bind 0.0.0.0:8000: This tells Gunicorn to listen for requests from any network interface on port 8000, which is exactly what we need inside a Docker container.
  5. Rebuild and Start Your Services:
    Because you've changed both the Python requirements (requirements.txt) and the startup command (docker-compose.yml), you must rebuild the image.

    docker-compose up -d --build
    
  6. Verify Gunicorn is Running:
    Check the logs of your web container.

    docker-compose logs -f web
    

    You should see output from Gunicorn, indicating it has started and is listening. It will look something like this:

    [INFO] Starting gunicorn ...
    [INFO] Listening at: http://0.0.0.0:8000 ...
    [INFO] Using worker: sync
    [INFO] Booting worker with pid: ...
    

    You can now visit your server's public IP address in your browser. The application should load just as before, but it's now being served by the much more powerful Gunicorn.


4. Configuring Gunicorn for Scalability

Simply running Gunicorn is a great first step, but its real power comes from its ability to use multiple worker processes. By default, it starts with only one. For a production application, you need more.

As this table shows, the primary way to scale a Django/Gunicorn component is to add more workers.

The common recommendation for the number of workers is (2 * number_of_CPU_cores) + 1. Since your t2.micro EC2 instance has 1 vCPU, a good starting number of workers is 3.

Your Task: Add Gunicorn Workers

Let's update your docker-compose.yml file one more time to add workers.

  1. SSH and Navigate: Connect to your EC2 instance and go to your project directory.

  2. Edit docker-compose.yml:
    Open the file with nano docker-compose.yml and modify the command for the web service again:

    
    
    
    # ...
        command: gunicorn infilytics.wsgi:application --bind 0.0.0.0:8000 --workers 3
    
    
    
    # ...
    

    We've simply added the --workers 3 flag.

  3. Restart the Service:
    Since you only changed the command and not the underlying image, you don't need to rebuild. You can just restart the services to apply the new command.

    docker-compose up -d 
    
  4. Verify the Workers:
    Check the logs again with docker-compose logs -f web. This time, you should see Gunicorn booting multiple worker processes:

    [INFO] Starting gunicorn ...
    [INFO] Listening at: http://0.0.0.0:8000 ...
    [INFO] Using worker: sync
    [INFO] Booting worker with pid: 9
    [INFO] Booting worker with pid: 10
    [INFO] Booting worker with pid: 11
    

    Your application is now capable of handling multiple requests concurrently.

For more advanced configuration, you can explore creating a Gunicorn configuration file. The Real Python article below provides a good example of this approach, but for now, command-line flags in docker-compose.yml are perfectly sufficient.

Securely Deploy a Django App With Gunicorn, Nginx, & ...

This article provides an alternative method for configuring Gunicorn using a dedicated Python file. This is a pattern you may encounter in other projects.

Focus on the section that begins with creating a configuration file. Notice how settings like workers and bind are defined as Python variables. This file-based approach can be useful for managing more complex configurations.


Conclusion

Fantastic work! You have taken a crucial step in moving your application from a development setup to a production-ready one. By replacing Django's runserver with Gunicorn, you have deployed a stable and scalable application server capable of handling real-world traffic.

In this lesson, you have:

  • Understood the limitations of Django's development server and the role of a WSGI application server.
  • Learned the concept of the pre-forking worker model that makes Gunicorn efficient.
  • Integrated Gunicorn into your Docker-based deployment by updating your requirements.txt and docker-compose.yml.
  • Configured Gunicorn to use multiple worker processes, enabling it to handle concurrent requests.

However, our work isn't done. Right now, Gunicorn is still directly exposed to the internet. This is not ideal for security or for serving static files efficiently. In our next lesson, we will introduce Nginx as a reverse proxy to sit in front of Gunicorn, adding another professional layer to our production stack.