3 min read

Putting an Astro site on a plain Nginx server

A static Astro site doesn't need PHP, Node or anything clever on the server. Here's the Nginx config I use, and the mistake I nearly shipped.

Server stack illustration for hosting an Astro site on Nginx

This site is built with Astro and comes out the other end as a folder of plain HTML, CSS and images. No database, no PHP, nothing running on the server except the web server itself. Which is lovely, right up until you go to write the Nginx config and realise every guide you can find assumes you’re running WordPress, Laravel or a Node app.

The mistake I nearly shipped

I’ve set up a lot of Laravel sites over the years, so my first go at the config was the usual Laravel template with the domain changed:

root /var/www/tim.thunderhat.com/public;
index index.php index.html;

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

Two problems. Astro builds into dist, not public (in an Astro project, public is where you put files to be copied into the build, so it’s missing all your actual pages). And that try_files line sends anything it can’t find to index.php, which doesn’t exist. So instead of a normal 404 you’d get a confusing error from PHP, assuming PHP is even installed.

The config that works

server {
    listen 80;
    listen [::]:80;
    server_name tim.thunderhat.com;

    root /var/www/tim.thunderhat.com/dist;
    index index.html;

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

    location /_astro/ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
        try_files $uri =404;
    }

    location ~ /\. {
        deny all;
    }

    gzip on;
    gzip_types text/css application/javascript application/json image/svg+xml application/xml text/plain;
}

What each bit is doing:

  • root points at dist. That’s where npm run build puts the finished site.
  • try_files $uri $uri/ $uri.html =404. By default Astro turns /about into dist/about/index.html, so the $uri/ part picks that up. If nothing matches, you get a proper 404.
  • The /_astro/ block. Astro puts its optimised images, CSS and JavaScript in here with a hash in every filename. If a file changes, its name changes too, so it’s safe to tell browsers to cache them for a year.
  • location ~ /\. stops anyone fetching hidden files like .env or .git if they ever end up in the folder by accident.

If you add a custom 404 page (src/pages/404.astro), add error_page 404 /404.html; inside the server block so Nginx uses it.

Getting the files onto the server

There are two sensible ways to do it.

You can build on the server: clone the repo, run npm install and npm run build, and Nginx serves dist straight from there. The downside is you need Node installed on the server just for the build.

Or build on your own machine and copy the result up:

npm run build
rsync -av --delete dist/ you@yourserver:/var/www/tim.thunderhat.com/dist/

The --delete flag removes files on the server that no longer exist locally, so old pages don’t hang around. Mind the trailing slash on dist/, it means “the contents of dist” rather than the folder itself.

HTTPS

Once your DNS is pointing at the server, Certbot sorts the rest:

sudo certbot --nginx -d tim.thunderhat.com

It adds the HTTPS server block and the redirect from HTTP for you.

Switching it on

sudo ln -s /etc/nginx/sites-available/tim.thunderhat.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Always run nginx -t before reloading. It checks the config and tells you what’s wrong, rather than finding out when the site goes down.

That’s the lot. A static site really is just files in a folder, and the config should be about that simple too.