Skip to main content

Welcome to a Grasp lesson

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

Lesson 78

Deploying Django with Gunicorn and Nginx

Recommended resources

Hello! Welcome back to your course on Backend Engineering.

In our last lesson, we established a crucial foundation for production applications by learning how to securely manage secrets and configuration using environment variables. We saw how to separate configuration from code, a vital practice for building secure and portable applications.

Today, we will build directly on that foundation. Our learning outcome is to serve a Django application using a WSGI server (Gunicorn) behind an Nginx reverse proxy. This lesson marks a significant transition from writing code that works to deploying code that performs securely and efficiently in a real-world environment. We will replace Django's simple development server with a professional-grade stack, and I'll connect this back to how the environment variables we configured are used in this new setup.

1. The Problem with the Development Server

So far, you've been running your application with the command python manage.py runserver. This is perfect for development because it's simple and provides features like automatic reloading when you change your code. However, it is explicitly not designed for production use.

The Django documentation itself warns against it:

"DO NOT USE THIS SERVER IN A PRODUCTION SETTING. It has not gone through security audits or performance tests... We’re in the business of making Web frameworks, not Web servers..."

The development server is single-threaded, meaning it can only handle one request at a time. It's not built to handle the security, performance, or concurrency demands of a live application. To go to production, we need a more robust architecture.


2. The Production Architecture: Nginx and Gunicorn

In a typical production deployment for a Django application, we use a combination of two key components: a reverse proxy (like Nginx) and a WSGI application server (like Gunicorn).

Let's look at a diagram of how they work together.

Django Application Deployment Architecture with Nginx and Gunicorn
This diagram illustrates the flow of a web request in a production environment. A client request first hits Nginx. Nginx serves static files (like CSS and images) directly. For dynamic content, it forwards (proxies) the request to Gunicorn, which runs the Django application. Gunicorn processes the request using your Django code, interacts with the database if needed, and returns a response back through the same chain.

Here's the division of labor:

  • Nginx (The Public-Facing Web Server): Sits at the edge, receiving all incoming traffic from the internet. It's highly optimized for handling thousands of concurrent connections.
  • Gunicorn (The Application Server): Sits between Nginx and your Django app. Its job is to run and manage multiple processes of your Python application, translating requests from Nginx into a format Django can understand.
  • Django (The Web Framework): Your application code, which focuses solely on business logic. It receives the processed request from Gunicorn.

Why this separation? Because each component is specialized for its task, leading to a more performant, scalable, and secure system.


3. The "Under the Hood": WSGI and Pre-forking

You've expressed an interest in the "under the hood" mechanisms, so let's dive into why Gunicorn is necessary. The link between a web server and a Python web framework is a specification called WSGI (Web Server Gateway Interface). It defines a standard way for them to communicate.

Early web servers used a technology called CGI, which started a new process for every single request. Given your background, you can immediately see the performance bottleneck: process creation is expensive. WSGI servers like Gunicorn solve this with a pre-forking model.

To understand this crucial concept, let's watch a video that explains the problem and the solution.

All You Need to Know about WSGI

The video 'All You Need to Know about WSGI' explains the history and technical reasons for using an application server like Gunicorn. It will clarify why the Gunicorn/Nginx combination is so effective.

