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.

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.
-
SSH into your EC2 Instance:
Connect to the EC2 instance where your project is deployed. -
Stop Your Running Application:
Navigate to your project directory and stop the currently running Docker containers.cd ~/infilytics-project docker-compose down -
Add Gunicorn to Your Dependencies:
Gunicorn is a Python package, so you need to add it to yourrequirements.txtfile.echo "gunicorn" >> requirements.txtYou can verify it was added with
cat requirements.txt. -
Update
docker-compose.ymlto Use Gunicorn:
Now, you'll edit yourdocker-compose.ymlfile to replace therunservercommand with a call togunicorn.
Open the file withnano docker-compose.yml. Find thewebservice definition. It probably looks something like this:services: web: build: . command: python manage.py runserver 0.0.0.0:8000 # ... other configurationsChange the
commandto the following:services: web: build: . # Replace the runserver command with gunicorn command: gunicorn infilytics.wsgi:application --bind 0.0.0.0:8000 # ... other configurationsLet'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. Theapplicationobject is defined inside yourwsgi.pyfile.--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.
-
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 -
Verify Gunicorn is Running:
Check the logs of yourwebcontainer.docker-compose logs -f webYou 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.

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.
-
SSH and Navigate: Connect to your EC2 instance and go to your project directory.
-
Edit
docker-compose.yml:
Open the file withnano docker-compose.ymland modify thecommandfor thewebservice again:# ... command: gunicorn infilytics.wsgi:application --bind 0.0.0.0:8000 --workers 3 # ...We've simply added the
--workers 3flag. -
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 -
Verify the Workers:
Check the logs again withdocker-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: 11Your 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.txtanddocker-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.