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), usetry_files $uri $uri.html $uri/ =404;. - Astro writes a
404.htmlpage;error_pageserves 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.