How to self-host a website (home or VPS)
Pick home or a VPS first, then put a static site live on nginx: server block, DNS record, firewall rule and TLS certificate, and what changes for an app.
Self-host a website at home, or on a VPS?
To self-host a website you need two things: a machine that is always reachable at one fixed public address, and ports 80 and 443 open to the internet. A VPS (virtual private server) gives you both by default. A home connection usually gives you neither, so the first decision is where the machine lives, not which web server to install.
Home hosting fails in four specific ways, and each has a check you can run before you buy anything.
The ISP blocks the ports. Many residential ISPs (internet service providers) block inbound TCP 80 and 443, and port 25 is blocked almost everywhere, so a web server or a mail server at home cannot be reached however you configure it. Test it: start a web server, forward the port on your router, then open your public address from a phone on mobile data. A timeout with the forwarding rule correct means the block is upstream of you.
The IP address changes. A residential address is leased, and it moves when the lease ends or the modem restarts. DNS (domain name system) then points at yesterday's address until a dynamic DNS client updates the record, and every visitor in between gets a timeout or somebody else's router.
You are behind CGNAT. CGNAT (carrier-grade NAT, network address translation done by the ISP) means the public-facing address on your router is itself private and shared with other customers. Port forwarding on your router does nothing, because inbound packets never reach your router. Open the router status page and read the WAN address: anything from 100.64.0.0 to 100.127.255.255, or inside 10.0.0.0/8, means you are behind CGNAT.
Uptime and power. A home machine goes down with every power cut and every ISP outage, and nothing tells you. A VPS sits in a datacenter with redundant power and network, and the only reboots are the ones you schedule.
Two solutions. The simple one is to put the site on the VPS and not host at home at all, which is what the steps below do. The other is to keep the files or the app at home and rent the cheapest VPS as the public entry point: the home machine opens an outbound connection to the VPS, and the VPS forwards visitors back down it. That design avoids all four problems, because the home side only ever connects outward. A reverse SSH tunnel through a VPS is the do-it-yourself version, and a Cloudflare Tunnel with no open ports is the hosted one.
If the site is static files and you want no server at all, GitHub Pages can host it for free, with the limits of someone else's platform. If you are leaving cPanel, moving from shared hosting to a VPS covers the migration itself. Everything below assumes a fresh Ubuntu 24.04 VPS that you have already taken through the first ten minutes on a new VPS: a non-root user with an SSH (secure shell) key, and updates applied.
Install nginx and serve a static page
nginx is the web server. It reads files from a directory and sends them to browsers, and later it also terminates TLS (transport layer security).
sudo apt update && sudo apt install -y nginx curl
sudo systemctl start nginx
systemctl is-active nginxsystemctl is-active nginx should print active. The Ubuntu package enables nginx at boot, so it comes back after a reboot with no extra step. It normally starts the service during installation as well, so the explicit start is a harmless no-op on a VPS; it matters only where the package could not start it, such as inside a container build.
Give the site its own directory. Replace example.com with your domain in every command from here on.
sudo mkdir -p /srv/www/example.com
sudo tee /srv/www/example.com/index.html > /dev/null <<'EOF'
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>example.com</title></head>
<body><h1>It works</h1></body>
</html>
EOF/srv is the directory the filesystem standard sets aside for data a server publishes, and nothing in the nginx package writes there, so an apt upgrade never touches your files. nginx only needs read permission, and files created with the default umask are world-readable. To upload later without sudo, hand the directory to your own user with sudo chown -R "$USER":"$USER" /srv/www/example.com.
Enable the server block
A server block tells nginx which hostname maps to which directory. Ubuntu keeps one file per site in /etc/nginx/sites-available/ and enables it with a symlink in /etc/nginx/sites-enabled/. Write this to /etc/nginx/sites-available/example.com:
sudo tee /etc/nginx/sites-available/example.com > /dev/null <<'EOF'
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /srv/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
EOFEnable it, test the configuration, then reload.
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxnginx -t parses every enabled file and prints syntax is ok and test is successful when it passes. When it fails it prints an [emerg] line naming the file and the line number, then test failed, and the running nginx keeps its old configuration, so a typo never takes the site down. Fix the line and test again before you reload. Use reload rather than restart: reload starts new workers on the new configuration and lets the old workers finish their requests, so no visitor sees a dropped connection.
Confirm from the server itself before touching DNS. Your domain does not resolve yet, so send the hostname in the Host header and nginx picks the block from that:
curl -H 'Host: example.com' http://127.0.0.1/You should see the HTML you wrote. If you see the package's "Welcome to nginx!" page instead, nginx did not match your server_name and answered with its default block. That happens because the symlink is missing from sites-enabled, or because you skipped the reload. If both are in place, the Host value you sent is not in server_name. ls -l /etc/nginx/sites-enabled/ shows which sites are really enabled.
Point the domain at the server (DNS)
The next three steps happen at the registrar, on your laptop, or need the server reachable from the internet, so you run them yourself, in this order.
Log into the registrar where you bought the domain and open its DNS records. Add an A record with the name @ (the bare domain) and your server's public IPv4 address as the value, then a second A record named www with the same address. If the VPS has an IPv6 address, add matching AAAA records. Set the TTL (time to live) to 300 seconds while you are testing, so a mistake is cheap to fix.
Check from your own computer once the record is saved with dig +short example.com A. dig comes from the bind9-dnsutils package on Ubuntu 24.04. It should print your server's address. An empty answer means the record has not propagated yet or was saved under the wrong name; the old address means your resolver is still caching the previous record for its TTL. Once it prints the right address, curl http://example.com/ from your laptop should return the same HTML you saw on the server.
Open the firewall
Nothing reaches nginx until ports 80 and 443 are open. On Ubuntu 24.04 the nginx package ships a ufw (uncomplicated firewall) application profile, so the rule is one line. Allow SSH first, or enabling the firewall locks you out of your own server. Run these on the server, in this order:
sudo ufw allow OpenSSHsudo ufw allow 'Nginx Full'sudo ufw enablesudo ufw status
Nginx Full opens TCP 80 and 443. Most VPS providers also run a separate network firewall in their control panel, and a port closed there stays closed whatever ufw says. If curl http://example.com/ from outside times out while the same command works on the server, the provider firewall is the next place to look.
Get a free TLS certificate
Browsers mark a plain HTTP site as "Not secure", so the last step is a certificate. Let's Encrypt issues one for free. certbot's nginx plugin requests it and edits your server block to use it, then a systemd timer renews it. Install it on the server with sudo apt install -y certbot python3-certbot-nginx, then run sudo certbot --nginx -d example.com -d www.example.com and answer its prompts.
Run this only after the DNS record resolves and port 80 is open. Let's Encrypt proves you control the name by fetching a file from http://example.com/.well-known/acme-challenge/ on your server, so a record that still points at the old address, or a closed port 80, fails the challenge. The certbot on Ubuntu 24.04 with nginx guide walks through the prompts, the renewal timer, the dry-run check and the rate limits that block repeated failed attempts.
That is a self-hosted website: files in /srv/www/example.com, nginx in front, a real name and a real certificate, on a server you control. To publish a new version, copy files into that directory. rsync -av ./site/ user@example.com:/srv/www/example.com/ from your laptop is enough, with no build step and no control panel in between.
What changes when the site is an app
A static site is files, and nginx serves files on its own. An app, meaning Django, Node, Rails, Go or anything else that runs as a process, changes two things.
First, something has to keep the process alive: start it at boot and restart it when it crashes. On a single VPS that is a systemd unit or a Docker Compose file. Second, nginx stays in front but stops serving a directory and instead proxies requests to the process on a local port, with proxy_pass http://127.0.0.1:8000; in the location block. nginx keeps TLS termination and the access logs, and the app only ever sees local traffic.
Two posts cover those routes. Deploying Django with Gunicorn behind nginx takes the systemd route: a unit file and the proxy block. Docker Compose basics on a VPS takes the container route, where Compose is the process manager. If the app came out of an AI code generator and you are not sure what runtime it needs, hosting an AI-generated app on a VPS starts from that question.
FAQ
Can I self-host a website from home instead of on a VPS?
You can if your ISP leaves inbound 80 and 443 open and gives you a real public address, and if you accept that the site goes down with your power. Check the router's WAN address first: anything in 100.64.0.0/10 or 10.0.0.0/8 means CGNAT, and no port forwarding rule can fix that. If a check fails, keep the machine at home and use a cheap VPS as the public entry point through a reverse tunnel, so the home side only makes outbound connections.
Do I need a domain name to self-host a website?
For testing, no. http://203.0.113.10/ works as soon as nginx is running and the firewall is open. For anything you share, yes. A certificate is issued for a name, and a name lets you move the site to a new server by changing one DNS record instead of telling every visitor a new address.
Why does nginx show the "Welcome to nginx!" page instead of my site?
nginx did not match the request to your server_name and fell back to its default server block. The usual causes are a missing symlink in /etc/nginx/sites-enabled/ or a reload that was never run. If both are fine, the hostname the visitor typed is not listed in server_name. Test with curl -H 'Host: example.com' http://127.0.0.1/ after each fix.
How big a VPS does a static website need?
The smallest one your provider sells. nginx serving files that fit in memory uses almost no CPU, and a 1 GB VPS handles far more visitors than a personal or small business site will see. Size the server for the app you plan to run behind nginx later, not for the static files.