page-banner-shape-1
page-banner-shape-2

Deploy Python Web Apps With Gunicorn and Nginx in 2026

  • Tanuj Chugh
  • October 28, 2026
Deploy Python

Deploy Python Web Apps With Gunicorn and Nginx in 2026

Deploy Python

Running python app.py on a server is fine while you build an application, but it is not how you should deploy Python code for real users. A production site has to handle many visitors at once, recover from crashes, and encrypt traffic. The standard way to run Flask or Django applications on Linux is to run the code under Gunicorn and to place Nginx in front of it. 

Gunicorn is a WSGI HTTP server for Python. It starts several worker processes, and each worker loads your application and answers requests. Nginx is a fast web server and reverse proxy that accepts connections from the internet, terminates HTTPS, serves static files, and forwards dynamic requests to Gunicorn. Together they give you a stack that is stable, secure, and easy to scale. 

This guide shows you how to deploy Python applications on Ubuntu with Gunicorn, systemd, and Nginx, using commands that work on Ubuntu 24.04 LTS and Ubuntu 26.04 LTS. It covers architecture, setup, HTTPS, framework settings, tuning, security, troubleshooting, and updates, so that you can deploy Python projects with confidence and keep them running through 2026 and beyond. The examples use Flask, and Django differences are noted where they matter. 

Why You Cannot Simply Run the Development Server to Deploy Python 

The built in servers that come with Flask and Django are meant for development. The Flask documentation and the Django documentation both state clearly that these servers must not be used in production. They are not hardened against hostile traffic and offer limited concurrency. If you deploy Python this way, one slow request can make your whole site unresponsive. 

A production setup has to solve several problems at once. It needs more than one process, so that a request waiting on a database does not block every other visitor. It needs a supervisor that restarts the application after a crash or a server reboot. It needs a hardened front end that deals with slow clients and malformed requests, and it needs TLS so that passwords and cookies are encrypted in transit. Gunicorn, systemd, and Nginx solve these problems one by one. 

Architecture to Deploy Python With Gunicorn and Nginx 

A request travels through a short chain. The visitor browser connects to Nginx on port 443 for HTTPS or port 80 for HTTP. If the request is for a static file, Nginx reads the file from disk and replies directly. If the request is for a dynamic page, Nginx forwards it to Gunicorn through a Unix socket. Gunicorn gives the request to a free worker, the worker runs your Flask or Django code through the WSGI interface, and the response travels back along the same path. 

Systemd sits beside this chain as the supervisor. It starts Gunicorn at boot, restarts it when it fails, and collects its logs. Most deployment errors come from a mismatch between the application user, the socket location, and the file permissions. When you deploy Python with this layout, keep these rules in mind. 

  • Gunicorn listens on a Unix socket or on 127.0.0.1 and never on a public address. 
  • Only Nginx is reachable from the internet, on ports 80 and 443. 
  • The application runs as a dedicated user that is not root. 

This guide uses a Unix socket because file permissions can protect it. A TCP address such as 127.0.0.1:8000 also works when Nginx and Gunicorn run in different containers. 

How Gunicorn Works When You Deploy Python Applications 

Gunicorn, short for Green Unicorn, is a Python WSGI HTTP server for Unix systems. It uses a pre fork worker model. A master process starts and manages a set of worker processes, and each worker handles requests on its own. The master does not serve requests. It watches the workers and restarts any that die or stop responding. The design page of the Gunicorn documentation explains this model in detail. 

WSGI, defined in PEP 3333, is the standard interface between Python web servers and Python web applications. Because Flask and Django both implement WSGI, Gunicorn can run either of them without special code. You point Gunicorn at a callable written as module:name, such as wsgi:app for a Flask project or myproject.wsgi:application for a Django project. Getting this name right is the first step when you deploy Python code with Gunicorn. 

The default worker type is sync, in which each worker handles one request at a time. The gthread type adds threads inside each worker, which helps applications that spend time waiting on databases and external services. For ordinary Flask and Django sites, sync workers are a safe starting point. 

Two settings matter most. The workers setting decides how many processes run, and the timeout setting decides how long a worker may stay silent before the master kills and replaces it. The default timeout is 30 seconds. The Gunicorn deployment guide also recommends running Gunicorn behind a proxy server such as Nginx, because Gunicorn is not meant to face slow or hostile clients directly. 

