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 myappand 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:
- Back up the database and confirm the backup is restorable.
- Fetch the reviewed release.
- Install locked dependencies.
- Run tests and
manage.py check --deploy. - Review and apply database migrations.
- Collect static files.
- Restart Gunicorn and reload Nginx only when its configuration changed.
- Run health checks and a short smoke test.
- 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=Falseand 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.



