Install and Configure Nginx on Ubuntu VPS: Static Site, Reverse Proxy and SSL

Nginx (pronounced "engine-x") is one of the world's most widely deployed web servers, trusted by high-traffic sites like Netflix, Airbnb, and GitHub. Its event-driven, non-blocking architecture allows a single worker process to handle tens of thousands of concurrent connections without excessive resource usage. This guide walks you through installing Nginx on an Ubuntu 22.04 VPS from scratch — covering static file serving, virtual hosts, reverse proxy for Node.js, SSL with Let's Encrypt, Gzip compression, and essential security headers.

What Is Nginx and How Does It Differ From Apache?

Apache and Nginx are both popular web servers, but they handle connections in fundamentally different ways.

Apache: Process-based / Thread-based

Apache uses a process-based (or thread-based) concurrency model. For each incoming request, Apache spawns a new process or thread. Under high concurrency — say 1,000 simultaneous connections — Apache creates 1,000 processes or threads, consuming significant RAM and CPU. This model works well for dynamic content and .htaccess flexibility, but it does not scale as efficiently under load.

Nginx: Event-driven, Non-blocking

Nginx uses an event-driven, non-blocking I/O model. A single worker process can manage thousands of connections simultaneously by queuing events rather than blocking on each one. This results in significantly lower memory usage compared to Apache under the same traffic load.

Which should you choose? If you run Node.js, Python (Django/Flask), or PHP-FPM on a VPS and need a lightweight reverse proxy, Nginx is the better fit. If you rely on cPanel or need per-directory .htaccess overrides, Apache may be more convenient.

Installing Nginx on Ubuntu 22.04

Connect to your VPS via SSH, then run the following commands to install and start Nginx.

Step 1: Update Package Index and Install

apt update && apt install nginx -y

Step 2: Enable and Start the Service

systemctl enable nginx
systemctl start nginx

The enable command ensures Nginx starts automatically on server reboot. The start command launches it immediately.

Step 3: Verify the Status and Port 80

# Check that Nginx is running
systemctl status nginx

# Confirm Port 80 is listening
ss -tlnp | grep :80

# If using UFW, open HTTP and HTTPS
ufw allow 'Nginx Full'
ufw status

Open a browser and navigate to your VPS IP address, e.g. http://123.456.789.0. You should see the "Welcome to nginx!" page, confirming a successful installation.

Nginx File Structure Overview

Understanding the file layout before making changes will save you a lot of debugging time.

/etc/nginx/
├── nginx.conf              # Main config file (global settings)
├── conf.d/                 # Auto-included configs (custom global settings)
├── sites-available/        # All virtual host definitions (stored but not yet active)
│   └── default             # Default virtual host
├── sites-enabled/          # Symlinks to active virtual hosts
│   └── default -> ../sites-available/default
├── modules-available/      # Available modules
├── modules-enabled/        # Loaded modules
└── snippets/               # Reusable config fragments (e.g. SSL params)

Configuring a Virtual Host for a Static Website

In Nginx terminology, a "virtual host" is called a server block. It tells Nginx which domain to respond to, where to find the files, and how to handle requests.

Create the Document Root Directory

# Create the document root
mkdir -p /var/www/mysite.com/html

# Set ownership to www-data (the user Nginx runs as)
chown -R www-data:www-data /var/www/mysite.com

# Set appropriate permissions
chmod -R 755 /var/www/mysite.com

Create the Server Block Config File

nano /etc/nginx/sites-available/mysite.conf

Add the following content, replacing mysite.com with your domain.

server {
    listen 80;
    listen [::]:80;

    server_name mysite.com www.mysite.com;

    root /var/www/mysite.com/html;
    index index.html index.htm;

    location / {
        try_files $uri $uri/ =404;
    }

    # Block access to hidden files (e.g. .env, .git)
    location ~ /\. {
        deny all;
    }

    # Log files
    access_log /var/log/nginx/mysite.com.access.log;
    error_log  /var/log/nginx/mysite.com.error.log;
}

