Skip to content
DevOps devops app-deployment 5 min read

Putting Nginx in Front of Your App

This is the page where everything you have learned about deploying comes together. You have an app running on a private port like localhost:3000, and you have a domain name pointing at your server. Now you need the missing piece in the middle: Nginx (pronounced “engine-x”), a fast web server that acts as a reverse proxy (a server that sits in front of your app and forwards incoming requests to it). Nginx is the public face of your deployment — it terminates HTTPS, serves static files, adds security headers, and hides your app behind a clean, professional front door.

The reference architecture

Before touching any config, picture the whole flow. A request from a user’s browser travels like this:

Browser  ──HTTPS:443──▶  Nginx  ──HTTP:3000──▶  Your app (Node / Python / Java)
(internet)              (public)               (localhost only)

Nginx listens on the public ports 80 (HTTP) and 443 (HTTPS). Your actual application listens only on 127.0.0.1:3000 (localhost), so the outside world can never reach it directly. Nginx receives the encrypted request, decrypts it, decides what to do, and forwards it internally over plain HTTP to your app. This is exactly the pattern used in production almost everywhere.

Why bother with a reverse proxy at all? Here is what Nginx gives you that your app cannot easily do on its own:

JobWhy Nginx does it better than your app
TLS / HTTPSOne central place to manage certificates instead of inside every app
Static filesServes CSS, images, JS from disk far faster than your app process
Multiple appsRoute api.example.com and example.com to different backends
Security headersAdd headers once, applied to every response
Buffering & timeoutsProtects a slow app from getting flooded by slow clients

When NOT to use Nginx: if you are deploying behind a managed platform (Heroku, Railway, AWS App Runner) that already provides TLS and routing, adding your own Nginx is redundant. Use it on a plain virtual machine (VM) where you own the whole server.

Step 1 — Install Nginx

On Ubuntu 22.04 or 24.04 LTS, install from the standard package manager apt.

sudo apt update
sudo apt install -y nginx

Open the firewall (ufw, Ubuntu’s Uncomplicated Firewall) so HTTP and HTTPS traffic can reach the box, then confirm Nginx is running.

sudo ufw allow 'Nginx Full'
sudo systemctl status nginx

Output:

● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Mon 2026-06-15 09:14:02 UTC; 6s ago
   Main PID: 1423 (nginx)
      Tasks: 3 (limit: 4915)

active (running) and enabled mean Nginx is up now and will also start automatically on every reboot.

Step 2 — Write the server block

Nginx configuration lives in server blocks (Nginx’s name for a virtual host — one block per site or domain). On Ubuntu the convention is to put each site’s config in /etc/nginx/sites-available/ and then symlink the ones you want active into /etc/nginx/sites-enabled/.

Create a file for your site:

sudo nano /etc/nginx/sites-available/myapp

Paste this configuration, replacing example.com with your real domain and 3000 with your app’s port:

server {
    listen 80;
    server_name example.com www.example.com;

    # Forward every request to the app running on localhost
    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;

        # Needed so WebSockets and SSE keep working through the proxy
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Those proxy_set_header lines matter. Without X-Forwarded-For and X-Forwarded-Proto, your app sees every visitor as coming from 127.0.0.1 over plain HTTP, because that is literally what Nginx hands it. The headers pass the real client information through.

Now enable the site, test the syntax, and reload:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Output:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Gotcha: Ubuntu ships a default site in sites-enabled that grabs port 80. If your domain shows the “Welcome to nginx” page, remove it: sudo rm /etc/nginx/sites-enabled/default then reload.

Step 3 — Add HTTPS with Certbot

Certbot is the official free tool from Let’s Encrypt that obtains and installs TLS certificates automatically. TLS (Transport Layer Security) is the encryption that turns http:// into https://. Certbot can even edit your Nginx config for you.

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Certbot asks for an email, verifies you control the domain, then rewrites your server block to listen on 443 with the certificate and redirect HTTP to HTTPS. Certificates last 90 days; Certbot installs a systemd timer that renews them automatically. Verify it:

sudo certbot renew --dry-run

Output:

Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/example.com/fullchain.pem (success)

Step 4 — Security headers and static assets

Open your config again and harden it. Add these headers inside the server block (the HTTPS one Certbot created), and add a location that serves static files straight from disk instead of bouncing them through your app.

    # Security headers applied to every response
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

    # Serve static assets directly — far faster than the app process
    location /static/ {
        alias /var/www/myapp/static/;
        expires 30d;
        access_log off;
    }

Serving static files this way means your app never wakes up for an image or a stylesheet. Nginx reads the file from /var/www/myapp/static/ and ships it, with a 30-day browser cache. Reload after every change:

sudo nginx -t && sudo systemctl reload nginx

Best Practices

  • Bind your app to 127.0.0.1 only, never 0.0.0.0, so the only way in is through Nginx.
  • Always run sudo nginx -t before reload — a syntax error on restart takes the whole site down, but reload with a bad config simply refuses and keeps the old config running.
  • Keep one server block per domain in sites-available and symlink it; never edit the main nginx.conf for site rules.
  • Let Certbot manage renewals, but check sudo systemctl list-timers | grep certbot occasionally to confirm the timer is active.
  • Serve static assets with location blocks and long expires headers so your app handles only dynamic requests.
  • Trust the X-Forwarded-Proto header in your app framework so it knows requests arrived over HTTPS even though Nginx forwarded them as HTTP.
Last updated June 15, 2026
Was this helpful?