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.
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 series | Latest release | Security fixes until |
|---|---|---|
| 5.2 LTS | 5.2.18 | April 2028 |
| 6.0 | 6.0.9 | April 2027 |
| 6.1 | 6.1.2 | December 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.
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.