Core Panel

How to Deploy a Django App on Ubuntu with Gunicorn and Nginx

CP
Written by Core Panel Team Product and documentation team · Published Sep 24, 2026 · 12 min read
How to Deploy a Django App on Ubuntu with Gunicorn and Nginx
On this page · 6 sections

Running Django with its built-in development server is useful during development, but it is not a production deployment. A reliable public application needs a process manager, a reverse proxy, HTTPS, controlled secrets, repeatable releases, monitoring, and a recovery plan.

This guide shows how to deploy a Django app on Ubuntu with Gunicorn and Nginx. The result is a practical baseline for a single-server production setup: Nginx receives web traffic, serves static files, and proxies application requests to Gunicorn; systemd keeps Gunicorn running; and Certbot provides TLS certificates.

Deployment architecture

The request path is straightforward:

  • DNS points your domain to the Ubuntu server.
  • Nginx listens on ports 80 and 443, terminates HTTPS, serves static assets, and forwards dynamic requests.
  • Gunicorn runs your Django WSGI application through a Unix socket.
  • systemd starts Gunicorn at boot and restarts it after failures.
  • Django reads secrets and environment-specific settings from a protected environment file.

This separation keeps each component focused and makes failures easier to diagnose.

Prerequisites

Before starting, prepare:

  • An Ubuntu 22.04 or 24.04 server with a non-root sudo user
  • A domain or subdomain with an A or AAAA record pointing to the server
  • Your Django project in Git or another trusted source
  • A production database such as PostgreSQL
  • SSH access and a tested backup destination

Update the server and install the required packages:

sudo apt update
sudo apt upgrade -y
sudo apt install -y python3-venv python3-pip nginx git

If you use PostgreSQL on the same server, install its client libraries as required by your Python database driver.

1. Create a dedicated application user

Avoid running the application as root. Create a restricted system user and an application directory:

sudo adduser --system --group --home /srv/myapp myapp
sudo mkdir -p /srv/myapp/app
sudo chown -R myapp:myapp /srv/myapp

Using a separate identity limits the damage a compromised process can cause and keeps file ownership predictable.

2. Fetch the project and create a virtual environment

Clone the project as the application user, create an isolated Python environment, and install locked dependencies:

sudo -u myapp git clone https://example.com/your/repository.git /srv/myapp/app
sudo -u myapp python3 -m venv /srv/myapp/venv
sudo -u myapp /srv/myapp/venv/bin/pip install --upgrade pip
sudo -u myapp /srv/myapp/venv/bin/pip install -r /srv/myapp/app/requirements.txt
sudo -u myapp /srv/myapp/venv/bin/pip install gunicorn

For reproducible releases, pin dependency versions and review changes before upgrading them in production.

3. Store production settings outside the repository

Create a protected environment file:

sudo install -m 600 -o myapp -g myapp /dev/null /etc/myapp.env
sudo nano /etc/myapp.env

Add only the values your project expects, for example:

DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_SECRET_KEY=replace-with-a-long-random-value
DJANGO_ALLOWED_HOSTS=example.com,www.example.com
DATABASE_URL=postgresql://user:[email protected]:5432/myapp

Never commit production secrets to Git. Restrict the file to the application user and include secret recovery in your operations plan.

4. Configure Django for production

Confirm that your production settings disable debug mode and accept only the intended hostnames:

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")

Set STATIC_ROOT to a deployment directory, configure media storage deliberately, and use secure cookie settings after HTTPS is working. Do not make uploaded media executable.

5. Run migrations and collect static files

Load the environment and run Django's release tasks:

sudo -u myapp bash -c 'set -a; source /etc/myapp.env; set +a; cd /srv/myapp/app && /srv/myapp/venv/bin/python manage.py check --deploy'
sudo -u myapp bash -c 'set -a; source /etc/myapp.env; set +a; cd /srv/myapp/app && /srv/myapp/venv/bin/python manage.py migrate'
sudo -u myapp bash -c 'set -a; source /etc/myapp.env; set +a; cd /srv/myapp/app && /srv/myapp/venv/bin/python manage.py collectstatic --noinput'

Review migration plans before applying destructive schema changes. Take a database backup before high-risk releases.

6. Create a systemd service for Gunicorn

Create /etc/systemd/system/myapp.service:

[Unit]
Description=Gunicorn for my Django application
After=network.target

[Service]
User=myapp
Group=www-data
WorkingDirectory=/srv/myapp/app
EnvironmentFile=/etc/myapp.env
RuntimeDirectory=myapp
RuntimeDirectoryMode=0755
ExecStart=/srv/myapp/venv/bin/gunicorn \
    --workers 3 \
    --bind unix:/run/myapp/gunicorn.sock \
    --access-logfile - \
    --error-logfile - \
    config.wsgi:application
