In this guide
One cheap VPS can comfortably run your portfolio site, a Node API for your project, and a Python dashboard for your sensors — at the same time. The catch is that only one program can listen on port 80, the port browsers use for HTTP. A reverse proxy solves exactly this: Nginx takes ownership of ports 80 and 443, reads the domain name in each incoming request, and forwards it to the right app running quietly on its own internal port. This guide walks you through the full setup — one subdomain per app, a clean Nginx config, free HTTPS with Certbot, WebSocket support, and the mistakes that waste most students' evenings.
The problem: one port 80, many apps
Browsers connect to port 80 for HTTP and 443 for HTTPS, and only one process can bind to each port at a time. So the second app you deploy has nowhere to go. The usual student workaround is to expose ports in the URL — http://your-vps-ip:3000 for the API, :5000 for the dashboard. That works, until it doesn't:
- Port numbers in URLs look unfinished and are painful to share in a report or demo.
- College and hostel firewalls often block non-standard ports, so your project "works on my laptop" but not in the lab.
- Getting HTTPS on
https://ip:3000is awkward; browsers warn, and some APIs refuse to talk to insecure origins. - Every new app means another port to remember, another firewall rule, another thing to secure.
Buying a bigger server per app defeats the whole point of a cheap VPS. The professional answer is a reverse proxy sitting in front of everything.
How a reverse proxy works
Nginx binds to ports 80 and 443. Your apps bind to private ports on the same machine — 127.0.0.1:3000, 127.0.0.1:5000, and so on. When a request arrives, Nginx reads the Host header — the domain name the browser asked for — and forwards the request to the matching app with proxy_pass. The app responds to Nginx, Nginx responds to the browser. The browser never talks to your apps directly.
One front door gives you several things for free: a single place to terminate TLS (HTTPS for every app from one setup), centralised access logs, and the ability to serve static files and cache them without touching your app code. Nginx itself is event-driven and tiny — on student traffic its overhead is effectively zero.
Step-by-step: three apps, one VPS
Assume you have three things running: a static portfolio site, a Node API on port 3000, and a Flask dashboard on port 5000. You own yourdomain.com and you want www.yourdomain.com, api.yourdomain.com, and dashboard.yourdomain.com.
1. Bind your apps to localhost. Each app should listen on 127.0.0.1 and its own port — not 0.0.0.0. Apps bound to all interfaces are reachable directly from the internet, bypassing Nginx and your HTTPS setup entirely. This is the single most common misconfiguration.
2. Point DNS at the VPS. Create A records for www, api, and dashboard, each pointing at your VPS IP. DNS changes can take a few minutes to a few hours to propagate — wait until ping api.yourdomain.com resolves to your server before continuing.
3. Install Nginx. On Ubuntu or Debian:
sudo apt update
sudo apt install nginx
4. Write one server block per subdomain. Server blocks live in /etc/nginx/sites-available/, symlinked into sites-enabled. Here is a complete, minimal block for the API:
server {
listen 80;
server_name api.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
The proxy_set_header lines matter more than they look. Without them, your app sees every visitor as 127.0.0.1, and features that build URLs from the request (login redirects, password-reset links) generate broken http://127.0.0.1:3000/... links. Duplicate this block for the dashboard on port 5000 with server_name dashboard.yourdomain.com.
For the static portfolio, let Nginx serve the files directly instead of proxying:
server {
listen 80;
server_name www.yourdomain.com;
root /var/www/portfolio;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
5. Test and reload. Never reload blind — one typo in any server block takes every site down:
sudo nginx -t
sudo systemctl reload nginx
6. Add HTTPS with Certbot. Certbot's Nginx plugin reads your server blocks and handles certificates automatically:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com -d api.yourdomain.com -d dashboard.yourdomain.com
It obtains free Let's Encrypt certificates, rewrites your server blocks to listen on 443, and sets up renewal. Certbot needs to reach your server on port 80 to prove ownership, so open port 80 in your firewall first.
7. Support WebSockets. Chat apps and live dashboards need the protocol upgrade to pass through, or you will get mysterious 400 errors:
location /socket.io/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
Subdomains or paths?
You have two routing styles. Subdomains (api.yourdomain.com) give each app full isolation: separate TLS certificates, separate cookies, and no code changes — the app thinks it owns its domain. The cost is one DNS record per app. Path prefixes (yourdomain.com/api) need no extra DNS, but every app must be rewritten to serve under its prefix, and cookie and CORS behaviour gets tangled. For student projects, subdomains are almost always the cleaner choice.
Mistakes that waste evenings
- Apps bound to 0.0.0.0. Directly reachable on their ports, bypassing Nginx. Bind to 127.0.0.1 and close the app ports in the firewall.
- Missing proxy headers. Every visitor logs as 127.0.0.1 and generated links break. Copy the four
proxy_set_headerlines into every proxy block. - Certbot failing. Almost always port 80 blocked by the firewall or DNS not yet propagated. Fix the plumbing, then re-run Certbot.
- WebSocket 400s. The
UpgradeandConnectionheaders are missing. Add the block above. - Reloading without testing. Always
nginx -tbeforereload. A failed test tells you the exact line number. - The default server block catching traffic. A request for a domain with no matching
server_namefalls into the default block. Either remove the default site or make it return 444 (drop the connection) so stray traffic doesn't land on a random app.
Frequently asked questions
Does Nginx replace my Node/Python app server?
No. Nginx is the receptionist, not the office. Your app still runs and does all the work; Nginx just routes, encrypts, and logs at the front door.
Can the frontend and backend share one domain?
Yes. Serve the built frontend files from Nginx at / and proxy /api/ to the backend. This also sidesteps most CORS headaches since everything comes from one origin.
What if my apps run in Docker?
Same pattern. Put the containers on a Docker network and proxy_pass to http://container-name:3000. Containers don't need published ports at all — Nginx is the only thing facing the internet.
Will Nginx slow my apps down?
Not measurably at student scale. Nginx handles tens of thousands of concurrent connections on modest hardware; your app will be the bottleneck long before Nginx is.
Do I need this for a single app?
Strictly, no — but it is still worth it. You get easy HTTPS, static-file caching, and clean logs, and adding a second app later becomes a five-minute job instead of a re-architecture.
Limitations
A reverse proxy is one machine's front door, not magic. It is a single point of failure — if the VPS goes down, everything behind it goes down too. All your apps share the VPS's CPU and RAM, so a runaway app can starve the others; that is a resource-planning problem, not something Nginx fixes. Each new subdomain needs DNS and certificate attention (wildcard certificates help here). And Nginx won't make a slow app fast — it routes and caches, but your code still has to perform.