What Nginx Does in Front of Gunicorn When You Deploy Python 

Nginx, pronounced engine x, is an open source web server, reverse proxy, and load balancer. It uses an event driven architecture in which a small number of worker processes handle thousands of connections each, so it needs little memory even under heavy traffic. It also buffers requests and responses, which means a visitor on a slow mobile connection does not hold a Gunicorn worker for the whole transfer. 

In this stack Nginx performs five jobs. It terminates TLS, serves static and media files straight from disk, forwards dynamic requests to Gunicorn together with headers that describe the original request, compresses responses, and enforces limits such as the maximum upload size and the request rate. The Nginx documentation describes every directive used in this guide. When you deploy Python services next to a website, the same Nginx can proxy several applications by using separate server blocks. 

Prerequisites Before You Deploy Python on a Server 

Prepare the following items before you deploy Python code on a production server. 

  • A Linux server with sudo access. Ubuntu 24.04 LTS and Ubuntu 26.04 LTS both work with every command in this guide. Ubuntu 26.04 LTS was released in April 2026. A Linux VPS with 1 GB of memory is enough for a small Flask or Django site. 
  • A domain name with an A record, and an AAAA record if you use IPv6, that points to the public address of the server. Certificates cannot be issued without it. 
  • A Flask or Django application stored in a Git repository, with a requirements file that lists every dependency. 
  • SSH access with key based login, and ports 22, 80, and 443 reachable from the internet. 

The commands below use the domain example.com, the project name myproject, the application user appuser, and the project directory /srv/myproject. Replace these with your own values. 

Step 1: Prepare the Server and Create a Virtual Environment to Deploy Python 

Log in over SSH, refresh the package lists, and install Python tooling and Nginx. The python3-dev and build-essential packages are only needed when a dependency has to compile C code, but installing them avoids confusing build errors later. 

sudo apt update 
sudo apt upgrade -y 
sudo apt install -y python3-venv python3-dev build-essential nginx 

Create a system user that cannot log in interactively. It will own the code and run the application, so a flaw in your code never gives an attacker root access. 

sudo adduser --system --group --home /srv/myproject appuser 

Copy or clone your project into /srv/myproject and make sure appuser owns the files. Then create the virtual environment and install the dependencies as that user. 

cd /srv/myproject 
sudo chown -R appuser:appuser /srv/myproject 
sudo -u appuser python3 -m venv venv 
sudo -u appuser venv/bin/pip install --upgrade pip 
sudo -u appuser venv/bin/pip install gunicorn flask 

For a Django project, install django instead of flask, or run pip install -r requirements.txt after adding gunicorn to that file. Pin exact versions in the requirements file so that every deploy Python release uses the same packages that you tested. 

Flask needs a small entry file named wsgi.py in the project directory that exposes the application object. If your Flask object is called app inside myapp.py, the file contains one line. 

from myapp import app 

Django already provides this entry point in myproject/wsgi.py as application. 

Step 2: Test Gunicorn Manually Before You Deploy Python as a Service 

Run Gunicorn by hand once before you turn it into a service, because errors are easier to read in a terminal. Bind to 127.0.0.1 so that the test is private. 

cd /srv/myproject 
sudo -u appuser venv/bin/gunicorn --bind 127.0.0.1:8000 wsgi:app 

For Django, replace wsgi:app with myproject.wsgi:application. Open a second SSH session and request the page. 

curl -I http://127.0.0.1:8000/ 

A response with a 200 status or a redirect means Gunicorn can import and run your code. Press Ctrl+C in the first session to stop it. A ModuleNotFoundError usually means the working directory is wrong or a package is missing. The message Failed to find attribute means the name after the colon does not match the object in your file. Do not bind to 0.0.0.0 while you deploy Python on a public server, because that exposes Gunicorn to the internet without Nginx in front of it. 

Step 3: Keep Gunicorn Settings in a Configuration File When You Deploy Python 

Long command lines are hard to read, so keep the settings in a Python configuration file that lives with your code and is reviewed like any other change. Create /srv/myproject/gunicorn.conf.py with this content. 

