Static sites — Astro, Hugo, Eleventy, Jekyll, Docusaurus, VitePress — compile to plain HTML, CSS and JavaScript. Serving them from your own VPS with Nginx is fast, cheap and has almost nothing to maintain: no runtime, no database, nothing to patch except the OS and web server.

This guide uses Astro as the example; the server side is the same for any static generator.

1. Build

npm ci
npm run build
ls dist/

Astro’s default output: 'static' writes everything to dist/. Check your trailingSlash and build.format settings — they decide whether pages are about/index.html (directory style, the default) or about.html (file style), which affects the Nginx config below.

2. Upload with rsync

rsync -avz --delete dist/ [email protected]:/var/www/example.com/

--delete removes files that no longer exist in the build. See rsync examples.

3. Nginx configuration

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

    # Directory-style pages: /about/ → /about/index.html
    location / {
        try_files $uri $uri/ =404;
    }

    # Hashed build assets: cache for a year
    location /_astro/ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
    }

    # HTML: always revalidate so new deploys show up
    location ~* \.html$ {
        add_header Cache-Control "no-cache";
    }

    error_page 404 /404.html;
}
  • For file-style output (about.html), use try_files $uri $uri.html $uri/ =404;.
  • Astro writes a 404.html page; error_page serves it with a proper 404 status.
  • Adapt the asset path for other generators (Hugo and Eleventy don’t hash by default — use shorter cache lifetimes there).

Trailing slashes

If your URLs end with /, Nginx automatically redirects /about → /about/ when about/ is a directory. Make internal links and the canonical URLs consistent with that, so you don’t create redirect hops. See Nginx rewrite rules for forcing one style.

sudo nginx -t && sudo systemctl reload nginx

4. HTTPS and compression

sudo certbot --nginx -d example.com -d www.example.com

Enable gzip/Brotli for HTML, CSS, JS and SVG — see Nginx compression. For even less CPU, pre-compress during the build and use gzip_static on;.

5. Atomic deploys

With plain rsync into the live directory, a visitor might load a page during the upload and get a mix of old HTML and missing new assets. Deploy into a fresh directory and switch a symlink instead:

#!/bin/bash
set -euo pipefail
RELEASE=/var/www/releases/$(date +%Y%m%d%H%M%S)
npm run build
rsync -az dist/ deploy@server:$RELEASE/
ssh deploy@server "ln -sfn $RELEASE /var/www/example.com && ls -1dt /var/www/releases/* | tail -n +6 | xargs -r rm -rf"

Point Nginx’s root at the symlink (/var/www/example.com). Rollback = point the symlink to the previous release. Keep a few old releases so cached HTML can still find its assets for a while.

6. Automate with CI

A GitHub Actions workflow can build and run the same rsync on every push to main — see GitHub Actions deploy to a VPS.

Hybrid or server-rendered Astro

If you use Astro’s server output (output: 'server' with @astrojs/node), run the generated server like any Node app (systemd or PM2) behind Nginx, serving dist/client assets directly — the same pattern as Next.js on a VPS.

Why self-host static sites?

Free static hosting platforms are excellent, but a VPS gives you full control over headers, redirects, logs and server location — and you can host the site next to the APIs, forms backend or analytics it uses. Static sites are so light that even the smallest VPS plan serves heavy traffic.

Related: React with Nginx, Nginx browser caching and static site generators.

  • Astro
  • Static Site
  • Nginx