---
title: "Host Django on a VPS with Gunicorn, Nginx and PostgreSQL | StreetHosting"
description: "Put a Django project into production on an Ubuntu VPS: venv, safe settings, Gunicorn as a systemd service, Nginx serving static files, HTTPS and PostgreSQL."
url: "https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx"
type: "page"
language: "en-US"
---

VPS · 8 min · Intermediate

Published on Nov 13, 2026 · Updated on Nov 13, 2026

# How to host a Django project on a VPS with Gunicorn, Nginx and PostgreSQL

runserver is not meant for production. Here is the full path on a VPS: virtual environment, production settings, Gunicorn under systemd, Nginx serving /static/ and HTTPS with certbot.

By [Equipe StreetHosting](https://streethosting.com.br/en/autores#equipe-streethosting) · StreetHosting infrastructure and support team

[Linux administration](https://streethosting.com.br/en/guides/topics/linux) [Databases](https://streethosting.com.br/en/guides/topics/databases) [Network, DNS and domains](https://streethosting.com.br/en/guides/topics/networking)

Summarize with:

[](https://chat.openai.com/?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fhost-django-vps-gunicorn-nginx.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "ChatGPT") [](https://claude.ai/new?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fhost-django-vps-gunicorn-nginx.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Claude") [](https://www.google.com/search?udm=50&aep=11&q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fhost-django-vps-gunicorn-nginx.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Google AI Mode") [](https://x.com/i/grok?text=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fhost-django-vps-gunicorn-nginx.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Grok") [](https://www.perplexity.ai/search/new?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fhost-django-vps-gunicorn-nginx.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Perplexity")

Share:

[](https://x.com/intent/tweet?text=Host%20Django%20on%20a%20VPS%20with%20Gunicorn%2C%20Nginx%20and%20PostgreSQL&url=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fhost-django-vps-gunicorn-nginx "Share on X") [](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fhost-django-vps-gunicorn-nginx "Share on Facebook") [](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fhost-django-vps-gunicorn-nginx "Share on LinkedIn") [](https://wa.me/?text=Host%20Django%20on%20a%20VPS%20with%20Gunicorn%2C%20Nginx%20and%20PostgreSQL%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fhost-django-vps-gunicorn-nginx "Share on WhatsApp")

For agents: Copy as Markdown [.md](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx.md)

In this guide 8 sections

* [How the pieces fit together](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#visao-geral)
* [Prepare the VPS and the virtual environment](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#preparar)
* [Production settings](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#settings)
* [PostgreSQL](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#postgresql)
* [Gunicorn as a systemd service](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#gunicorn)
* [Nginx and HTTPS](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#nginx)
* [Updating and troubleshooting](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#atualizar)
* [Where to run this](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#onde-rodar)

Quick answer

In production Django runs like this: **Gunicorn** executes the project as a systemd service, listening on `127.0.0.1:8000`; **Nginx** receives the traffic, serves the `/static/` folder straight from disk and passes everything else to Gunicorn; certbot handles HTTPS. In the project, keep `DEBUG = False`, list the domain in `ALLOWED_HOSTS`, read the `SECRET_KEY` from an environment variable and run `migrate` and `collectstatic`.

## How the pieces fit together[](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#visao-geral)

A Django site in production has four layers, each with a single job:

| Piece      | Role                                                                         | Listens on                           |
| ---------- | ---------------------------------------------------------------------------- | ------------------------------------ |
| Nginx      | Takes public traffic, does HTTPS, serves static files and passes the rest on | Ports 80 and 443, open to the public |
| Gunicorn   | Runs the Django code, with several processes                                 | 127.0.0.1:8000, local only           |
| Django     | Your application, handed to Gunicorn through the wsgi module                 | Inside Gunicorn                      |
| PostgreSQL | Stores the data                                                              | 127.0.0.1:5432, local only           |

Gunicorn and the database are tied to the local address on purpose: only Nginx is exposed to the internet. The examples use Ubuntu 24.04, the `meusite` project and the `meusite.com.br` domain. Swap in your own.

## Prepare the VPS and the virtual environment[](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#preparar)

Log in to the VPS over SSH with a regular user that has sudo (here, the `deploy` user) and install what you need:

`sudo apt update sudo apt install -y python3 python3-venv python3-dev build-essential git nginx # project folder outside /home, so Nginx can read the static files sudo mkdir -p /srv/meusite sudo chown deploy:deploy /srv/meusite`

Put the code in `/srv/meusite`, with `git clone` or over SFTP, so that `manage.py` sits directly inside it. Then create the virtual environment and install the dependencies:

`cd /srv/meusite python3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install -r requirements.txt`

Your `requirements.txt` needs, besides Django, Gunicorn and the PostgreSQL driver. Generate the file on your development computer with `pip freeze` or write it by hand:

`Django>=5.0 gunicorn psycopg[binary]`

If the installer complains about a package that needs compiling, the `build-essential` and `python3-dev` packages, already installed above, cover most cases.

## Production settings[](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#settings)

What most often breaks a Django deploy is taking the development `settings.py` to the VPS. Adjust these points in the project file, reading secrets from the environment instead of writing them in the code:

`import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] DEBUG = os.environ.get("DJANGO_DEBUG", "0") == "1" ALLOWED_HOSTS = os.environ["DJANGO_ALLOWED_HOSTS"].split(",") CSRF_TRUSTED_ORIGINS = [ "https://meusite.com.br", "https://www.meusite.com.br", ] DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": os.environ["DB_NAME"], "USER": os.environ["DB_USER"], "PASSWORD": os.environ["DB_PASSWORD"], "HOST": "127.0.0.1", "PORT": "5432", } } STATIC_URL = "static/" STATIC_ROOT = BASE_DIR / "staticfiles" # Nginx ends the HTTPS and tells Django through this header SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True`

* **DEBUG:** with the variable missing it stays off, which is the safe behavior. Left on in production, an error page hands out the code, the variables and the settings.
* **SECRET\_KEY:** signs sessions and tokens. If it leaks, whoever has the key can forge sessions. Never put it in Git.
* **ALLOWED\_HOSTS:** the list of domains the site accepts. Outside it, Django returns a 400 error.
* **CSRF\_TRUSTED\_ORIGINS:** origins accepted for forms and login. Since Django 4, each entry must include the scheme, like https://. Declaring the domain here avoids a 403 error on the admin login when Django cannot confirm on its own, from what the proxy passes along, that the origin matches the site.
* **STATIC\_ROOT:** the folder where `collectstatic` copies every static file, the admin ones included. This is the folder Nginx will serve.

Now create the file with the variables. It lives outside the project, at `/etc/meusite.env`, so it never ends up in a commit. Generate the secret key with Python and use values without spaces or quotes:

`python3 -c "import secrets; print(secrets.token_urlsafe(50))" sudo nano /etc/meusite.env`

`DJANGO_SECRET_KEY=cole_aqui_a_chave_gerada DJANGO_ALLOWED_HOSTS=meusite.com.br,www.meusite.com.br DB_NAME=meusite DB_USER=meusite DB_PASSWORD=troque_por_uma_senha_longa`

`# only root and the deploy user can read it sudo chown root:deploy /etc/meusite.env sudo chmod 640 /etc/meusite.env`

## PostgreSQL[](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#postgresql)

Install PostgreSQL following the guide on [installing PostgreSQL on an Ubuntu VPS](https://streethosting.com.br/en/guides/vps/install-postgresql-ubuntu-vps). With it running, create the user and the database Django will use, with the same data as the variables file:

`sudo -u postgres psql CREATE USER meusite WITH PASSWORD 'troque_por_uma_senha_longa'; CREATE DATABASE meusite OWNER meusite; \q`

The database listens only on 127.0.0.1 by default, and that is how it should stay. With the variables loaded in the terminal, run the setup tasks:

`cd /srv/meusite source venv/bin/activate # loads the variables from the file into the current session set -a source /etc/meusite.env set +a python manage.py check --deploy python manage.py migrate python manage.py collectstatic --noinput python manage.py createsuperuser`

`check --deploy` lists security warnings. Some, like the HTTPS redirect one, only make sense after the certificate step further down.

## Gunicorn as a systemd service[](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#gunicorn)

Test before turning it into a service. With the environment active and the variables loaded, run `gunicorn meusite.wsgi:application --bind 127.0.0.1:8000`. The name before the colon is the project folder, the one that holds `wsgi.py`. If it came up with no errors, stop it with Ctrl+C and create the service, as in the guide on [creating a systemd service](https://streethosting.com.br/en/guides/vps/create-systemd-service-linux):

`sudo nano /etc/systemd/system/meusite.service`

`[Unit] Description=Gunicorn do meusite After=network.target postgresql.service [Service] User=deploy Group=www-data WorkingDirectory=/srv/meusite EnvironmentFile=/etc/meusite.env ExecStart=/srv/meusite/venv/bin/gunicorn meusite.wsgi:application --workers 3 --bind 127.0.0.1:8000 Restart=on-failure RestartSec=3 [Install] WantedBy=multi-user.target`

`sudo systemctl daemon-reload sudo systemctl enable --now meusite sudo systemctl status meusite curl -I http://127.0.0.1:8000`

`enable` makes the service start by itself at boot and `Restart=on-failure` brings it back up if it crashes. About workers: the common rule is two times the number of vCPUs plus one, but on a 2 GB RAM VPS start with 2 or 3 and watch memory.

Instead of port 8000, Gunicorn can also listen on a Unix socket, with `--bind unix:/run/meusite/gunicorn.sock --umask 007` plus `RuntimeDirectory=meusite` in the unit, and Nginx uses `proxy_pass http://unix:/run/meusite/gunicorn.sock;`. The `Group=www-data` line in the unit is what lets Nginx open the socket: the file is created with the www-data group, and umask 007 gives read and write to the group and nothing to others. It is slightly faster and uses no port. To start with, the local port is easier to test with curl and to diagnose.

## Nginx and HTTPS[](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#nginx)

Create the site in Nginx. Point the domain DNS at the VPS IP first, as explained in the guide on [pointing a domain at your VPS](https://streethosting.com.br/en/guides/vps/point-domain-to-vps). The file is `/etc/nginx/sites-available/meusite`:

`server { listen 80; server_name meusite.com.br www.meusite.com.br; client_max_body_size 20M; location /static/ { alias /srv/meusite/staticfiles/; expires 30d; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $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; } }`

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

* **The alias ends with a slash**, just like the `location` path. Without the final slash Nginx builds the wrong path and the files return 404.
* **Proxy and HTTPS:** the `X-Forwarded-Proto` header is the one the settings `SECURE_PROXY_SSL_HEADER` expects. Without it Django thinks the connection is plain HTTP, and secure cookies and the CSRF check cause trouble.
* **client\_max\_body\_size** limits upload size. The Nginx default is 1 MB, too little for anyone uploading images.

For HTTPS, install the certificate with certbot, as shown in the guide on [a free SSL certificate with Let's Encrypt on Nginx](https://streethosting.com.br/en/guides/vps/lets-encrypt-ssl-certificate-vps). The command is usually `sudo certbot --nginx -d meusite.com.br -d www.meusite.com.br`. It edits the site file, adds the 443 block and the redirect from 80. Until this step, the admin login over HTTP does not keep the session, because the settings mark the cookies as secure. That is expected. For the concepts behind the proxy, see also the guide on [Nginx as a reverse proxy](https://streethosting.com.br/en/guides/vps/nginx-reverse-proxy-vps).

With HTTPS up, run `check --deploy` again. If you like, turn on `SECURE_HSTS_SECONDS` in the settings, starting with a low value, because HSTS makes the browser remember HTTPS for that long.

## Updating and troubleshooting[](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#atualizar)

With every new version of the code, the sequence is always the same:

`cd /srv/meusite git pull source venv/bin/activate pip install -r requirements.txt set -a source /etc/meusite.env set +a python manage.py migrate python manage.py collectstatic --noinput sudo systemctl restart meusite`

When something will not open, start with the service log, using `sudo journalctl -u meusite -n 50 --no-pager`, and with the Nginx log at `/var/log/nginx/error.log`. The most common symptoms:

| Symptom                   | Likely cause                                                                                                                        |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| 502 Bad Gateway           | Gunicorn is not running or the proxy\_pass port is wrong. Check status and journalctl. The Nginx 502 guide goes into the diagnosis. |
| 400 Bad Request           | The domain is not in ALLOWED\_HOSTS.                                                                                                |
| 403 on login or on a form | CSRF\_TRUSTED\_ORIGINS is missing https://, or Nginx is not sending X-Forwarded-Proto.                                              |
| Admin without CSS         | collectstatic was not run, or the /static/ alias points to the wrong folder.                                                        |
| 500 with no detail        | A code error. The traceback is in journalctl; never turn DEBUG on to find it.                                                       |
| Database connection error | Wrong DB variables in the environment file, or the PostgreSQL user is not the owner of the database.                                |

The environment file is read only when the service starts. After editing `/etc/meusite.env`, run `sudo systemctl restart meusite`, or the change looks like it had no effect. For the 502 error in detail, see the guide on [the 502 Bad Gateway error in Nginx](https://streethosting.com.br/en/guides/vps/nginx-502-bad-gateway-error).

## Where to run this[](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#onde-rodar)

This path requires installing packages, creating a systemd service and editing the Nginx configuration, all with root. On [StreetHosting VPS](https://streethosting.com.br/en/vps) you get root access on a KVM server with its own kernel, so you can follow the guide as written. All of them are in São Paulo, with AntiDDoS included, and activate in about 60 seconds after Pix or card approval.

A small Django project with PostgreSQL runs well on the Xeon VPS from R$ 26.00 per month, with 2 vCPU, 2 GB DDR4 and 20 GB NVMe. Memory is the resource to watch: each Gunicorn worker and PostgreSQL use RAM. Check with `free -h` after the deploy and adjust the number of workers to the headroom.

In this guide

* [How the pieces fit together](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#visao-geral)
* [Prepare the VPS and the virtual environment](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#preparar)
* [Production settings](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#settings)
* [PostgreSQL](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#postgresql)
* [Gunicorn as a systemd service](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#gunicorn)
* [Nginx and HTTPS](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#nginx)
* [Updating and troubleshooting](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#atualizar)
* [Where to run this](https://streethosting.com.br/en/guides/vps/host-django-vps-gunicorn-nginx#onde-rodar)

## Frequently asked questions

Why can I not use runserver in production?

runserver was built for development. The Django docs themselves warn that it has not gone through security audits or been designed for performance, and it restarts on its own when files change. In production Gunicorn receives the requests for the app and Nginx sits in front, handling HTTPS, static files and slow connections.

How many Gunicorn workers should I use?

The common rule is two times the number of vCPUs plus one. On a small VPS, 2 or 3 workers are enough. Each worker is a process with the app loaded in memory, so check RAM usage before raising it.

Django does not show my admin CSS and images. What should I do?

With DEBUG off, Django does not serve static files. Set STATIC\_ROOT, run collectstatic and have Nginx serve that folder at the /static/ path. If the page opens but the styling does not load, it is almost always the Nginx alias pointing to the wrong folder or the folder lacking read permission.

I get a 400 Bad Request error when opening the site. Is it Nginx?

Probably Django. With DEBUG off, it refuses any Host that is not in ALLOWED\_HOSTS. Add the domain, with and without www, and restart Gunicorn. The service log shows the DisallowedHost message.

Can I use SQLite in production with Django?

For a small site with few writes it works, and backup means copying a file. When there are many simultaneous writes or the project grows, PostgreSQL is safer and allows backups without stopping the app.

Next step

See VPS plans

Root VPS in Brazil with NVMe and Anti-DDoS.

[See VPS plans](https://streethosting.com.br/en/vps)

## Related guides

[VPS Intermediate How to set up Nginx as a reverse proxy on a VPS Your app runs on an internal port and you want to serve it on a domain with HTTPS. Nginx as a reverse proxy solves that and also brings several apps together in one place. 3 min Read guide](https://streethosting.com.br/en/guides/vps/nginx-reverse-proxy-vps) [VPS Intermediate How to install PostgreSQL on an Ubuntu VPS securely PostgreSQL is the most complete open source relational database and the default for many modern applications. Learn how to install it on an Ubuntu VPS, create a user and database for your application, understand authentication, allow remote access without exposing the port, schedule backups and tune memory. 10 min Read guide](https://streethosting.com.br/en/guides/vps/install-postgresql-ubuntu-vps) [VPS Beginner SSL certificate on a VPS with Let's Encrypt and Nginx A site without the padlock loses trust and rankings. This guide shows how to issue, renew and audit a free SSL certificate on your VPS with Nginx and Certbot. 4 min Read guide](https://streethosting.com.br/en/guides/vps/lets-encrypt-ssl-certificate-vps)

[← Back to the Guide Center](https://streethosting.com.br/en/guides)