bind = "unix:/run/gunicorn/myproject.sock" 
workers = 3 
worker_class = "sync" 
timeout = 30 
graceful_timeout = 30 
max_requests = 1000 
max_requests_jitter = 100 
umask = 0o007 
accesslog = "-" 
errorlog = "-" 
loglevel = "info" 

The bind line places the socket in /run/gunicorn, a directory that systemd will create in the next step. Three workers suit one CPU core, following the common formula of two workers per core plus one. The max_requests settings restart each worker after roughly one thousand requests, with jitter so that workers do not all restart together. The umask value of 0o007 makes the socket readable and writable only by its owner and group, which is exactly what Nginx needs. Writing both logs to standard output lets systemd store them in the journal. 

Step 4: Run Gunicorn as a systemd Service to Deploy Python Reliably 

Store secrets such as the secret key and database password in an environment file that only root can read. Systemd reads this file as root before it starts the application. 

sudo mkdir -p /etc/myproject 
sudo nano /etc/myproject/myproject.env 
sudo chmod 600 /etc/myproject/myproject.env 

Add one KEY=value pair per line inside that file, for example SECRET_KEY and DATABASE_URL. Then create the service file with sudo nano /etc/systemd/system/gunicorn.service. 

[Unit] 
Description=Gunicorn instance for myproject 
After=network.target 
[Service] 
Type=notify 
User=appuser 
Group=www-data 
RuntimeDirectory=gunicorn 
WorkingDirectory=/srv/myproject 
EnvironmentFile=/etc/myproject/myproject.env 
ExecStart=/srv/myproject/venv/bin/gunicorn --config /srv/myproject/gunicorn.conf.py wsgi:app 
ExecReload=/bin/kill -s HUP $MAINPID 
KillMode=mixed 
TimeoutStopSec=40 
Restart=on-failure 
PrivateTmp=true 
NoNewPrivileges=true 
[Install] 
WantedBy=multi-user.target 

Several lines deserve an explanation. Type=notify lets Gunicorn tell systemd when it is ready, which current Gunicorn releases support. The service runs as appuser with the group www-data, so the socket belongs to the group that Nginx uses. RuntimeDirectory=gunicorn creates /run/gunicorn at start and removes it at stop. ExecReload sends the HUP signal so that you can reload without dropping requests. TimeoutStopSec is set a little above the graceful timeout, and Restart=on-failure brings the service back after a crash. 

Load the new unit, start it, and confirm that the socket exists. 

sudo systemctl daemon-reload 
sudo systemctl enable --now gunicorn 
sudo systemctl status gunicorn 
ls -l /run/gunicorn/ 
sudo curl --unix-socket /run/gunicorn/myproject.sock http://localhost/ 

The enable option with now starts the service immediately and at every boot, so your deploy Python setup survives a restart of the server. The systemd documentation for service units describes each directive in full. 

Step 5: Configure Nginx to Deploy Python Behind a Reverse Proxy 

Create the server block with sudo nano /etc/nginx/sites-available/myproject. The upstream block names the Gunicorn socket, and the location blocks decide which requests go where. 

upstream myproject_app { 
     server unix:/run/gunicorn/myproject.sock fail_timeout=0; 
} 
server { 
    listen 80; 
    listen [::]:80; 
    server_name example.com www.example.com; 
    client_max_body_size 10m; 
    location /static/ { 
        alias /var/www/myproject/static/; 
        expires 30d; 
        access_log off; 
    } 
   location / { 
        proxy_set_header Host $http_host; 
        proxy_set_header X-Real-IP $remote_addr; 
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
        proxy_set_header X-Forwarded-Proto $scheme; 
        proxy_redirect off; 
        proxy_pass http://myproject_app; 
    } 
} 

The Host header names the requested domain, X-Forwarded-For and X-Real-IP carry the visitor address, and X-Forwarded-Proto tells the application whether the original request used HTTPS. The client_max_body_size value raises the default upload limit of 1 MB to 10 MB, so set it to the largest upload your application accepts. Enable the site, test the syntax, and reload Nginx. 

sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/ 
sudo rm -f /etc/nginx/sites-enabled/default 
sudo nginx -t 
sudo systemctl reload nginx 