Enable the Virtual Host and Reload

# Create a symlink from sites-available to sites-enabled
ln -s /etc/nginx/sites-available/mysite.conf /etc/nginx/sites-enabled/

# Test the config for syntax errors (very important!)
nginx -t

# If the test passes, reload gracefully (no downtime)
systemctl reload nginx

Important: Always run nginx -t before systemctl reload nginx. If a config file has a syntax error and you reload without testing, Nginx may fail to start and take your site down.

Serving Static Files: root, index, try_files, and location Blocks

Several key directives control how Nginx serves static content. Here is an expanded example that adds caching headers for assets.

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

    root /var/www/mysite.com/html;

    # Files to serve when the request path is a directory
    index index.html index.htm;

    # Primary location: try exact file, then directory, then 404
    location / {
        try_files $uri $uri/ =404;
    }

    # Cache images and static assets for 30 days
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2|svg)$ {
        expires 30d;
        add_header Cache-Control "public, no-transform";
    }

    # Block hidden files
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Nginx as a Reverse Proxy for Node.js / PHP-FPM

One of the most common Nginx use cases is acting as a reverse proxy — sitting in front of an application server (Node.js, Python, PHP-FPM) and forwarding requests to it while handling TLS termination, caching, and compression.

Reverse Proxy for Node.js

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

    location / {
        proxy_pass http://localhost:3000;

        # Forward original headers to the application
        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;

        # Timeout settings
        proxy_connect_timeout 60s;
        proxy_send_timeout    60s;
        proxy_read_timeout    60s;
    }
}

Upstream Block for Load Balancing

If you run multiple application instances (e.g. a Node.js cluster or multiple app servers), use an upstream block to distribute traffic across them.

# Define the upstream group before the server block
upstream myapp_backend {
    # Round-robin by default
    server localhost:3000;
    server localhost:3001;
    server localhost:3002;

    # Reuse connections with keepalive
    keepalive 16;
}

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

    location / {
        proxy_pass http://myapp_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";  # Required for keepalive upstream
        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;
    }
}

Reverse Proxy for PHP-FPM

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

    root /var/www/myphp.com/html;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # Pass PHP files to PHP-FPM
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    }

    location ~ /\.ht {
        deny all;
    }
}

SSL with Let's Encrypt and Certbot

Let's Encrypt is a free Certificate Authority that issues SSL certificates automatically. Certbot is the official client that integrates directly with Nginx to obtain and install certificates in one command.

Step 1: Install Certbot

apt install certbot python3-certbot-nginx -y

Step 2: Obtain a Certificate and Auto-configure Nginx

# Replace example.com with your domain
# Certbot will update nginx config automatically and redirect HTTP to HTTPS
certbot --nginx -d example.com -d www.example.com

Certbot will prompt for your email address (for expiry notifications) and ask whether to redirect all HTTP traffic to HTTPS. Choosing to redirect is strongly recommended.

Step 3: Verify Auto-renewal

Certbot installs a systemd timer (or cron job) that automatically renews certificates before they expire. Verify it works correctly with a dry run.

# Test renewal without actually renewing
certbot renew --dry-run

# Check the systemd timer status
systemctl status certbot.timer

# Or check the cron entry (depending on your system)
cat /etc/cron.d/certbot

Let's Encrypt certificates are valid for 90 days. Certbot automatically renews them when fewer than 30 days remain.

Prerequisite: Before running Certbot, your domain's DNS A record must already point to your VPS IP address. If the domain does not resolve to the server, Certbot's domain ownership verification (ACME challenge) will fail.

Gzip Compression in Nginx

Enabling Gzip compression reduces the size of transferred files by 60–80%, significantly improving page load times for HTML, CSS, JS, and JSON responses.