Please watch the following segments: (07:15 - 08:48): This part explains the performance limitations of the old CGI model, highlighting why starting a new process for every request is inefficient. (08:48 - 10:18): This introduces the 'pre-forking' model used by Gunicorn. It explains that Gunicorn is essentially a minimal web server that can efficiently manage multiple Python processes (workers). (10:18 - 11:04): This segment beautifully ties everything together, showing how a web server like Nginx (which can't run WSGI apps itself) forwards requests to a WSGI server like Gunicorn.

The key takeaways are:

  1. Gunicorn pre-spawns multiple "worker" processes of your Django application before requests arrive.
  2. When a request comes in, it's handed off to an already-running worker, avoiding the costly overhead of starting a new process.
  3. Nginx acts as the front door, passing requests to Gunicorn's pool of workers.

4. The "How": A Practical Docker Implementation

Now, let's build this architecture. We will continue using Docker and Docker Compose, as this is the standard for modern deployments. The following video gives a fantastic, step-by-step walkthrough of setting this up. We will then break down the key configuration files.

Docker + Django: Containerize the Right Way with Nginx, Postgresql & Gunicorn

The video 'Docker + Django: Containerize the Right Way' provides a complete, practical guide to setting up the Nginx-Gunicorn-Django stack in Docker. Watch it to see how all the pieces fit together in code.

Focus on these specific parts of the video which demonstrate the core setup: Gunicorn Setup (03:31 - 04:57): Pay attention to how Gunicorn is started with a specific command in the entrypoint.sh script. Note the wsgi_app path and worker configuration. Docker Compose Overview (09:43 - 10:48): Observe how the three services (database, Django app, Nginx) are defined in docker-compose.yml. Notice the depends_on key and the crucial static volume. Nginx Configuration (11:25 - 12:51): This is the most important part. Understand the nginx.conf file, especially the proxy_pass directive that forwards requests to the Django service and the location /static/ block that serves static files. Connecting Volumes (12:51 - 13:33): See how the nginx.conf file and the shared static volume are mounted into the Nginx container within docker-compose.yml.

Breakdown of the Configuration

Let's consolidate what you saw in the video into the key configuration files you'll need. The following article provides excellent code examples that we can analyze.

Dockerizing Django with Postgres, Gunicorn, and Nginx

The article 'Dockerizing Django with Postgres, Gunicorn, and Nginx' provides clear, text-based examples of the configuration files. We'll use it to examine the specifics.

Please read the sections titled 'Gunicorn' and 'Nginx'. Focus on the code blocks for docker-compose.prod.yml and nginx.conf. We'll dissect them below.

Let's review the three critical pieces of the puzzle.

1. The Gunicorn Command

In your docker-compose.yml (or an entrypoint.sh script as shown in the video), the command to start your app service changes from python manage.py runserver to:

gunicorn your_project_name.wsgi:application --bind 0.0.0.0:8000 --workers 3
  • your_project_name.wsgi:application: This tells Gunicorn where to find the WSGI entry point for your Django project.
  • --bind 0.0.0.0:8000: Gunicorn listens for requests on port 8000 inside the container's private network.
  • --workers 3: Gunicorn starts 3 worker processes. The optimal number depends on your server's CPU cores.

2. The Nginx Configuration (nginx.conf)

This file tells Nginx how to handle incoming requests.

server {
    listen 80;

    location /static/ {
        alias /path/to/your/static/files/;
    }

    location / {
        proxy_pass http://web: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;
    }
}
  • listen 80;: Nginx listens on port 80, the standard HTTP port.
  • location /static/: When a request URL starts with /static/, Nginx serves the file directly from the filesystem (alias ...). It does not pass this to Django. This is fast and efficient.
  • location /: This is a catch-all for any other request.
    • proxy_pass http://web:8000;: This is the magic. It forwards the request to the service named web (your Django/Gunicorn service in docker-compose.yml) on port 8000.
    • proxy_set_header: These lines pass along important information about the original request to your Django application.

3. The docker-compose.yml Orchestration

Your docker-compose.yml file ties everything together.

version: '3.8'

services:
  web:
    build: .
    command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - static_volume:/app/staticfiles
    expose:
      - 8000
    depends_on:
      - db

  nginx:
    build: ./nginx
    volumes:
      - static_volume:/app/staticfiles
    ports:
      - "80:80"
    depends_on:
      - web

volumes:
  static_volume:
  • web service (Django/Gunicorn):
    • Uses the gunicorn command.
    • expose: - 8000: This makes port 8000 available only to other services in the Docker network (like Nginx), but not to the host machine.
    • volumes: - static_volume:/app/staticfiles: It mounts a shared volume. When you run collectstatic, the files land here.
  • nginx service:
    • ports: - "80:80": This maps port 80 on your host machine to port 80 in the Nginx container, making it accessible from the outside world.
    • volumes: - static_volume:/app/staticfiles: It mounts the exact same shared volume, giving it read-access to the static files collected by Django.
Test your understanding!

A user visits http://yourdomain.com/admin/login/.

  1. Which service receives this request first?
  2. Based on the nginx.conf above, what does that service do with the request?
  3. Which service ultimately processes the request and returns the admin login page?
Show answer
  1. Nginx receives the request first because it's listening on the public port 80.
  2. The URL path /admin/login/ does not match location /static/. Therefore, it matches the catch-all location / block. Nginx then executes proxy_pass http://web:8000;, forwarding the request to the web service (Gunicorn).
  3. Gunicorn receives the request from Nginx, passes it to a Django worker process, which then processes the logic for the admin login view and returns the HTML response.

Conclusion

You have now learned how to construct a professional, production-grade deployment stack for a Django application. This architecture separates concerns, allowing each component to do what it does best, resulting in a system that is performant, scalable, and secure.

Key Takeaways:

  • runserver is for development only. Production requires a dedicated WSGI server and a reverse proxy.
  • Gunicorn is the WSGI server. It runs your Django application in multiple worker processes, handling requests efficiently using a pre-forking model.
  • Nginx is the reverse proxy. It acts as the front door, serving static files directly and forwarding dynamic requests to Gunicorn via proxy_pass.
  • Docker Compose orchestrates the stack. It defines the services, manages the network between them, and uses shared volumes for static files.

Preview of the Next Lesson:
We have designed and containerized a production-ready application. The next logical step is to get it running on the internet. In our next lesson, we will provision a cloud server (using AWS EC2) and configure its basic security, preparing the ground for deploying the Docker containers we've just designed.