Removing the default site prevents it from answering requests meant for your domain. When nginx -t reports that the syntax is ok, visit http://example.com in a browser. Your application should load, which means you can now deploy Python pages to real visitors, and the next step is to protect the connection. 

Step 6: Firewall and HTTPS When You Deploy Python in Production 

Enable the Ubuntu firewall, and allow SSH first. Turning the firewall on before allowing SSH can lock you out of the server. 

sudo ufw allow OpenSSH 
sudo ufw allow 'Nginx Full' 
sudo ufw enable 
sudo ufw status 

The Nginx Full profile opens ports 80 and 443. Port 80 must stay open, because certificate validation and the redirect to HTTPS both use it. Next, install Certbot with the Nginx plugin and request a certificate for both names. 

sudo apt install -y certbot python3-certbot-nginx 
sudo certbot --nginx -d example.com -d www.example.com 
sudo certbot renew --dry-run 

Certbot, maintained by the Electronic Frontier Foundation, edits the Nginx server block, adds the certificate paths, and sets up a redirect from HTTP to HTTPS. Certificates from Let’s Encrypt are short lived, so automatic renewal is essential, and the dry run confirms that it works. If you need an organization validated or wildcard certificate from a commercial authority, SSL certificates are available for that purpose. 

Framework Settings to Deploy Python Apps Correctly Behind a Proxy 

The application sees Nginx as the client, so it must be told how to read the forwarded headers and which settings to use in production. For Django, add the following to the settings module and load secrets from the environment. 

import os 
SECRET_KEY = os.environ["SECRET_KEY"] 
DEBUG = False 
ALLOWED_HOSTS = ["example.com", "www.example.com"] 
CSRF_TRUSTED_ORIGINS = ["https://example.com", "https://www.example.com"] 
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 
SECURE_SSL_REDIRECT = True 
SESSION_COOKIE_SECURE = True 
CSRF_COOKIE_SECURE = True 
STATIC_ROOT = "/var/www/myproject/static/" 

SECURE_PROXY_SSL_HEADER makes Django treat requests as secure when Nginx reports HTTPS. Only use it when the proxy always sets or overwrites that header, as the configuration above does. CSRF_TRUSTED_ORIGINS requires the scheme in each entry. Read the official Django deployment checklist before launch. 

For Flask, wrap the application with ProxyFix so that Flask reads the client address and the scheme from the headers that Nginx sets. Trust exactly one proxy. 

import os 
from werkzeug.middleware.proxy_fix import ProxyFix 
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1) 
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"] 
app.config["SESSION_COOKIE_SECURE"] = True 

The Flask proxy documentation explains why you should only enable ProxyFix when a real proxy sits in front of the app, because otherwise a visitor could forge those headers. Never call app.run with debug enabled in production, since the interactive debugger can allow code execution. When you deploy Python behind a proxy on another host, also set the Gunicorn forwarded_allow_ips setting to the proxy address. 

Static and Media Files When You Deploy Python Web Apps 

Static files are the CSS, JavaScript, and images that ship with your code, and media files are uploads from users. Nginx should serve both. Create the directories, give the application user ownership, and let the www-data group read them. 

sudo mkdir -p /var/www/myproject/static /var/www/myproject/media 
sudo chown -R appuser:www-data /var/www/myproject 
sudo chmod -R 755 /var/www/myproject 

In Django, set STATIC_ROOT as shown earlier and run the collectstatic command on every release, as the application user and with the same environment variables as the service, so the files are gathered into one directory that the Nginx alias points to. Add a second location block for /media/ with its own alias. Never let uploaded files execute as code. 

Long cache headers suit static files whose names change when their content changes. WhiteNoise can serve static files from the Python process on small sites, but Nginx is faster and keeps workers free. Test this early when you deploy Python projects, because a missing stylesheet is a common launch day surprise. 

Tuning Gunicorn and Nginx to Deploy Python for Performance 

Start with the Gunicorn documentation advice of two workers per CPU core plus one, then measure. Each sync worker handles one request at a time, so the worker count is your concurrency limit. Memory is the other limit, because every worker holds a full copy of the application. If the server starts swapping, reduce the workers. For applications that wait on external services, try the gthread worker class and compare the results under load. 

