DevOps Intermediate

Deploy Django with Gunicorn & Nginx on Linux

Step-by-step guide to deploying a Django application on a Linux server with Gunicorn, Nginx, and systemd. Production-ready setup.

DjangoZen Team Mar 29, 2026 16 min read 193 views

Building a Django application is one thing; getting it running reliably on a real server is another, and the gap trips up many developers. The standard production setup pairs Gunicorn, an application server that runs your Django code, with Nginx, a web server that sits in front handling client connections and static files. Understanding why this architecture exists and how the pieces fit makes deploying Django far less mysterious. This tutorial walks through the production stack on a Linux server.

Why not the development server

Django's built-in development server is perfect for development and entirely unsuitable for production. It is single-threaded, not built for performance or security, and explicitly warned against for production use. Serving real users requires a proper application server that can handle concurrent requests efficiently and securely. Understanding this is the starting point: the command you use to run Django while developing is not how you run it in production, and reaching for a production-grade server is not optional but necessary. The development server's convenience comes with limitations that make it inappropriate the moment real users and real traffic are involved.

The production architecture

The standard production setup has a clear division of labor. Gunicorn is the application server: it runs your Django code, executing your views to produce responses, across multiple worker processes for concurrency. Nginx is the web server in front: it accepts connections from clients, serves static files directly, and passes dynamic requests back to Gunicorn. This two-layer design — Nginx handling what it does best at the front, Gunicorn running your Python application behind it — is the architecture most production Django runs on. Understanding the role of each piece is the key to the whole setup and to diagnosing issues when something does not work.

Gunicorn: the application server

Gunicorn runs your Django application, translating incoming requests into calls to your code and returning the responses. It runs multiple worker processes so it can handle several requests concurrently, which is essential for serving more than one user at a time. You point it at your Django application and configure the number of workers based on your server's resources. Gunicorn is mature, reliable, and the common choice for running Django in production. Understanding that Gunicorn is the process actually executing your Django code — the engine running your application behind the scenes — clarifies its central role in the stack and why its configuration affects your application's capacity.

Configuring workers

Gunicorn's concurrency comes from worker processes, and how many you run affects how many requests your application can handle at once. A common guideline bases the number on your server's CPU cores, with the right figure depending on whether your workload is CPU-bound or spends time waiting on the database and external services. Too few workers limits throughput; too many wastes memory. Tuning the worker count to your server and workload is part of getting good performance from the stack. Understanding that workers are what give Gunicorn its concurrency, and that their number is a deliberate configuration choice rather than a default to ignore, helps you size your application appropriately.

Nginx: the web server

Nginx sits at the front of the stack, facing the internet, and handles the things it does far better than an application server: accepting and managing many client connections efficiently, serving static files like CSS, JavaScript, and images directly without involving Django, and forwarding dynamic requests to Gunicorn. It also commonly handles HTTPS termination. Putting Nginx in front offloads connection handling and static file serving from your application, letting Gunicorn focus on running your code. Understanding Nginx's role as the efficient front door that handles connections and static content, passing only dynamic work to your application, explains why this layered architecture performs and scales well.

How the reverse proxy works

The arrangement where Nginx receives requests and passes them to Gunicorn is called a reverse proxy. A client connects to Nginx, which decides whether to serve the request itself (for static files) or forward it to Gunicorn (for dynamic Django responses) and then relay Gunicorn's response back to the client. The client never talks to Gunicorn directly. This indirection is what lets Nginx handle the public-facing concerns while Gunicorn stays focused on the application. Understanding the reverse-proxy flow — request comes to Nginx, gets routed appropriately, response goes back — is the mental model that makes the whole setup and its configuration comprehensible.

Serving static and media files

Django applications have static files — CSS, JavaScript, images — and often media files uploaded by users, and these should be served by Nginx directly rather than through Django, which is inefficient at it. In production you collect your static files into one location and configure Nginx to serve them from there, so requests for static content are handled fast by Nginx without touching your application. Media files are handled similarly. Getting this right is important both for performance and because static files often simply do not appear if the collection and Nginx configuration are not set up correctly, which is a common deployment frustration that understanding this step prevents.

Keeping Gunicorn running

