$linuxjunkies
>

Host Static Sites with nginx

Install nginx on Linux, host multiple static sites with SSL, gzip/brotli compression, long-lived caching headers, and HTTP-to-HTTPS redirects.

BeginnerUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

Before you start

  • A Linux server with a public IP address and SSH access
  • DNS A records for each domain pointing to the server's IP
  • sudo or root privileges on the server
  • Ports 80 and 443 reachable from the internet (check cloud provider security groups)

nginx is a fast, low-memory web server that handles static files extremely well. Whether you are serving a personal blog, a marketing site, or a web app's compiled front-end, nginx is the right tool. This guide walks through installing nginx, configuring multiple virtual hosts, enabling compression, setting proper caching headers, redirecting HTTP to HTTPS, and terminating SSL—all on a modern Linux server.

Install nginx

Install from your distro's package manager. nginx in current LTS repositories is recent enough for everything covered here.

Debian / Ubuntu

sudo apt update && sudo apt install -y nginx

Fedora / RHEL 9 / Rocky Linux

sudo dnf install -y nginx

Arch Linux

sudo pacman -S nginx

Enable and start the service with systemd:

sudo systemctl enable --now nginx

Confirm it is running:

systemctl status nginx

Directory Structure for Multiple Sites

nginx uses a sites-available / sites-enabled pattern on Debian-family systems. Fedora and Arch use a single conf.d/ directory instead. Both patterns work the same way—each site gets its own config file.

Debian / Ubuntu layout

sudo mkdir -p /var/www/site-one/html /var/www/site-two/html
sudo chown -R $USER:$USER /var/www/site-one /var/www/site-two

Fedora / Arch / RHEL layout

sudo mkdir -p /usr/share/nginx/site-one/html /usr/share/nginx/site-two/html
sudo chown -R $USER:$USER /usr/share/nginx/site-one /usr/share/nginx/site-two

Drop a placeholder index file into each directory so you can test immediately:

echo '<h1>Site One</h1>' > /var/www/site-one/html/index.html
echo '<h1>Site Two</h1>' > /var/www/site-two/html/index.html

Write the Server Block Configs

Create a config file for each virtual host. The example below uses /var/www paths (Debian/Ubuntu). Adjust the root path if you are on Fedora or Arch. Replace site-one.example.com with your actual domain.

Site One — HTTP only (temporary, before SSL)

sudo nano /etc/nginx/sites-available/site-one
server {
    listen 80;
    listen [::]:80;
    server_name site-one.example.com;

    root /var/www/site-one/html;
    index index.html;

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

Create a matching file for site two, then enable both by symlinking into sites-enabled (Debian/Ubuntu only—Fedora/Arch place files directly in /etc/nginx/conf.d/):

sudo ln -s /etc/nginx/sites-available/site-one /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/site-two /etc/nginx/sites-enabled/

Test the configuration before every reload—make this a habit:

sudo nginx -t && sudo systemctl reload nginx

Obtain SSL Certificates with Certbot

Let's Encrypt certificates are free and automatically renew. Certbot is the standard client.

Debian / Ubuntu

sudo apt install -y certbot python3-certbot-nginx

Fedora / RHEL / Rocky

sudo dnf install -y certbot python3-certbot-nginx

Arch

sudo pacman -S certbot certbot-nginx

Run Certbot for both domains. It edits your nginx configs automatically and sets up a systemd timer for renewal:

sudo certbot --nginx -d site-one.example.com -d site-two.example.com

Certbot will add listen 443 ssl blocks and certificate directives. Verify the renewal timer is active:

systemctl status certbot.timer

HTTP → HTTPS Redirect

After Certbot runs, your port-80 server block should be updated automatically. If not, or if you prefer to control it manually, replace the port-80 block with a clean redirect:

server {
    listen 80;
    listen [::]:80;
    server_name site-one.example.com;
    return 301 https://$host$request_uri;
}

A 301 is a permanent redirect. Browsers and search engines will cache it, so use 302 only while testing.

Gzip and Brotli Compression

Compression is configured globally in /etc/nginx/nginx.conf inside the http block, which makes it apply to all sites automatically.

Gzip (built-in, always available)

sudo nano /etc/nginx/nginx.conf

Add or uncomment inside the http { } block:

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types
    text/plain
    text/css
    text/javascript
    application/javascript
    application/json
    application/xml
    image/svg+xml
    font/woff2;

Brotli (requires the ngx_brotli module)

The stock nginx package does not include Brotli. On Debian/Ubuntu you can install it from the nginx PPA or compile it as a dynamic module. On Fedora 38+ it is available as nginx-mod-http-brotli. If you need Brotli on Debian/Ubuntu, the easiest path is the official nginx apt repo:

# Fedora / RHEL
sudo dnf install -y nginx-mod-http-brotli

Then load the modules and configure them in nginx.conf:

load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;
brotli on;
brotli_comp_level 6;
brotli_types
    text/plain
    text/css
    application/javascript
    application/json
    image/svg+xml
    font/woff2;

Caching Headers

Browsers cache assets based on response headers. Set aggressive caching for fingerprinted assets (anything with a hash in the filename) and short or no caching for HTML, which changes frequently.

Add these location blocks inside your HTTPS server block:

# HTML — always revalidate
location ~* \.html$ {
    add_header Cache-Control "no-cache, must-revalidate";
}

# Fingerprinted assets — cache for 1 year
location ~* \.(css|js|woff2|woff|ttf|ico|webp|png|jpg|jpeg|gif|svg)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
    access_log off;
}

