Nginx From Scratch: Host a Real Website on Your Own Server

Nginx From Scratch: Host a Real Website on Your Own Server

Installing Nginx is only the first step. A useful web server also needs a correct domain configuration, HTTPS and reliable certificate renewal. There’s a particular satisfaction in watching the access log tick over for a page you served yourself, knowing every byte passed through a config file you typed. This walks you from a bare Ubuntu VPS to a real site on your own domain, locked down with free HTTPS. The commands target Ubuntu 24.04 LTS and follow the nginx, Certbot and Ubuntu documentation linked under Sources, and I’ll point out the India-specific snags, DNS propagation on Jio and the certificate renewals mostly, when we hit them.

About the prices and versions: Rupee figures are approximate and include 18 percent GST; hosting prices move quickly, so treat them as indicative rather than a quote. Commands were written for Ubuntu 24.04 LTS (supported until May 2029); Ubuntu 26.04 LTS was released on 23 April 2026 and these steps have not been re-checked on it. Latency ranges here are typical figures, not measurements; yours will differ with distance, ISP and time of day.

what you need first

Five-stage flow: install Nginx, point DNS, create a server block, add HTTPS with Certbot, then tune gzip and caching.
The five steps in this guide, in the order they depend on each other.

Three things, give or take. A VPS with a public IP, ideally sitting in Mumbai or Bangalore so your Indian visitors get 30 to 60 ms latency rather than the 250 ms you’d eat from a default US region. Think of it like the difference between a local train and changing three lines across the city, the distance is the cost. No VPS yet? My VPS beginner guide covers picking and spinning one up for somewhere around ₹500 to ₹700 a month, GST included. You also need a domain. A .in or .com runs roughly ₹600 to ₹1,000 a year, and my domain name guide covers where to buy one without walking into a renewal-price trap. And SSH access as a sudo user. Not root.

Nginx over Apache, why? Both work. But Nginx eats less RAM, which actually matters on a 1 GB server, the config reads cleaner once it clicks for you, and it’s widely used in production stacks and asked for in DevOps job descriptions. Learn it once and reverse proxies, load balancers, Kubernetes ingress, they all stop looking like alien tech.

step 1: install nginx, open the firewall

Terminal: apt installs nginx, ufw allows Nginx Full for ports 80 and 443, and systemctl status shows active (running).
active (running) plus the server IP in a browser should show the Welcome to nginx page.
Flow: a visitor's request passes the provider's cloud firewall, then ufw on the server, before reaching Nginx.
ufw says open while the provider console says closed is the most common hour lost in this setup.

The version in Ubuntu’s repo is fine for nearly everyone. SSH in, run this:

sudo apt update
sudo apt install nginx -y
sudo ufw allow 'Nginx Full'
sudo systemctl status nginx

That ufw rule opens ports 80 and 443. If status comes back active (running), drop your server’s IP into a browser and you should land on the “Welcome to nginx!” page. Nothing showing up? The obvious culprit is Nginx, but check whether your provider runs its own cloud firewall sitting in front of port 80. AWS Lightsail and Oracle both do this out of the box, so you have to open the port in their web console too. Not just in ufw. It’s the toll booth you forgot was on the highway.

step 2: point your domain at the server

Terminal: dig queries for example.in and www.example.in against 1.1.1.1 both return 203.0.113.42.
Asking 1.1.1.1 directly sidesteps any ISP resolver holding an old record.

Over in your registrar’s DNS panel, make two records. An A record for @ pointing at your server IP, and an A record for www pointing at the same IP. A TTL of 300 seconds is fine.

Propagation is usually quick. Some ISP resolvers keep an old record longer than its TTL suggests. Check from the server itself, which sidesteps your ISP entirely:

dig +short example.in @1.1.1.1
dig +short www.example.in @1.1.1.1

Once both spit back your server IP, you’re set. If your phone’s still showing the old page on Jio, don’t sit there waiting, just set the phone’s Private DNS provider hostname to one.one.one.one.

step 3: create a server block

Hub diagram: one nginx server routes example.in to /var/www/example.in/html and a second domain to its own web root through server blocks.
Nginx reads the Host header, picks the matching server block, and serves that block’s root.
Terminal: symlink the site into sites-enabled, nginx -t reports syntax ok and test successful, then systemctl reload nginx.
Edit in sites-available, symlink into sites-enabled, test, then reload.

Nginx calls virtual hosts “server blocks”. One machine can host a pile of sites, each with its own block. Start with a web root and a test page:

sudo mkdir -p /var/www/example.in/html
sudo chown -R $USER:$USER /var/www/example.in/html
echo '<h1>Namaste from my own server</h1>' > /var/www/example.in/html/index.html

Now the config file, at /etc/nginx/sites-available/example.in, with this inside:

