We are going to get you off of localhost.
Dynamic web pages or apps are interactive and will change based on user input or database state.
The “front end” of a dynamic web app is the process responsible for generating and updating the user interface. It does this by communicating with the “back end” of the web app, which manages data storage and business logic while responding to the front end’s requests.
This guide will show you how to host a dynamic web page. We will use the tried and true Django back-end framework and the ubiquitous React front-end framework as concrete examples, but we will also discuss general principles that apply to the myriad other web dev frameworks.
For the rest of this guide, I will assume that you have basic Django and React applications set up and running locally. Follow those links for the official tutorials if you need help getting started.
I will also assume that you have a Virtual Private Server (VPS) set up and ready to go and a domain name purchased and pointed at your VPS. If you don’t have these set up yet, I recommend reading and following my previous post on setting up a VPS and domain name for hosting static sites.
Python comes stock with venv
, a module for creating lightweight virtual environments. You are running this on a resource-constrained (cheap) VPS, so you should keep your venv lean.
From your dev environment, create a requirements.txt file by running pip freeze > requirements.txt
. Copy this to your server and create a venv there with:
python3 -m venv yourvenv
source yourvenv/bin/activate
pip install -r requirements.txt
You should namespace all of your back end endpoints with some common prefix so they don’t clash with your front end’s urls (e.g., /api/v1/whatever
).
Later, this will allow us to route front-end requests and back-end requests to separate processes more easily.
settings.py
Because your back end and front end are separate processes and will have their own origins, you will need to configure a few things in your settings.py so your back end can serve requests from your front end. If you do not do these things, then your back end cannot answer requests from your front end.
Cross-site request forgery (CSRF) is an exploit where a site’s browser is tricked into sending a request to a different site. This can cause the user to unwittingly submit forms on some other site (like their bank) or expose user information. To prevent this, browsers block cross-origin requests by default. However, because your front end and back end are on different origins, you will need to explicitly allow requests from your front end to your back end.
Cross-origin resource sharing (CORS) is a feature that permits browsers to make limited cross-origin requests. To enable your cross-origin requests, do the following:
django-cors-headers
package:
pip install django-cors-headers
corsheaders
to your INSTALLED_APPS
in settings.py
:
INSTALLED_APPS = [
# ...existing code...
'corsheaders',
# ...existing code...
]
CorsMiddleware
to your MIDDLEWARE
in settings.py
, making sure it’s placed above CommonMiddleware
:
MIDDLEWARE = [
# ...existing code...
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
# ...existing code...
]
settings.py
:
CORS_ALLOWED_ORIGINS = [
"https://your_domain.com",
"http://localhost:3000", # for local development
]
settings.py
:
CSRF_TRUSTED_ORIGINS = [
"https://your_domain.com",
"http://localhost:3000", # for local development
]
To allow your server to be hosted on your domain, add the following to settings.py
:
ALLOWED_HOSTS = [
"your_domain.com",
"localhost", # for local development
]
Django’s secret key is used for cryptographic signing. You should set it in a .env
file and load it in settings.py
. We will tell Gunicorn which .env
file to use in the next section.
Gunicorn is a popular HTTP server for Python applications such as those built with Django or Flask. It receives HTTP requests from clients, forwards them to the Python process running your app, and forwards the Python app’s response to the the client. It can respond to multiple requests concurrently.
Install it with pip install gunicorn
.
Make sure that all project files are owned by the user that will run the Gunicorn process (usually www-data
on Ubuntu). You can change the ownership with:
sudo chown -R www-data:www-data /var/www/yourproject
Test it with:
gunicorn your_project.wsgi:application
But, this is not how you should run Gunicorn on your server. Instead, you should use a service that will spin up Gunicorn when it is needed to serve a request and manage the process for you. systemd
is the best choice for this.
To configure systemd
to manage your Gunicorn process, we will create two files. systemd.service
files define how the service will be controlled. systemd.socket
files define how the service will be started.
systemd.socket
file for Gunicorn:
# filepath: /etc/systemd/system/yoursite.socket
[Unit]
Description=gunicorn socket
# Direct Gunicorn requests to this socket file
[Socket]
ListenStream=/run/yoursite.sock
# Create this socket on boot
[Install]
WantedBy=sockets.target
Make sure to replace yoursite
with the actual name of your project.
systemd.service
file for Gunicorn:
# filepath: /etc/systemd/system/yoursite.service
[Unit]
Description=gunicorn daemon
# Require the file we created in step 1
Requires=yoursite.socket
After=network.target
[Service]
# Allow this service to access back end files
User=www-data
Group=www-data
# Set base folder to the one that contains your Django project
WorkingDirectory=/var/www
#
ExecStart=/var/www/your-venv/bin/gunicorn \
--access-logfile - \ # log to stdout
--workers 3 \ # number of worker processes
--bind unix:/run/yoursite.sock \ # This is the socket file from step 1 defined with ListenStream
--access-logfile /var/log/gunicorn/yoursite.access.log \
--error-logfile /var/log/gunicorn/yoursite.error.log \
yoursite.wsgi:application
# If you have environment variables
EnvironmentFile=/var/www/yoursite/.env
[Install]
WantedBy=multi-user.target
Make sure to replace yoursite
with the actual name of your project, set your correct WorkingDirectory
and EnvironmentFile
, and pass the correct virtual environment path in ExecStart
.
sudo systemctl start yoursite.socket
sudo systemctl enable yoursite.socket
sudo systemctl status yoursite.socket
You should see something like this:
● yoursite.socket - gunicorn socket
Loaded: loaded (/etc/systemd/system/yoursite.socket; enabled; preset: enabled)
Active: active (running) since Tue 2025-09-02 22:12:07 UTC; 7s ago
Triggers: ● yoursite.service
Listen: /run/yoursite.sock (Stream)
CGroup: /system.slice/yoursite.socket
file /run/yoursite.sock
You should see something like this:
/run/yoursite.sock: socket
sudo journalctl -u yoursite.socket
curl --no-buffer -XGET --unix-socket /run/yoursite.sock localhost/api/endpoint/
and you should see the HTTP response from your endpoint.
npm install
Your resource-constrained VPS will have very little RAM, so you need to keep your environment lean. This is where the super-cheap VPS approach can easily fail. Luckily, we can use the --production
flag to skip dev dependencies:
npm install --production
If you see weird errors or if this run takes a very long time, you are probably coming up against your VPS’s RAM constraints.
We can get around this by using swap. Swap is disk memory that you can use to emulate RAM. Accessing swap is of course slower than accessing real RAM, but if you only need it for this one-time install, users will never see a slowdown from it.
Alternatively, install and build your app on your local machine and then transfer the build artifacts to your VPS.
npm run build
Once your dependencies are installed, you can build your React app:
npm run build
This will create a build
directory containing your compiled web app’s files. If you are building on your local machine, just transfer the build
directory to your VPS:
zip -r build.zip /path/to/your/build # create a zip archive of your site
scp build.zip username@vps_ip_address:/var/www/yoursite # copy the zip file to the VPS
Reverse proxies are intermediate servers that route client requests to the appropriate backend server. We will use Nginx as a reverse proxy to either forward user requests to the Gunicorn socket or serve the React app’s static files.
sudo apt update
sudo apt install nginx
sudo nano /etc/nginx/sites-available/yoursite
# Each server block defines a virtual server's routing behavior
# This server handles HTTP requests by redirecting them to HTTPS
server {
# HTTP connections come in on port 80
listen 80;
server_name your_domain.com www.your_domain.com;
# Redirect those HTTP requests to HTTPS
return 301 https://$host$request_uri;
}
# This server handles HTTPS requests
server {
# HTTPS connections come in on port 443
listen 443 ssl;
server_name your_domain.com www.your_domain.com;
# SSL certificate files - tell Nginx where to find proof that the domain is yours
ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
# Set logs
access_log /var/log/nginx/yoursite.access.log;
error_log /var/log/nginx/yoursite.error.log;
# Root directory for the website: tells Nginx where to find the files to serve
root /var/www/yoursite;
# Default file to serve
index index.html;
# This location block forwards API requests to your Gunicorn process
location /api/ {
include proxy_params;
proxy_pass http://unix:/run/yoursite.sock;
}
# All other requests will be served with static files
# This makes any file in /var/www/yoursite accessible, so make sure not to expose sensitive files
# Any file that is not found will return a 404 error
location / {
try_files $uri $uri/ =404;
}
}
Replace your_domain.com
with your actual domain and set the correct paths to your SSL certificate files.
Make sure not to expose sensitive files in your web root directory!
sudo ln -s /etc/nginx/sites-available/yoursite /etc/nginx/sites-enabled/
This creates a symbolic link between your site’s configuration file in sites-available
and the sites-enabled
directory, which Nginx uses to determine which sites to serve.
sudo nginx -t
sudo systemctl reload nginx
We set up logging for both Nginx and Gunicorn to monitor the requests they are serving and any errors that pop up.
# ...
# Set logs
access_log /var/log/nginx/yoursite.access.log;
error_log /var/log/nginx/yoursite.error.log;
# ...
--access-logfile
and --error-logfile
options in the systemd.service
file:
# ...
--access-logfile /var/log/gunicorn/yoursite.access.log
--error-logfile /var/log/gunicorn/yoursite.error.log
# ...
You can access these logs with either cat
or tail
. For instance:
# View the last 100 lines of the access log
tail -n 100 /var/log/nginx/yoursite.access.log
# View all of the Gunicorn error log
cat /var/log/gunicorn/yoursite.error.log