Independent marketplace for cloud provider accounts across 22 providers

Developer Workflows

Nginx as a Reverse Proxy on a Cloud Server

Nginx handles TLS, hostnames, static files and request limits so your application does not have to. This guide builds a working reverse proxy on a Linux cloud server, adds a certificate, fixes the four settings everyone forgets, and decodes the errors you will meet.

Nginx receiving HTTPS requests on a cloud server and forwarding them to two local application processes

One Public Door, Many Private Rooms

An application server is happiest listening on a high port on the loopback interface, knowing nothing about certificates, hostnames or the public internet. Nginx sits in front of it and handles everything that belongs to the edge: TLS, virtual hosts, static files, compression, request size limits and the small amount of buffering that protects a single threaded application from slow clients.

That division is why the reverse proxy pattern survives every change in fashion. Whether the thing behind it is Node, Django, Rails, PHP-FPM or a container, the front door looks the same.

This guide builds that front door on a Linux cloud server in the order you would actually do it: install, a minimal working proxy, TLS, the headers and limits that everyone forgets, then multiple applications on one machine.

Install and Learn the File Layout

On Debian and Ubuntu, sudo apt install nginx is enough. On RHEL derivatives it is sudo dnf install nginx followed by sudo systemctl enable --now nginx. Both leave a default welcome page on port 80, which is a convenient way to confirm the firewall and DNS are right before any application is involved.

The layouts differ and this trips people up when they follow a tutorial written for the other family. Debian and Ubuntu use /etc/nginx/sites-available/ for configuration files and /etc/nginx/sites-enabled/ for symlinks to the ones that are live. RHEL packages skip that split and load everything in /etc/nginx/conf.d/.

Either way, never edit nginx.conf itself beyond global settings. Create one file per site, name it after the hostname, and enable it with a symlink. Remove the default site before going live, because it otherwise answers for any hostname you have not configured.

Two commands should become muscle memory. sudo nginx -t parses the whole configuration and reports the file and line of any error. sudo systemctl reload nginx applies the change without dropping existing connections. Always run the first before the second; a restart with a broken configuration leaves the site down.

The Smallest Configuration That Works

Assume an application listening on 127.0.0.1:3000. Create /etc/nginx/sites-available/app.example.com:

server {
listen 80;
listen [::]:80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
}
}

Enable and reload:

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

Every line in that block is load bearing. Without proxy_set_header Host, the application receives the literal upstream address and any absolute URL it generates will be wrong. Without X-Forwarded-For, every request in your application log appears to come from the proxy itself. Without X-Forwarded-Proto, a framework that enforces HTTPS will see plain HTTP, issue a redirect to itself, and produce an infinite loop that is maddening to debug.

The second listen line covers IPv6. If the server has an address on that protocol, and most do now, omitting it means half the internet reaches a connection refused. The IPv6 article covers where providers assign those addresses.

Adding TLS Without Hand Editing Certificates

Point the DNS record at the server and wait for it to resolve before requesting anything, because the validation step checks the name from outside. Then install the Nginx plugin for Certbot from your distribution packages and run sudo certbot --nginx -d app.example.com.

The plugin edits the server block in place: it adds a listen 443 ssl block, points at the issued certificate and key, and offers to add a redirect from port 80. Accept the redirect. The result is a site that answers on both ports and sends every plain request to the encrypted one.

Renewal runs from a systemd timer installed alongside the package. Confirm it works with sudo certbot renew --dry-run rather than discovering the problem when the certificate expires. The Let's Encrypt challenge documentation explains why port 80 must stay reachable for the common validation method, which is the usual cause of a failed renewal on a server somebody firewalled later.

The certificate guide goes further into wildcard issuance and DNS validation, which you need when the service is not reachable from the public internet.

Four Settings That Cause Support Tickets

WebSocket upgrades

A connection that starts as HTTP and upgrades will fail unless the proxy passes the upgrade headers. Add proxy_set_header Upgrade $http_upgrade; and proxy_set_header Connection "upgrade"; inside the location, alongside the proxy_http_version 1.1 line already present.

Upload size

Nginx rejects request bodies above a modest default with a 413 status before the application ever sees them. Set client_max_body_size in the server block to whatever your largest legitimate upload requires, and set the matching limit in the application so the two agree.

Slow responses

A report or export that takes longer than the default proxy read timeout produces a 504 while the backend is still working happily. Raise proxy_read_timeout for the specific location that needs it rather than globally, and treat the need as a hint that the work belongs in a background job.

Buffering

Nginx buffers responses by default, which is what protects the backend from slow clients. For streaming responses, server sent events or long polling, that buffering holds output until the response completes. Turn it off with proxy_buffering off; in exactly those locations and nowhere else. The proxy module reference documents each directive and its default.

Serving Static Files and Several Applications

Proxying every request to the application wastes it. Let Nginx serve assets directly, because it does so from the page cache with no interpreter involved:

location /static/ {
alias /srv/app/public/;
expires 30d;
access_log off;
}

Put that location before the catch-all location /. Nginx matches prefixes by specificity rather than order for this case, but keeping the file readable matters more than the parsing rule.

Several applications on one server is simply several files, one per hostname, each with its own server_name and its own upstream port. The proxy routes by the Host header, so api.example.com and app.example.com can point at the same IP address and land in different processes.

When an application runs as more than one process, declare an upstream block listing each local port and reference it by name in proxy_pass. That gives round robin distribution on a single machine. It is not a substitute for a real load balancer across hosts, which the load balancer article covers, but it is enough to use every core on the box.

This pattern is why a single modest VPS goes a long way. Teams routinely put several small sites behind one proxy on a Contabo account or a Vultr account, choosing between them mostly on memory per unit of cost and network location, which the Contabo and Vultr comparison sets out. The Node.js deployment workflow shows the application side of the same machine.

Reading the Errors

SymptomUsual causeFirst check
502 Bad GatewayNothing is listening on the upstream addressss -ltnp to confirm the port and interface, then the application log
504 Gateway Time-outBackend slower than proxy_read_timeoutTime the request directly with curl against the loopback port
413 Request Entity Too Largeclient_max_body_size too lowThe server block, then the application framework limit
Redirect loopMissing X-Forwarded-ProtoThe header list in the location block
Wrong site servedDefault server block still enabled, or server_name typosudo nginx -T to print the fully resolved configuration
Every client shows the same IPApplication not reading the forwarded header, or a CDN in frontTrusted proxy settings in the framework, and set_real_ip_from

The Short Version

  • Keep the application on the loopback interface and let Nginx own port 80 and port 443.
  • One configuration file per hostname, enabled by symlink, with the packaged default site removed.
  • Set the four forwarding headers from the start; two of the nastiest bugs come from omitting them.
  • Let Certbot write the TLS block, then verify renewal with a dry run rather than assuming it.
  • Adjust body size, read timeout, buffering and upgrade headers per location, not globally.
  • Run nginx -t before every reload, and reach for nginx -T when a directive seems to be ignored.

Questions about a cloud account?

Ask about products, variants, availability or purchasing on either channel.