Sign in Get started
Solutions
ComplianceDigital ForensicsAccount SecurityWebsite SecurityWeb OptimizationSEO / GEOBusiness IntelligenceUI / UXStartupsDigital TransformationWhite LabelAI Code AuditAI / MLConsulting & AdvisoryBusiness Strategy
Products
Web HostingVPS HostingSSL CertificatesSiteLock SecurityWebsite BackupEmail Spam FilterOX App Suite360 MonitoringWeebly BuilderSite BuilderSEO (Marketgoo)Account SecurityDomainsAll Products
More
NetworkSecurity AboutCareersContactRoadmapLegal
Blog

Deploying a React or Django app to a Linux server

Older guides for deploying React or Django often rely on tools that have since been retired, such as Create React App or hosting control panels with a one-click Python button. This guide shows the current way to put each one on a plain Linux server with nginx. The steps work on any recent Ubuntu or Debian server.

React: start with Vite, not Create React App

The React team deprecated Create React App in February 2025. For new projects, react.dev recommends a framework such as Next.js or React Router, or a build tool such as Vite when you want a plain single-page app. This guide covers the Vite case, because it produces static files that any web server can host.

Vite needs a recent Node.js (its guide lists 20.19 or 22.12 as the minimum). Use a current long-term support release on your machine or build server:

npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run build

The build writes everything to a dist/ folder: an index.html and an assets/ folder with file names that change whenever their content changes.

How a Vite React app is deployed1. Buildnpm run build on yourmachine or in CI2. dist/Plain HTML, CSS andJavaScript files3. Copyrsync the folderto the server4. Servenginx serves files,else index.html1. Buildnpm run build on your machine or in CI2. dist/Plain HTML, CSS and JavaScript files3. Copyrsync the folder to the server4. Servenginx serves files, else index.html
A single-page React app is a folder of static files. The server needs no Node.js.

React: environment values are public

Vite only passes variables that start with VITE_ into your code, and it writes their values into the JavaScript files at build time. Anyone can read them in the browser. Put API addresses there, never secret keys. Anything secret belongs on a server.

React: copy the files and configure nginx

rsync -av --delete dist/ deploy@server:/var/www/my-app/

A single-page app handles its own routes in the browser. If a visitor reloads /orders/7, the server has no such file and returns a 404, unless you tell it to fall back to index.html. That is what try_files does here:

server {
    listen 443 ssl;
    server_name app.example.com;
    root /var/www/my-app;

    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location / {
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "no-cache";
    }
}

The hashed files in /assets/ can be cached for a year, because a new build gives them new names. index.html should not be cached, so visitors always get the latest version. If your app lives in a sub-folder, set Vite's base option to that path before you build. Add a free HTTPS certificate with Let's Encrypt (for example with certbot), and the React side is done.

Django: pick a supported version

From the Django download page, these series are supported today:

Django seriesLatest releaseSecurity fixes until
5.2 LTS5.2.18April 2028
6.06.0.9April 2027
6.16.1.2December 2027

For an app you will run for years, use 5.2 LTS. It supports Python 3.10 to 3.14. The 6.x series needs Python 3.12 or later. Stay on the latest patch release of whichever series you choose.

Django: settings come from the environment

Keep the same code on every machine and change only the environment. Read secrets from environment variables, turn debug off, and list the host names the app answers to:

# settings.py
import os

DEBUG = os.environ.get("DJANGO_DEBUG") == "1"
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
ALLOWED_HOSTS = ["app.example.com"]
CSRF_TRUSTED_ORIGINS = ["https://app.example.com"]
STATIC_ROOT = "/var/www/my-site/static"
SECURE_PROXY_SSL_HEADER = (
    "HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

SECURE_PROXY_SSL_HEADER tells Django that nginx already handled HTTPS. Only set it when nginx is the only way in, because it trusts the header nginx sends.

Django: install, check and collect static files

On the server, create a non-root user for the app, put the code in a folder such as /srv/mysite, and run:

python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt gunicorn
python manage.py check --deploy
python manage.py migrate
python manage.py collectstatic --noinput

check --deploy runs Django's own deployment checklist and warns about unsafe settings. collectstatic copies CSS, JavaScript and images into STATIC_ROOT, where nginx serves them directly. Files that users upload (media) need their own folder and are never served from the code directory.

Django: run Gunicorn with systemd

Never use manage.py runserver in production. Run an application server instead. Gunicorn is the common choice for standard Django code; if your app relies on async views or WebSockets, the Django docs cover ASGI servers such as Uvicorn and Daphne. systemd starts Gunicorn at boot and restarts it if it stops:

# /etc/systemd/system/mysite.service
[Unit]
Description=mysite gunicorn
After=network.target

[Service]
User=deploy
WorkingDirectory=/srv/mysite
EnvironmentFile=/etc/mysite.env
ExecStart=/srv/mysite/.venv/bin/gunicorn \
  --workers 3 \
  --bind unix:/run/mysite/gunicorn.sock \
  mysite.wsgi:application
RuntimeDirectory=mysite
Restart=always

[Install]
WantedBy=multi-user.target

Put the secrets in /etc/mysite.env, readable only by root, then run systemctl enable --now mysite. After each deploy, run the migrate and collectstatic steps, then systemctl restart mysite.

How a request reaches a Django appBrowserHTTPS requestto your domainnginxEnds HTTPS, serves/static/ itselfGunicornRuns Django, keptalive by systemdDjangoYour code andthe databaseBrowserHTTPS request to your domainnginxEnds HTTPS, serves /static/ itselfGunicornRuns Django, kept alive by systemdDjangoYour code and the database
nginx handles the outside world. Gunicorn only talks to nginx, over a local socket.

Django: put nginx in front

server {
    listen 443 ssl;
    server_name app.example.com;

    location /static/ {
        alias /var/www/my-site/static/;
    }

    location / {
        proxy_pass http://unix:/run/mysite/gunicorn.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For
            $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Add HTTPS the same way as for the React app. Check the logs with journalctl -u mysite if a page returns a 502; it usually means Gunicorn is not running or the socket path does not match.

Common questions

Do I need Node.js on the server for a Vite app?
No. You build on your machine or in CI, and the server only serves the files in dist/.
Why do my React routes return 404 after a refresh?
The server is looking for a file with that path. Add the try_files fallback to index.html.
Can I serve Django static files without nginx?
Yes. WhiteNoise lets Django serve them efficiently, which helps on platforms without a separate web server. With nginx in place, let nginx do it.
How many Gunicorn workers should I run?
Gunicorn's documentation suggests two workers per CPU core plus one as a starting point. Adjust after watching memory and response times under real traffic.

Shipping apps like these for clients? Getting paid on Internetivo explains how developers find paid tasks and get paid for accepted work.