Talk to a real engineer, 24×7 — +91 75994 50220 support@bigdomainhost.com
Guide

How to deploy a Flask, Django or FastAPI app on an Ubuntu VPS

This guide takes a Python web app from a Git repository to a live HTTPS site on Ubuntu 24.04, with configs you can copy and the fixes for the errors you are most likely to hit.

Short answer: Create a non-root user, install the app in a Python virtual environment, and run it with Gunicorn (Flask, Django) or Gunicorn with uvicorn_worker.UvicornWorker (FastAPI) as a systemd service on a Unix socket or 127.0.0.1 port. Put Nginx in front to proxy requests and serve static files, open ports 80 and 443, and add HTTPS with Certbot.

The short version: an app server behind Nginx, kept alive by systemd

A Python web app on a VPS runs as three layers. Your code runs inside an application server: Gunicorn for WSGI frameworks (Flask, Django), or Gunicorn with Uvicorn workers for ASGI frameworks (FastAPI). systemd starts that process at boot and restarts it if it dies. Nginx faces the internet on ports 80 and 443, terminates HTTPS, serves static files and passes everything else to the app over a Unix socket or a port on 127.0.0.1. Never expose Gunicorn or Uvicorn directly to the public internet.

The steps below assume a fresh Ubuntu 24.04 server, a domain whose A record already points at the server's IPv4 address, and an app in a Git repository. Replace myapp, deploy and example.in with your own names.

Quick checklist

  1. Create a non-root user and log in with an SSH key.
  2. Install python3-venv, nginx and Git; create a virtual environment.
  3. Put secrets in an environment file readable only by root.
  4. Install the app and Gunicorn (plus uvicorn-worker for FastAPI) inside the venv.
  5. Write a systemd unit and start it.
  6. Write an Nginx server block that proxies to the socket or port.
  7. Open ports 80 and 443, then run Certbot for HTTPS.

1. Create a user and install packages

Log in as root once, create a working user and give it sudo:

adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

Log in again as deploy and install the system packages. Ubuntu 24.04 ships Python 3.12, but the venv module needs a separate package:

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

2. Keep secrets in an environment file

Do not commit secret keys or database passwords to Git. Put them in a file that systemd reads at start-up:

sudo mkdir -p /etc/myapp
sudo nano /etc/myapp/env

Add one KEY=value per line; wrap a value in single quotes if it contains spaces or characters such as $ or #:

DJANGO_SECRET_KEY='change-me'
DATABASE_URL='postgres://myapp:password@127.0.0.1:5432/myapp'

Save the file, then make it readable by root only:

sudo chmod 600 /etc/myapp/env

systemd reads the file as root before starting your process, so it can stay owned by root with mode 600. Your code reads the values with os.environ["DJANGO_SECRET_KEY"]. Commands you run by hand, such as a test run or manage.py migrate, need the same variables; load them into your current shell with:

set -a; source <(sudo cat /etc/myapp/env); set +a

3. Install the app in a virtual environment

cd ~
git clone https://github.com/you/myapp.git
cd myapp
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt gunicorn

For FastAPI, also install the worker package. Uvicorn's own documentation marks its built-in uvicorn.workers module as deprecated and points to the separate uvicorn-worker package, whose class is uvicorn_worker.UvicornWorker:

pip install fastapi uvicorn uvicorn-worker

Test the app by hand before writing any service file. Load the environment file as shown in step 2, run for example venv/bin/gunicorn --bind 127.0.0.1:8000 myproject.wsgi for Django, then run curl -I -H "Host: example.in" http://127.0.0.1:8000 from a second SSH session. The Host header matters for Django: with DEBUG = False, a request for 127.0.0.1 that is not in ALLOWED_HOSTS gets a 400.

Django: settings and static files

In production settings, set DEBUG = False, list your hostnames in ALLOWED_HOSTS, and tell Django to trust the scheme header Nginx sends so it knows requests arrived over HTTPS:

ALLOWED_HOSTS = ["example.in", "www.example.in"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
STATIC_URL = "static/"
STATIC_ROOT = "/var/www/myapp/static/"

Gunicorn does not serve static files; Nginx does. Ubuntu home directories are not readable by other users by default, so keep STATIC_ROOT outside /home:

sudo mkdir -p /var/www/myapp/static
sudo chown deploy:deploy /var/www/myapp/static
set -a; source <(sudo cat /etc/myapp/env); set +a
python manage.py migrate
python manage.py collectstatic --noinput

Run collectstatic again after every deploy that changes CSS, JavaScript or images.

4. Run the app with systemd

Flask or Django: Gunicorn on a Unix socket

Gunicorn's deployment documentation uses a pair of units: a socket unit that creates /run/myapp.sock with Nginx's user as owner, and a service unit that runs Gunicorn as your own user. Create /etc/systemd/system/myapp.socket:

[Unit]
Description=myapp socket

[Socket]
ListenStream=/run/myapp.sock
SocketUser=www-data
SocketGroup=www-data
SocketMode=0660

[Install]
WantedBy=sockets.target

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

[Unit]
Description=myapp gunicorn
Requires=myapp.socket
After=network.target

[Service]
Type=notify
NotifyAccess=main
User=deploy
Group=deploy
WorkingDirectory=/home/deploy/myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/home/deploy/myapp/venv/bin/gunicorn --workers 3 myproject.wsgi
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
Restart=on-failure

[Install]
WantedBy=multi-user.target

For Flask, replace myproject.wsgi with your module and app object, such as app:app. Gunicorn picks up the socket systemd hands it, so no --bind is needed. Its documentation suggests 2 to 4 workers per CPU core as a starting point; on a small VPS, memory usually runs out first, so see how much RAM a VPS needs before raising it.

FastAPI: Gunicorn with Uvicorn workers on a local port

The same layout works for FastAPI with a socket. To show the other option, this unit binds to a loopback port instead, which needs no socket unit. Create its environment file at /etc/myapi/env the same way as in step 2, then write /etc/systemd/system/myapi.service:

[Unit]
Description=myapi uvicorn
After=network.target

[Service]
User=deploy
Group=deploy
WorkingDirectory=/home/deploy/myapi
EnvironmentFile=/etc/myapi/env
ExecStart=/home/deploy/myapi/venv/bin/gunicorn main:app -k uvicorn_worker.UvicornWorker --workers 2 --bind 127.0.0.1:8001
Restart=on-failure

[Install]
WantedBy=multi-user.target

Binding to 127.0.0.1 rather than 0.0.0.0 keeps the port unreachable from outside. Start everything:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp.socket myapp.service
sudo systemctl enable --now myapi.service

5. Configure Nginx as the reverse proxy

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

upstream myapp_server {
    server unix:/run/myapp.sock fail_timeout=0;
    # FastAPI example: server 127.0.0.1:8001 fail_timeout=0;
}

server {
    listen 80;
    server_name example.in www.example.in;
    client_max_body_size 10m;

    location /static/ {
        alias /var/www/myapp/static/;
    }

    location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
        proxy_pass http://myapp_server;
    }
}

Enable it, disable Nginx's default site, test the syntax and reload. Removing sites-enabled/default deletes only a symlink; the original file stays in sites-available:

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

6. Firewall and HTTPS

Open 80 and 443 at every firewall layer in front of the server. If you use UFW on the server, allow SSH first so you do not lock yourself out:

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

Port 80 must be reachable for Let's Encrypt to validate the domain. Install Certbot from the snap, as its own instructions recommend, and let it edit the Nginx block:

sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot
sudo certbot --nginx -d example.in -d www.example.in
sudo certbot renew --dry-run

Certbot adds the certificate paths and a redirect from HTTP to HTTPS, and installs a timer that renews certificates automatically.

7. Logs, restarts and deploys

  • App logs: sudo journalctl -u myapp.service -f
  • Nginx errors: sudo tail -f /var/log/nginx/error.log
  • Reload Gunicorn workers after a code change: sudo systemctl reload myapp (sends HUP)
  • Full restart after changing the unit or the env file: sudo systemctl daemon-reload && sudo systemctl restart myapp

A typical deploy is git pull, venv/bin/pip install -r requirements.txt, migrations and collectstatic for Django, then a reload.

Common errors and their fixes