Add the following to the http block in /etc/nginx/nginx.conf, or create a dedicated file at /etc/nginx/conf.d/gzip.conf.

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types
    text/plain
    text/css
    text/xml
    text/javascript
    application/json
    application/javascript
    application/x-javascript
    application/xml
    application/xml+rss
    application/xhtml+xml
    image/svg+xml
    font/woff
    font/woff2;

Security Headers in Nginx

Adding security-related response headers protects against common attack vectors such as clickjacking, MIME sniffing, and information leakage. Add these inside your server block.

server {
    # ... other config ...

    # Prevent clickjacking (disallow embedding in iframes from other origins)
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Prevent MIME type sniffing
    add_header X-Content-Type-Options "nosniff" always;

    # Control the referrer sent to third-party sites
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Legacy XSS protection for older browsers
    add_header X-XSS-Protection "1; mode=block" always;

    # Force HTTPS for the duration of max-age (requires SSL to be working first)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Hide the Nginx version number from response headers
    server_tokens off;
}

Note: Only add Strict-Transport-Security (HSTS) after confirming your HTTPS setup works correctly. Once set, browsers will refuse HTTP connections for the entire max-age period — if SSL breaks, users will be locked out.

Testing Config and Reloading Safely

Nginx includes a built-in config test that should be run before every reload or restart to prevent downtime caused by configuration errors.

# Test config syntax without touching the running server
nginx -t

# Expected output on success:
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

# Graceful reload (zero downtime — serves in-flight requests until complete)
systemctl reload nginx

# Full restart (brief downtime — use only when necessary)
systemctl restart nginx

# Tail the error log in real time
tail -f /var/log/nginx/error.log

# Tail the access log in real time
tail -f /var/log/nginx/access.log

Reload vs Restart: systemctl reload nginx performs a graceful reload — Nginx forks new worker processes with the updated config while old workers finish serving in-flight requests, resulting in zero downtime. restart stops and starts Nginx entirely, which may drop active connections.

Common Problems and How to Fix Them

502 Bad Gateway

This error means Nginx cannot reach the backend (Node.js or PHP-FPM has stopped, or the port is wrong).

# Check that the backend service is running
systemctl status php8.2-fpm
# Or for Node.js
ps aux | grep node

# Confirm the backend is listening on the expected port
ss -tlnp | grep 3000

# Review Nginx error logs
tail -100 /var/log/nginx/error.log

# Restart the backend
systemctl restart php8.2-fpm

Permission Denied (403 Forbidden)

Usually caused by incorrect file or directory permissions, or AppArmor/SELinux blocking access.

# Check ownership of files in the document root
ls -la /var/www/mysite.com/

# Fix ownership for www-data
chown -R www-data:www-data /var/www/mysite.com/

# Set correct permissions: directories 755, files 644
find /var/www/mysite.com/ -type d -exec chmod 755 {} \;
find /var/www/mysite.com/ -type f -exec chmod 644 {} \;

Config Syntax Error

Happens after a bad edit. nginx -t will fail and Nginx will refuse to reload.

# Run to see the exact error and line number
nginx -t

# Common error messages:
# nginx: [emerg] unexpected "}" — missing semicolon on a previous line
# nginx: [emerg] "server" directive is not allowed here — block is in the wrong place
# nginx: [emerg] host not found in upstream — upstream name or port is wrong

# If Nginx stopped entirely, restore the last known-good config
# then fix the error and start again

Nginx Shows Default Page Instead of Your Site

# Check that the symlink exists in sites-enabled
ls -la /etc/nginx/sites-enabled/

# Verify the server_name matches your domain
grep server_name /etc/nginx/sites-available/mysite.conf

# Remove the default site if it is interfering
rm /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx

Ready to Run Nginx on Your Own VPS?

AsiaGB VPS gives you full root access — install Nginx, Node.js, or PHP-FPM in minutes. Linux VPS plans start from 500 THB/month. Data centers in Thailand and Singapore.

View AsiaGB VPS Plans