How to Host a Website on a Linux Server
From a fresh VPS to a live HTTPS site: configure DNS, install Nginx, set up a virtual host, and issue a free Let's Encrypt TLS certificate with Certbot.
Before you start
- ▸A VPS with a public IP address and SSH access
- ▸A registered domain name with access to its DNS settings
- ▸A non-root user with sudo privileges on the server
A fresh VPS, a domain name, and about an hour is all you need to serve a real website over HTTPS. This guide walks through every layer: DNS, web server installation, virtual host configuration, and a free TLS certificate via Let's Encrypt. Commands are shown for Debian/Ubuntu, Fedora/RHEL, and Arch where they differ.
Step 1: Point Your Domain to the Server
Before touching the server, log into your domain registrar and create two DNS records. Replace 203.0.113.10 with your actual VPS IP and example.com with your domain.
- A record:
example.com→203.0.113.10 - A record:
www.example.com→203.0.113.10
DNS propagation takes anywhere from a few minutes to 48 hours. You can check progress with:
dig +short example.com A
dig +short www.example.com A
Both should return your server IP before you attempt TLS certificate issuance. If you try to get a certificate before DNS resolves, Certbot will fail.
Step 2: Secure the Server First
Run updates and make sure SSH key authentication is your only login method before opening web ports.
Update packages
# Debian / Ubuntu
sudo apt update && sudo apt upgrade -y
# Fedora / RHEL / Rocky
sudo dnf upgrade -y
# Arch
sudo pacman -Syu
Open firewall ports
You need ports 80 (HTTP) and 443 (HTTPS) open. Port 80 is required even if you plan to redirect everything to HTTPS — Certbot's HTTP-01 challenge uses it.
# UFW (Debian/Ubuntu default)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
# firewalld (Fedora/RHEL/Rocky)
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
# nftables (Arch or manual setup)
# Add to /etc/nftables.conf inside your input chain:
# tcp dport { 80, 443 } accept
sudo systemctl restart nftables
Step 3: Install Nginx
Nginx is a solid default for new deployments — low memory footprint, straightforward config syntax, and excellent static-file performance. Apache is equally valid; the concepts are the same.
# Debian / Ubuntu
sudo apt install nginx -y
# Fedora / RHEL / Rocky
sudo dnf install nginx -y
# Arch
sudo pacman -S nginx
Enable and start the service with systemd:
sudo systemctl enable --now nginx
Verify it's running:
sudo systemctl status nginx
The output should show active (running). At this point, visiting your server's IP in a browser should show the default Nginx welcome page.
Step 4: Create a Virtual Host (Server Block)
A virtual host tells Nginx which directory to serve for a given domain. The config layout differs slightly between distros.
Create the web root and a test page
sudo mkdir -p /var/www/example.com/html
sudo chown -R $USER:$USER /var/www/example.com/html
echo '<h1>It works</h1>' > /var/www/example.com/html/index.html
Write the server block config
# Debian/Ubuntu path:
sudo nano /etc/nginx/sites-available/example.com
# Fedora/RHEL/Arch path (drop directly in conf.d):
sudo nano /etc/nginx/conf.d/example.com.conf
Paste the following, substituting your domain:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
Enable the config (Debian/Ubuntu only)
Debian/Ubuntu uses a sites-enabled symlink pattern. Fedora/RHEL/Arch load everything in conf.d/ automatically.
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
Test and reload
sudo nginx -t
sudo systemctl reload nginx
nginx -t must report syntax is ok and test is successful before you reload. Fix any errors it reports before continuing.
Step 5: Issue a Free TLS Certificate with Certbot
Certbot from the EFF automates Let's Encrypt certificate issuance and renewal. Make sure DNS is already resolving to your server before running this.
Install Certbot
# Debian / Ubuntu
sudo apt install certbot python3-certbot-nginx -y
# Fedora / RHEL / Rocky (enable EPEL first on RHEL/Rocky)
sudo dnf install epel-release -y # RHEL/Rocky only
sudo dnf install certbot python3-certbot-nginx -y
# Arch
sudo pacman -S certbot certbot-nginx
Obtain and install the certificate
sudo certbot --nginx -d example.com -d www.example.com
Certbot will ask for an email address and whether to redirect HTTP to HTTPS. Choose redirect (option 2) — it will rewrite your Nginx config automatically to add a 301 redirect and the ssl_certificate directives.
Verify automatic renewal
Certbot installs a systemd timer (on most distros) or a cron job to renew certificates automatically before they expire. Check the timer:
sudo systemctl status certbot.timer
# Or on systems using cron:
sudo certbot renew --dry-run
The dry run should complete with Congratulations, all simulated renewals succeeded. Certificates renew automatically every 60 days; Let's Encrypt certs expire after 90.
Step 6: Verify Everything End-to-End
Open a browser and navigate to https://example.com. You should see your test page served over HTTPS with a valid certificate. Also confirm the HTTP-to-HTTPS redirect works:
curl -I http://example.com
The response should include 301 Moved Permanently and a Location: https://example.com/ header. Check your certificate details:
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
This prints the notBefore and notAfter dates of the certificate — confirm the expiry is roughly 90 days out.
Step 7: Deploy Your Actual Site Content
Replace the test page with your real files. If you're deploying a static site, copy files directly:
rsync -avz ./my-site/ [email protected]:/var/www/example.com/html/
For a dynamic application (Node.js, Python, PHP, etc.), you'll run the app as its own systemd service and proxy requests through Nginx using an upstream block — that's a separate guide. For PHP specifically, install php-fpm and add a fastcgi_pass directive to your server block.
Troubleshooting
- 502 Bad Gateway: Nginx is running but can't reach your application backend. Check that your app process is active with
systemctl status your-appand that the socket or port in the Nginxproxy_passmatches. - Certbot says DNS doesn't resolve: DNS hasn't propagated yet, or your firewall blocks port 80. Recheck
dig +short example.com Aandsudo ufw status/firewall-cmd --list-all. - 403 Forbidden: Nginx can't read your files. Check permissions — the web root and all parent directories need execute permission for the
www-data(Debian/Ubuntu) ornginx(Fedora/Arch) user. Runsudo chown -R www-data:www-data /var/www/example.comon Debian/Ubuntu. - Certificate not renewing: Run
sudo certbot renew --dry-runand read the error. Common causes: port 80 blocked by firewall, or the Nginx config was edited and broke the.well-knownlocation block Certbot added. - Nginx won't start after reboot: Confirm
sudo systemctl is-enabled nginxreturnsenabled. If not, runsudo systemctl enable nginx.
Frequently asked questions
- Do I need to open port 80 if I only want HTTPS?
- Yes. Certbot's HTTP-01 challenge requires port 80 to be reachable during certificate issuance and renewal. You can redirect all HTTP traffic to HTTPS immediately after, but the port must stay open.
- Can I use Apache instead of Nginx?
- Absolutely. Install apache2 (Debian/Ubuntu) or httpd (Fedora/RHEL), use VirtualHost blocks in /etc/apache2/sites-available or /etc/httpd/conf.d, and install python3-certbot-apache instead of the Nginx plugin.
- How do I host multiple websites on the same server?
- Create a separate web root directory, virtual host config file, and run certbot for each domain. Nginx routes requests to the right block based on the server_name directive.
- Why does Let's Encrypt only issue 90-day certificates?
- Short lifetimes limit the damage from a compromised key and encourage automation. Certbot's systemd timer handles renewal automatically, so the short lifespan is a non-issue in practice.
- My VPS provider has its own firewall — do I still need ufw or firewalld?
- Provider-level firewalls (security groups on AWS, cloud firewalls on DigitalOcean) are separate from the OS-level firewall. It's good practice to run both, but at minimum ensure ports 22, 80, and 443 are open at the provider level before troubleshooting the OS firewall.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.