Gunicorn needs to run continuously and restart if it crashes or the server reboots, which is the job of a process manager — on modern Linux, typically systemd. You define a service that starts Gunicorn, keeps it running, restarts it on failure, and starts it automatically when the server boots. Without this, Gunicorn would stop the moment you closed your terminal or the server restarted. Setting up Gunicorn as a managed service is what makes the deployment durable rather than something you have to babysit. Understanding that a process manager keeps your application server alive and recovering from failures is essential to a production setup that stays up reliably.

Adding HTTPS

Any production site should serve traffic over HTTPS, and Nginx is where this is typically handled. You obtain a certificate — modern tooling makes this free and largely automatic — and configure Nginx to use it, encrypting traffic and redirecting plain HTTP to HTTPS. Because Nginx is the front-facing layer, it is the natural place to terminate HTTPS, decrypting incoming requests before passing them to Gunicorn. Setting up HTTPS is a non-negotiable part of deploying a real site, protecting your users' data in transit, and the combination of Nginx and automated certificate tooling makes it a manageable step rather than the ordeal it once was.

A deployment checklist

Pulling it together, a production Django deployment involves configuring Django's settings for production (debug off, allowed hosts set, secret key secured), running it under Gunicorn with an appropriate number of workers, putting Nginx in front as a reverse proxy serving static files and forwarding dynamic requests, managing Gunicorn with a process manager so it stays running, and enabling HTTPS. Each piece addresses a specific concern, and together they form the reliable, performant, secure stack that production Django runs on. Working through these steps methodically — settings, Gunicorn, Nginx, process management, HTTPS — turns the daunting task of deployment into a clear sequence with a well-understood result.

Environment configuration

A production deployment needs its configuration — secret key, database credentials, debug setting, allowed hosts — supplied through the environment rather than hard-coded, both for security and to keep the same code working across environments. You load these from environment variables on the server, keeping secrets out of your code and version control. This separation of configuration from code is a foundational deployment practice. Understanding that production settings come from the server's environment, not from values committed in your settings file, is essential both to keeping credentials secure and to running the same application safely across development and production with environment-appropriate configuration in each.

The database in production

Production Django almost always uses a proper database like PostgreSQL rather than the development default, running either on the same server or a separate one. You configure Django to connect to it, ensure it is set up and secured, and run your migrations against it to create the schema. The database is a critical part of the deployment that needs its own setup and care, including backups. Understanding that deploying involves provisioning and connecting a production-grade database — not relying on the development database — is part of the complete picture, since your application's data lives there and its proper configuration is as important as the application server itself.

Logging and monitoring

In production you cannot watch the console as you do in development, so configuring logging to capture what your application is doing — errors, important events — and having a way to review it is important for knowing your application's health and diagnosing problems. Beyond logs, monitoring that alerts you when something goes wrong lets you respond before users are badly affected. Setting up logging and basic monitoring is part of operating a deployed application responsibly. Understanding that a production deployment needs visibility into its behavior — so that errors and issues are captured and noticed rather than silently happening — is what lets you keep a deployed application running well over time.

Deploying updates

Deployment is not a one-time event; you will update the application as you develop it, and doing so smoothly matters. Updating typically involves getting the new code onto the server, installing any new dependencies, running new migrations, collecting static files, and restarting the application server so it runs the new code. Having a reliable, repeatable process for this — ideally automated — makes updates routine rather than risky. Understanding that deployment includes an ongoing update process, not just the initial setup, prepares you for the reality of maintaining a live application, where you regularly ship changes and want each update to go out reliably without disrupting your users.

Summary

Deploying Django to production means moving beyond the development server to a proper stack, and the standard one pairs Gunicorn with Nginx on a Linux server. Gunicorn is the application server that actually runs your Django code across multiple worker processes for concurrency, while Nginx is the web server in front, acting as a reverse proxy that efficiently handles client connections, serves static and media files directly, and forwards dynamic requests to Gunicorn. A process manager like systemd keeps Gunicorn running and restarts it on failure or reboot, and Nginx is also where you terminate HTTPS using a certificate to encrypt all traffic. Configure Django's settings for production, collect and serve static files through Nginx, and work through the deployment as a clear sequence. Understanding the role of each layer — what Gunicorn runs, what Nginx fronts, what keeps it alive, and how HTTPS fits — turns deployment from a mysterious hurdle into a comprehensible, repeatable process that gets your application running reliably for real users.