server {
    listen 80;
    listen [::]:80;
    server_name example.in www.example.in;
    root /var/www/example.in/html;
    index index.html;

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

Enable it, test the syntax, reload:

sudo ln -s /etc/nginx/sites-available/example.in /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Make nginx -t a reflex. It catches the typo before it takes your site down, and reload applies the change without dropping anyone who’s mid-connection, a bit like switching the green-wave signal timing without stopping traffic. Visit your domain and the Namaste page should be there.

step 4: free https with let’s encrypt

Terminal: install certbot and the nginx plugin, run certbot --nginx for both domains, check the certbot timer, and a renew dry run reports all simulated renewals succeeded.
If the dry run passes, the systemd timer handles every renewal from here.
Bar chart of Let's Encrypt certificate lifetimes: 90 days today, 64 days from 10 February 2027, 45 days by 2028.
Shorter lifetimes only hurt if renewal is manual. Check the timer once and forget it.

Plain HTTP gets a ‘Not secure’ label in browsers, gives up the small ranking preference search engines give HTTPS, and leaves your pages open to modification by anything sitting between your server and the visitor. Certbot sorts all of that in about two minutes for ₹0:

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

Certbot edits your server block, installs the certificate, and wires up an HTTP-to-HTTPS redirect when you pick that option. Pick it. Let’s Encrypt certificates currently last 90 days (64 days from 10 February 2027, 45 days by 2028), and a systemd timer renews them on its own. Confirm the timer’s actually alive:

sudo systemctl list-timers | grep certbot
sudo certbot renew --dry-run

If the dry run passes you can basically forget certificates exist. Some hosts still sell paid certificates as an add-on; this does the same job for free. Once the redirect’s in place, load your site once over plain http:// and check the browser ends up on https:// with the padlock. It’s a thirty-second check you’ll be tempted to skip, but it catches the rare case where Certbot edited the wrong server block on a multi-site box.

step 5: defaults worth adding

Three cards: gzip on cuts text payloads 60 to 80 percent, expires 30d caches assets, server_tokens off hides the version.
Small lines that matter most on Indian mobile connections.

The stock config does work. A handful of extra lines make it noticeably better on Indian mobile connections, though, where every kilobyte and every round trip is a cost you pay. Inside your server block (the HTTPS one Certbot built), add:

gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;

location ~* \.(jpg|jpeg|png|webp|svg|css|js|woff2)$ {
    expires 30d;
    add_header Cache-Control "public";
}

server_tokens off;

Gzip shaves text payloads by 60 to 80 percent. The expires block lets a returning visitor pull assets from cache instead of re-downloading them over metered Jio data. And server_tokens off stops Nginx broadcasting its exact version to every scanner crawling the internet. Run sudo nginx -t and reload after each change.

One habit to build early: actually watch what Nginx is doing. The command sudo tail -f /var/log/nginx/access.log streams requests live, and inside a day you’ll catch bots poking at /wp-admin and /.env on a site that has neither. That’s just background radiation on the public internet, the equivalent of people rattling every door handle on the street, and seeing it firsthand is what makes the hardening steps feel worth doing rather than paranoid. If one IP gets genuinely obnoxious, a single deny 198.51.100.7; line inside the server block shuts it up on the spot.

where to go next: reverse-proxying an app

Flow: internet traffic on 443 reaches Nginx, which terminates TLS and proxies to an app listening only on 127.0.0.1 port 3000.
The app listens on localhost; Nginx is the only thing facing the internet.

Static files are step one. The actual payoff is parking Nginx in front of a Node, Python or Go app running on a local port. The location block looks like this:

location /api/ {
    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-Proto $scheme;
}

Your app never touches the internet directly. Nginx takes the TLS, the slow clients, the logging, like a traffic cop at the junction so the app doesn’t have to deal with the chaos. The same pattern fronts a self-hosted automation stack, covered in the n8n self-hosting guide. And before you put anything you care about behind it, work through the Ubuntu server security checklist, because a web server is the single most-attacked thing you’ll ever run.

common mistakes

  • Editing sites-enabled directly. Edit in sites-available and symlink. The enabled directory is just pointers.
  • Forgetting the cloud firewall. ufw says open, the provider console says closed, and you spend an hour debugging the wrong layer.
  • Running certbot before DNS resolves. Validation fails, and repeated failures walk you straight into Let’s Encrypt rate limits. Confirm with dig first.
  • restart instead of reload. systemctl reload nginx is zero-downtime. restart drops live connections for no reason.
  • chmod 777 on the web root. Don’t. Files 644, directories 755, owned by your user, read by Nginx.

FAQ

Can I host multiple websites on one ₹600 VPS?

Yes, that’s the entire point of server blocks. One block per domain, each with its own root directory and its own certificate. A 1 GB server will happily serve five or six static sites. RAM only becomes the ceiling once each site wants its own database or app process.

Do I still need Cloudflare in front of Nginx?

Need? No. Their free tier does add DDoS absorption and caching at Indian edge locations, which helps if you take traffic spikes. For a personal site though, plain Nginx with the gzip and caching config above is fast enough out of Mumbai.

How do I host WordPress on this setup?

Add PHP-FPM and MySQL, then a server block that hands .php requests off to the FPM socket. It runs fine on 2 GB of RAM. Weigh the effort honestly against the managed options in my Indian web hosting roundup before you commit to it.

Where are the logs when something breaks?

/var/log/nginx/access.log and /var/log/nginx/error.log. The error log answers ninety percent of the “why is it 404 or 502” questions on its own. Tail it live with sudo tail -f /var/log/nginx/error.log while you reproduce the problem.

Update history: removed an unsupported test claim, clarified the GST basis and the Ubuntu version, added the upcoming Let’s Encrypt certificate lifetimes, fixed the Android Private DNS step and added sources.

Sources