The immutable directive tells the browser not to revalidate the asset even on a hard reload, which is safe only when file names change on each build (as all modern front-end build tools do).

Security Headers (Bonus)

While you have the config open, add these inside the HTTPS server block. They cost nothing and meaningfully improve security posture:

add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

Open Firewall Ports

Your firewall must allow ports 80 and 443. Commands vary by firewall tool.

ufw (Debian / Ubuntu)

sudo ufw allow 'Nginx Full'
sudo ufw reload

firewalld (Fedora / RHEL / Rocky)

sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reload

Verify Everything Works

Run a final config test, reload, and then check each site and certificate:

sudo nginx -t && sudo systemctl reload nginx
# Check HTTP redirect
curl -I http://site-one.example.com

# Check HTTPS response and compression
curl -sI --compressed https://site-one.example.com | grep -E 'content-encoding|cache-control|strict-transport'

You should see content-encoding: gzip (or br for Brotli), appropriate cache-control values, and the HSTS header. Use securityheaders.com and SSL Labs for a full audit.

Troubleshooting

  • 502 / blank page after config change: Run sudo nginx -t first. Syntax errors prevent reload from applying changes but nginx keeps serving the last valid config.
  • Permission denied on document root: SELinux on RHEL-family systems may block nginx from reading /var/www. Run sudo restorecon -Rv /var/www or set the correct context with chcon -Rt httpd_sys_content_t /var/www/site-one.
  • Certbot fails with port 80 blocked: Ensure your firewall rule is in place and no other process is listening on port 80 (ss -tlnp | grep :80).
  • Brotli module load fails: Confirm the module file exists at the path specified in load_module with ls /usr/lib64/nginx/modules/ and adjust the path accordingly.
  • Logs: Access and error logs live at /var/log/nginx/access.log and /var/log/nginx/error.log. Tail them with sudo journalctl -u nginx -f for real-time output.
tested on:Ubuntu 24.04Debian 12Fedora 40Rocky 9

Frequently asked questions

Can I host multiple domains with different SSL certificates on one server?
Yes. Each server block has its own ssl_certificate and ssl_certificate_key directives. Certbot manages separate certificates per domain automatically, and nginx uses SNI to route TLS connections correctly.
Why does my CSS or JS still not compress even with gzip on?
Check that the MIME type is listed in gzip_types. Also confirm the response is not already compressed (nginx skips gzip if Content-Encoding is already set). Use `curl -sI --compressed` to inspect the actual response headers.
Is the immutable Cache-Control directive safe to use?
Only when asset file names change on each build—which tools like Vite, Webpack, and Parcel do by default by appending a content hash. If you serve the same filename with updated content, remove `immutable` and rely on max-age alone.
Does nginx serve Brotli automatically or only if the browser requests it?
nginx negotiates compression based on the Accept-Encoding request header sent by the browser. Brotli is only sent to clients that advertise `br` in that header, so there is no risk of sending incompatible responses.
How do I reload nginx without dropping active connections?
Use `sudo systemctl reload nginx` or `sudo nginx -s reload`. Both send SIGHUP, which causes nginx to re-read its config and gracefully hand off existing connections to new worker processes without any downtime.

Related guides