Restart=on-failure
RestartSec=5
PrivateTmp=true
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

Replace config.wsgi:application with your project's WSGI import path. Worker count depends on available CPU, memory, response time, and workload; measure it rather than copying a fixed number blindly.

Load and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
sudo journalctl -u myapp -n 100 --no-pager

7. Configure Nginx as a reverse proxy

Create /etc/nginx/sites-available/myapp:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    client_max_body_size 20m;

    location /static/ {
        alias /srv/myapp/app/staticfiles/;
        access_log off;
        expires 7d;
    }

    location /media/ {
        alias /srv/myapp/app/media/;
    }

    location / {
        include proxy_params;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_pass http://unix:/run/myapp/gunicorn.sock;
    }
}

Enable the site and validate the configuration before reloading:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp
sudo nginx -t
sudo systemctl reload nginx

A configuration test is a small step that prevents an avoidable outage.

8. Enable HTTPS

After DNS resolves to the server and port 80 is reachable, install Certbot and request a certificate:

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

Keep automatic renewal enabled and monitor renewal failures. Redirect HTTP to HTTPS after the certificate is issued.

9. Verify the deployment

Test the application from both inside and outside the server:

  • Open the HTTPS URL and exercise login, forms, uploads, and important business flows.
  • Confirm static files load without 404 responses.
  • Check systemctl status myapp and the Gunicorn journal.
  • Inspect Nginx access and error logs.
  • Restart the server during a maintenance window and verify the service returns automatically.
  • Run Django's deployment checks after every relevant settings change.

10. Use a repeatable release workflow

A safe deployment should be a sequence you can run consistently:

  1. Back up the database and confirm the backup is restorable.
  2. Fetch the reviewed release.
  3. Install locked dependencies.
  4. Run tests and manage.py check --deploy.
  5. Review and apply database migrations.
  6. Collect static files.
  7. Restart Gunicorn and reload Nginx only when its configuration changed.
  8. Run health checks and a short smoke test.
  9. Keep a documented rollback path.

For lower-risk releases, deploy into a versioned directory and switch a current symlink only after preparation succeeds. That approach also makes code rollback faster, although database migrations still require careful planning.

Security and reliability checklist

  • Use SSH keys, restrict administrative access, and keep Ubuntu packages patched.
  • Expose only required ports through the firewall.
  • Run Django and Gunicorn as a non-root user.
  • Keep secrets outside the repository with restrictive permissions.
  • Set DEBUG=False and narrowly define allowed hosts and trusted origins.
  • Back up the database, uploaded media, environment configuration, and deployment metadata.
  • Monitor disk space, memory, CPU, HTTP errors, certificate renewal, and service restarts.
  • Test restoration, not only backup creation.

For a broader hardening plan, use the practical VPS security baseline and the security and recovery checklist.

Troubleshooting common deployment problems

502 Bad Gateway

Check whether Gunicorn is running, whether the socket exists, and whether Nginx can access it:

sudo systemctl status myapp
sudo ls -la /run/myapp/gunicorn.sock
sudo journalctl -u myapp -n 100 --no-pager
sudo tail -n 100 /var/log/nginx/error.log

Static files return 404

Run collectstatic, verify STATIC_ROOT, confirm the Nginx alias ends with a slash, and check directory permissions along the full path.

DisallowedHost or CSRF failures

Verify the exact public hostnames in ALLOWED_HOSTS and HTTPS origins in CSRF_TRUSTED_ORIGINS. Confirm that Nginx forwards the original host and protocol headers.

Application fails after reboot

Confirm the systemd unit is enabled, the environment file is readable by the service, dependencies use absolute paths, and any required database or network dependency is available.

Manage Python hosting from one control plane

Command-line deployment teaches you exactly how the stack works. As the number of applications grows, centralizing routine operations reduces context switching and helps teams apply a consistent process.

Core Panel for Python hosting brings websites, application runtimes, domains, TLS, files, databases, logs, backups, access controls, and server health into one browser dashboard. Review the Node.js and Python application documentation to understand the available workflow, and use the manual architecture in this guide as the basis for evaluating any control panel.

Final takeaway

A production Django deployment is more than starting Gunicorn. The dependable setup is a chain of clear responsibilities: DNS routes the domain, Nginx handles public traffic and static content, Gunicorn serves Django, systemd supervises the process, and your release and backup procedures keep changes recoverable.

Build the smallest setup that meets your workload, document it, test failure and recovery, and improve it with evidence from real traffic.

Related posts

Stay in the loop

New posts and release notes, delivered to your inbox. No spam.

Ready to switch?
14-day free trial
Get Started