SymptomLikely causeFix
502 Bad Gateway, log says (2: No such file or directory)Socket path in Nginx does not match the unit, or the service is not runningCompare ListenStream with the upstream line; run systemctl status myapp
502, log says (13: Permission denied)Nginx's www-data user cannot reach the socket, often because it sits inside /home/deployKeep the socket in /run with SocketUser=www-data
502, log says (111: Connection refused)Nothing is listening on the port, or the app crashed at start-upCheck journalctl -u myapi for an import error or missing env variable
502, log says upstream prematurely closed connection; journal shows WORKER TIMEOUTA sync Gunicorn worker was silent longer than the 30-second default --timeout and was killedMove slow work to a background job; raise --timeout only if needed
504 Gateway TimeoutThe app took longer than Nginx's proxy_read_timeout (60 seconds by default) to respondSpeed up or offload the request; raise proxy_read_timeout and Gunicorn's --timeout together only if needed
400 Bad Request from DjangoHostname missing from ALLOWED_HOSTSAdd it and reload
Pages load without CSScollectstatic not run, or alias path wrongRun it again and check the trailing slashes
Redirect loop after enabling HTTPSApp does not see the original schemeSend X-Forwarded-Proto and set SECURE_PROXY_SSL_HEADER

Running this on Bigdomainhost

A Bigdomainhost KVM VPS gives you full root access and offers Ubuntu 24.04 as an image, so every step above works as written. Paste your SSH key at order time and the server boots with password login disabled; the quickstart covers the first login.

One step differs from a bare Ubuntu install. Each server sits behind a firewall in the control panel that denies all inbound traffic except SSH on port 22, so you need to add rules for ports 80 and 443 there before Nginx or Certbot can be reached; the networking and firewall docs explain where. If a UFW change locks you out, the out-of-band console in the panel still gets you in. Outbound port 25 is blocked by default, so send app email through your mail provider's submission port instead.

Servers run in Mumbai (BOM-1) and Delhi NCR (DEL-1) and are billed in 30-day periods paid in advance from your rupee wallet. For heavier apps there are dedicated servers in the same locations. If you get stuck, call or WhatsApp +91 75994 50220 at any hour, or email support@bigdomainhost.com.

Sources (checked )

Answers

Frequently asked questions

Still unsure? Call +91 75994 50220 or write to support@bigdomainhost.com — a human replies, 24×7.

Should I use Gunicorn or Uvicorn for FastAPI?

Either works. Uvicorn alone can run several processes with its --workers flag, and its built-in process manager restarts workers that die and does a graceful rolling restart on a HUP signal. Many people still run Gunicorn as the process manager with gunicorn -k uvicorn_worker.UvicornWorker from the uvicorn-worker package, because it fits the same systemd socket setup and Gunicorn settings they already use for Flask and Django.

Is uvicorn.workers.UvicornWorker deprecated?

Yes. Uvicorn's documentation says the uvicorn.workers module is deprecated and will be removed in a future release. Install the separate uvicorn-worker package with pip and change the worker class to uvicorn_worker.UvicornWorker. The command line is otherwise the same.

Why does Nginx show 502 Bad Gateway for my Django app?

Nginx could not get a response from Gunicorn. Check /var/log/nginx/error.log: 'No such file or directory' means the socket path is wrong or the service is down, 'Permission denied' means www-data cannot reach the socket, 'Connection refused' means nothing listens on the port, and 'upstream prematurely closed connection' usually means Gunicorn killed a worker that hit its 30-second timeout. Then read journalctl -u for your service.

Should Gunicorn use a Unix socket or a 127.0.0.1 port?

Both are fine when Nginx runs on the same server. A Unix socket avoids TCP overhead and cannot be reached from the network at all, but you have to get file permissions right. A loopback port such as 127.0.0.1:8000 is simpler to debug with curl. Never bind to 0.0.0.0 behind Nginx.

How many Gunicorn workers should I run?

Gunicorn's documentation suggests 2 to 4 workers per CPU core as a starting point, then tuning for your workload. Each worker loads a full copy of your app, so on a VPS with 1 or 2 GB of RAM, memory is usually the limit. Watch free -m under load before adding workers.

Support that picks up the phone.

24×7, from our office in Agra, in IST — Hindi or English. Sales, migration and emergencies all reach the same engineers. No offshore queue, no 48-hour first reply.

Need cPanel or WHM with the ports already sorted?

Web & cPanel hosting comes with cPanel ready to log in to, reseller hosting adds white-label WHM, and a KVM VPS can have cPanel & WHM pre-installed. Call +91 75994 50220 or email support@bigdomainhost.com.