Keep the timeout settings consistent. Gunicorn kills a sync worker that stays silent for longer than its timeout, and Nginx waits 60 seconds by default for a response before it gives up. If Gunicorn kills a worker mid request, visitors see 502 errors, and if Nginx gives up first, they see 504 errors. Long running work belongs in a background job such as a Celery or RQ worker, not in a longer timeout. The preload_app setting saves memory by loading the application before the workers fork, but a reload then does not pick up new code. 

On the Nginx side, keep worker_processes at auto, which is the default in Ubuntu packages, enable gzip compression for text based content types, and set long expiry headers for versioned static files. Enable HTTP/2 using the syntax for your Nginx version, which is the http2 directive from Nginx 1.25.1 onward and a listen parameter in older releases. Test changes with a load tool such as wrk or Locust, one setting at a time. The same configuration works on an Akamai Cloud or Amazon Web Services server. 

Security Hardening When You Deploy Python With Gunicorn and Nginx 

Security starts with the server. Use SSH keys and disable password login. Enable automatic security updates with unattended-upgrades, and add fail2ban to block repeated login attempts. Keep secrets in the environment file instead of the repository, and update dependencies regularly. A tool such as pip-audit checks installed packages against known vulnerabilities. 

Add a few directives to the Nginx server block for a safer default. They hide the version string and ask browsers to respect the declared content types and framing rules. 

server_tokens off; 
add_header X-Content-Type-Options nosniff always; 
add_header X-Frame-Options SAMEORIGIN always; 
add_header Referrer-Policy strict-origin-when-cross-origin always; 

Nginx only inherits add_header lines from the server level when a location block defines none of its own, so repeat the headers wherever you add headers inside a location. Rate limiting protects login pages and APIs from brute force attempts. Define a shared memory zone in the http block, keyed on the client address, and apply it to sensitive locations. 

limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s; 
location /login/ { 
    limit_req zone=login burst=10 nodelay; 
    # repeat the proxy_set_header lines and proxy_pass here 
} 

After HTTPS works on every subdomain, consider the Strict-Transport-Security header. When you deploy Python behind a CDN or load balancer, restore the real client address with the Nginx realip module, or the rate limits will only see the address of the proxy. Layered protection against floods is available through DDoS protection services, and a wider review of your exposure fits into a cyber security plan. 

Troubleshooting Errors After You Deploy Python 

Most problems fall into a small number of patterns. Start with the logs, because they almost always name the cause. 

sudo systemctl status gunicorn 
sudo journalctl -u gunicorn -n 100 --no-pager 
sudo tail -n 50 /var/log/nginx/error.log 
sudo nginx -t 
Symptom Likely cause Fix 
502 Bad Gateway Gunicorn is stopped, the socket path differs between the two files, or Nginx cannot access the socket Check the service status, match the socket path, and confirm the group is www-data with umask 0o007 
504 Gateway Timeout The request took longer than the Nginx proxy_read_timeout value Speed up the query, move the work to a background job, or raise the timeout in Nginx and Gunicorn together 
403 on static files The www-data user cannot read the directory or one of its parents Fix ownership and permissions of the static directory 
404 on static files The alias path does not match the location, or collectstatic was not run Match location and alias, then run collectstatic 

A WORKER TIMEOUT line in the Gunicorn log means a worker was silent for longer than the timeout and was replaced, which points to a slow request. When a new release breaks the site, repeat the manual test from Step 2 to deploy Python code once more in a clean terminal, where the traceback is easy to read. 

Reloads and Zero Downtime Updates for Deploy Python Apps 

A reload sends the HUP signal to the Gunicorn master. The master starts new workers with the new configuration and shuts the old workers down gracefully, so requests that are already running can finish. Without preload_app, the new workers also load the new version of your code. A typical release looks like this. 

cd /srv/myproject 
sudo -u appuser git pull 
sudo -u appuser venv/bin/pip install -r requirements.txt 
sudo systemctl reload gunicorn 

For Django, run database migrations and collectstatic before the reload, and write migrations that keep the old code working until the reload finishes. Use restart instead of reload when you change the systemd unit, after running daemon-reload, or when preload_app is enabled. For Nginx changes, run nginx -t first and then reload Nginx, so that a typing mistake never takes the site down. This routine lets you deploy Python updates many times a day without visible interruption. 

