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.

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:
- Gunicorn pre-spawns multiple "worker" processes of your Django application before requests arrive.
- When a request comes in, it's handed off to an already-running worker, avoiding the costly overhead of starting a new process.
- 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 namedweb(your Django/Gunicorn service indocker-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:
webservice (Django/Gunicorn):- Uses the
gunicorncommand. 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 runcollectstatic, the files land here.
- Uses the
nginxservice: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/.
- Which service receives this request first?
- Based on the
nginx.confabove, what does that service do with the request? - Which service ultimately processes the request and returns the admin login page?
Show answer
- Nginx receives the request first because it's listening on the public port 80.
- The URL path
/admin/login/does not matchlocation /static/. Therefore, it matches the catch-alllocation /block. Nginx then executesproxy_pass http://web:8000;, forwarding the request to thewebservice (Gunicorn). - 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:
runserveris 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.