Nginx reverse proxy on a Linux VPS

2 min
VPS/VDS Hosting

A reverse proxy receives public traffic on ports 80 and 443 and forwards it to applications that listen only locally. It gives you HTTPS, several sites on one IP and a single place for logs and limits. This guide sets it up on a VPS Linux Infrawire for Node.js, Python or Docker apps.

📋 Prerequisites

  • A Linux VPS (Ubuntu 24.04 / Debian 12 or AlmaLinux 9 / Rocky 9) with root SSH access. The IP and the root password are in your customer area.
  • A domain example.com with A/AAAA records pointing to VPS_IP.
  • An application listening on a local port, for example 127.0.0.1:3000.

📦 Install Nginx

Bash
# Debian / Ubuntu sudo apt update && sudo apt install -y nginx # AlmaLinux / Rocky sudo dnf install -y nginx sudo systemctl enable --now nginx

On AlmaLinux/Rocky, SELinux blocks Nginx from connecting to backends by default. Allow it once:

Bash
sudo setsebool -P httpd_can_network_connect 1

🚀 A first proxied application

Your app must listen on 127.0.0.1, not on 0.0.0.0, so it is only reachable through Nginx. Whatever the language, the pattern is the same: node server.js on port 3000, gunicorn -b 127.0.0.1:8000 app:app for Python, or a container published with docker run -d -p 127.0.0.1:3000:80 IMAGE.

Create /etc/nginx/sites-available/example.com (on AlmaLinux/Rocky use /etc/nginx/conf.d/example.com.conf):

Nginx
1server { 2 listen 80; 3 listen [::]:80; 4 server_name example.com; 5 6 location / { 7 proxy_pass http://127.0.0.1:3000; 8 proxy_http_version 1.1; 9 proxy_set_header Host $host; 10 proxy_set_header X-Real-IP $remote_addr; 11 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 proxy_set_header X-Forwarded-Proto $scheme; 13 } 14}

Enable it on Debian/Ubuntu, then validate and reload:

Bash
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx

The X-Forwarded-* headers let the application know the real client IP and whether the visitor used HTTPS. Without them, your app only sees 127.0.0.1 and may generate http:// links. Many frameworks must also be told to trust the proxy (for example app.set('trust proxy', 1) in Express).

🔌 WebSocket support

WebSocket needs the HTTP upgrade headers. Add a map at the top of the http context (for example in /etc/nginx/conf.d/websocket.conf):

Nginx
map $http_upgrade $connection_upgrade { default upgrade; '' close; }

Then in the location block:

Nginx
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade;

⏱️ Timeouts and body size

Defaults are 60 seconds and 1 MB. Adjust them for slow endpoints or uploads:

Nginx
proxy_connect_timeout 5s; proxy_read_timeout 120s; proxy_send_timeout 120s; client_max_body_size 50m;

For a long-lived WebSocket, raise proxy_read_timeout (for example 3600s) or send pings from the application.

🔒 HTTPS with Certbot

Open the firewall for 80 and 443 first, then:

Bash
# Debian / Ubuntu sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d example.com

On AlmaLinux/Rocky, install Certbot with sudo dnf install -y epel-release followed by sudo dnf install -y certbot python3-certbot-nginx. Certbot adds the certificate to your server block, offers the HTTP to HTTPS redirect and installs automatic renewal. Check it with:

Bash
sudo certbot renew --dry-run

🗂️ Several apps on one VPS

Create one server block per domain, each with its own server_name and its own local port:

Nginx
1server { 2 listen 80; 3 server_name app1.example.com; 4 location / { proxy_pass http://127.0.0.1:3000; } 5} 6 7server { 8 listen 80; 9 server_name app2.example.com; 10 location / { proxy_pass http://127.0.0.1:8000; } 11}

Repeat the proxy headers in each block, then run Certbot with -d app1.example.com -d app2.example.com. You can also split by path (location /api/ { proxy_pass http://127.0.0.1:8000/; }); the trailing slash then strips the prefix.

✅ Test

Bash
curl -I https://example.com ss -ltnp | grep -E ':(80|443|3000)' sudo tail -f /var/log/nginx/error.log

The backend port should be bound to 127.0.0.1 only, while Nginx listens on 80 and 443.

🛠️ Troubleshooting

502 Bad Gateway. The app is stopped or on another port. Check with ss -ltnp, then start it. On AlmaLinux/Rocky, a Permission denied in the error log means the SELinux boolean above is missing.

504 Gateway Timeout. The backend takes longer than proxy_read_timeout. Raise it or optimize the endpoint.

WebSocket disconnects or fails with 400/426. The Upgrade and Connection headers are missing, or proxy_http_version 1.1; is absent.

Wrong client IP in logs. X-Forwarded-For is sent but your app does not trust the proxy. Enable the trust setting of your framework.


🎉 Your applications are now served behind Nginx with HTTPS, clean headers and room for as many sites as your VPS can handle.

Stay up to date with the latest offers, discounts and special promotions.

View promotions

We use cookies

We use analytics cookies to improve your experience on our site. No data is sold to third parties. Learn more in our privacy policy.