Logging and Monitoring a Deploy Python Setup 

Gunicorn writes to standard output, so its logs are in the systemd journal. Follow them live with journalctl -u gunicorn -f, or search a time range with the since option. Nginx writes access and error logs under /var/log/nginx, and the Ubuntu package rotates them automatically through logrotate. 

Track CPU, memory, disk space, and response times with a monitoring agent, add an external uptime check against a simple health endpoint, and send exceptions to an error tracking service. Alerts on disk usage and on repeated 5xx responses catch most incidents early on any server where you deploy Python apps. 

Conclusion 

Running a Python application in production is mostly about putting the right program in the right place. Gunicorn runs your code in several worker processes and speaks the WSGI interface that Flask and Django understand. Nginx faces the internet, handles HTTPS, serves static files quickly, buffers slow clients, and forwards the remaining requests through a protected Unix socket. Systemd keeps the whole arrangement running, restarts it after failures, and collects its logs. When you deploy Python in this way, each layer can be tested, tuned, and replaced on its own. 

The most important points are easy to summarize. Test Gunicorn by hand before you create a service. Run the application as a dedicated user with a socket that only Nginx can reach. Keep secrets out of the code, switch off debug mode, and tell the framework that it sits behind a proxy so that redirects, cookies, and CSRF checks work over HTTPS. Size the workers to the CPU and memory of your server, keep the Gunicorn and Nginx timeouts consistent, and move slow jobs into a queue. Use reloads for releases, and watch logs and metrics so that you notice trouble before your visitors do. 

This setup also scales well beyond a single small site. You can add more workers, move the database to its own server, place a CDN in front of Nginx, or run several copies of the application behind a load balancer without changing the basic design. The same pattern works on Ubuntu 24.04 LTS and Ubuntu 26.04 LTS. Review the official documentation after each major release and update your dependencies on a schedule. A careful, repeatable process is what lets you deploy Python projects with confidence, whether you run one application or many. 

Frequently Asked Questions

Why do I need both Gunicorn and Nginx to deploy Python apps? 

Gunicorn runs your Python code, but it is not meant to face the open internet or to serve static files at scale. Nginx handles TLS, buffers slow clients, serves static files, and forwards dynamic requests to Gunicorn. 

How many Gunicorn workers should I use? 

A common starting point from the Gunicorn documentation is two workers per CPU core plus one. A server with two cores would start with five workers. Then watch memory use and response times under load, and adjust. 

Should Gunicorn use a Unix socket or a TCP port? 

A Unix socket is a good default when Nginx and Gunicorn run on the same server, because file permissions control who can connect. A TCP address such as 127.0.0.1:8000 is also valid and is easier when the two programs run in separate containers. Never bind Gunicorn to a public address. 

How do I fix a 502 Bad Gateway error? 

A 502 error means Nginx could not get a valid answer from Gunicorn. Check that the service is running with systemctl status gunicorn, confirm that the socket path is identical in the Gunicorn configuration and the Nginx upstream, and make sure the socket group is www-data with a umask that allows group access. 

How do I make Gunicorn start automatically after a reboot? 

Create a systemd service and run sudo systemctl enable gunicorn. The service then starts at boot. 

Can I use the same setup to deploy Python ASGI apps such as FastAPI? 

The Nginx part is almost identical, because it still proxies requests to a socket or port. For the application server, run Uvicorn directly or run Gunicorn with an ASGI capable worker. 

How do I update my application without downtime? 

Pull the new code, install any new dependencies, run migrations and collectstatic if needed, and then run sudo systemctl reload gunicorn. If preload_app is enabled, use a restart instead, because a reload does not load new code in that mode. 

Is it safe to run Gunicorn as root? 

No. If an attacker finds a flaw in your application, a root process gives them full control of the server. Run Gunicorn as a dedicated user with limited permissions, and keep Nginx as the only public entry point. 

Tanuj Chugh

He is the CEO and Founder with over a decade of experience in cloud infrastructure, DevOps, and server optimization. With a strong vision and hands-on leadership approach, he has built scalable, secure, and high-performance cloud solutions trusted by businesses across industries.

https://cloudminister.com/
Call Now Button