[#]: subject: "Hosting my static sites with nginx" [#]: via: "https://jvns.ca/blog/2022/01/24/hosting-my-static-sites-with-nginx/" [#]: author: "Julia Evans https://jvns.ca/" [#]: collector: "lujun9972" [#]: translator: " " [#]: reviewer: " " [#]: publisher: " " [#]: url: " " Hosting my static sites with nginx ====== Hello! Recently I’ve been thinking about putting my static sites on servers that I run myself instead of using managed services like Netlify or GitHub Pages. Originally I thought that running my own servers would require a lot of maintenance and be a huge pain, but I was chatting with Wesley about what kind of maintainance [their servers][1] require, and they convinced me that it might not be that bad. So I decided to try out moving all my static sites to a $5/month server to see what it was like. Everything in here is pretty standard but I wanted to write down what I did anyway because there are a surprising number of decisions and I like to see what choices other people make. ### the constraint: only static sites To keep things simple, I decided that this server would only run `nginx` and only serve static sites. I have about 10 static sites right now, mostly projects for [wizard zines][2]. I decided to use a $5/month DigitalOcean droplet, which should very easily be able to handle my existing traffic (about 3 requests per second and 100GB of bandwidth per month). Right now it’s using about 1% of its CPU. I picked DigitalOcean because it was what I’ve used before. Also all the sites were already behind a CDN so they’re still behind the same CDN. ### problem 1: getting a clean Git repo for each build This was the most interesting problem so let’s talk about it first! Building the static sites might seem pretty easy – each one of them already has a working build script. But I have pretty bad hygiene around files on my laptop – often I have a bunch of uncommitted files that I don’t want to go onto the live site. So I wanted to start every build with a clean Git repo. I also wanted this to be _fast_ – I’m impatient so I wanted to be able to build and deploy most of my sites in less than 10 seconds. I handled this by hacking together a tiny build system called [tinybuild][3]. It’s basically a 4-line bash script, but with extra some command line arguments and error checking. Here are the 4 lines of bash: ``` docker build - -t tinybuild < Dockerfile CONTAINER_ID=$(docker run -v "$PWD":/src -v "./deploy:/artifact" -d -t tinybuild /bin/bash) docker exec $CONTAINER_ID bash -c "git clone /src /build && cd /build && bash /src/scripts/build.sh" docker exec $CONTAINER_ID bash -c "mv /build/public/* /artifact" ``` These 4 lines: 1. Build a Dockerfile with all the dependencies for that build 2. Clone my repo into `/build` in the container, so that I always start with a clean Git repo 3. Run the build script (`/src/scripts/build.sh`) 4. Copy the build artifacts into `./deploy` in the local directory Then once I have `./deploy`, I can rsync the result onto the server It’s fast because: * the `docker build -` means I don’t send any state from the repository to the Docker daemon. This matters because one of my repos is 1GB (it has a lot of PDFs in it) and sending all that to the Docker daemon takes forever * the `git clone` is from the local filesystem and I have a SSD so it’s fast even for a 1GB repo * most of the build scripts just run `hugo` or `cat` so they’re fast. The `npm` build scripts take maybe 30 seconds. ### apparently local git clones make hard links A tiny interesting fact: I tried to do `git clone --depth 1` to speed up my git clone, but git gave me this warning: ``` warning: --depth is ignored in local clones; use file:// instead. ``` I think what’s going on here is that git makes hard links of all the objects to make a local clone (which is a lot faster than copying). So I guess with the hard links approach `--depth 1` doesn’t make sense for some reason? And `file://` forces git to copy all objects instead, which is actually slower. ### bonus: now my builds are faster than they used to be! One nice thing about this is that my build/deploy time is less than it was on Netlify. For `jvns.ca` it’s about 7 seconds to build and deploy the site instead of about a minute previously. ### running the builds on my laptop seems nice I’m the only person who develops all of my sites, so doing all the builds in a Docker container on my computer seems to make sense. My computer is pretty fast and all the files are already right there! No giant downloads! And doing it in a Docker container keeps the build isolated. ### example build scripts Here are the build scripts for this blog (`jvns.ca`). **Dockerfile** ``` FROM ubuntu:20.04 RUN apt-get update && apt-get install -y git RUN apt-get install -y wget python2 RUN wget https://github.com/gohugoio/hugo/releases/download/v0.40.1/hugo_0.40.1_Linux-64bit.tar.gz RUN wget https://github.com/sass/dart-sass/releases/download/1.49.0/dart-sass-1.49.0-linux-x64.tar.gz RUN tar -xf dart-sass-1.49.0-linux-x64.tar.gz RUN tar -xf hugo_0.40.1_Linux-64bit.tar.gz RUN mv hugo /usr/bin/hugo RUN mv dart-sass/sass /usr/bin/sass ``` **build-docker.sh**: ``` set -eu scripts/parse_titles.py sass sass/:static/stylesheets/ hugo ``` **deploy.sh**: ``` set -eu tinybuild -s scripts/build-docker.sh \ -l "$PWD/deploy" \ -c /build/public rsync-showdiff ./deploy/ [email protected]:/var/www/jvns.ca rm -rf ./deploy ``` ### problem 2: getting rsync to just show me which files it updated When I started using rsync to sync the files, it would list every single file instead of just files that had changed. I think this was because I was generating new files for every build, so the timestamps were always newer than the files on the server. I did a bunch of Googling and figured out this incantation to get rsync to just show me files that were updated; ``` rsync -avc --out-format='%n' "[email protected]" | grep --line-buffered -v '/$' ``` I put that in a script called `rsync-showdiff` so I could reuse it. There might be a better way, but this seems to work. ### problem 3: configuration management All I needed to do to set up the server was: * install nginx * create directories in /var/www for each site, like `/var/www/jvns.ca` * create an nginx configuration for each site, like `/etc/nginx/sites-enabled/jvns.ca.conf` * deploy the files (with my deploy script above) I wanted to use some kind of configuration management to do this because that’s how I’m used to managing servers. I’ve used Puppet a lot in the past at work, but I don’t really _like_ using Puppet. So I decided to use Ansible even though I’d never used it before because it seemed simpler than using Puppet. Here’s [my current Ansible configuration][4], minus some of the templates it depends on. I didn’t use any Ansible plugins because I wanted to maximize the probability that I would actually be able to run this thing in 3 years. The most complicated thing in there is probably the `reload nginx` handler, which makes sure that the configuration is still valid after I make an nginx configuration update. ### problem 4: replacing a lambda function I was using one Netlify lambda function to calculate purchasing power parity (“PPP”) for countries that have a weaker currency relative to the US on . Basically it gets your country using IP geolocation and then returns a discount code if you’re in a country that has a discount code. (like 70% off for India, for example). So I needed to replace it. I handled this by rewriting the (very small) program in Go, copying the static binary to the server, and adding a `proxy_pass` for that site. The program just looks up the country code from the [geolocation HTTP header][5] in a hashmap, so it doesn’t seem like it should cause maintenance problems. ### a very simple nginx config I used the same nginx config file for templates for almost all my sites: ``` server { listen 80; listen [::]:80; root /var/www/{{item.dir}}; index index.html index.htm; server_name {{item.server}}; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; } } ``` The `{{item.dir}}` is an Ansible thing. I also added support for custom 404 pages (`error_page /404.html`) in the main `nginx.conf`. I’ll probably add TLS support with certbot later. My CDN handles TLS to the client, I just need to make the connection between the CDN and the origin server use TLS Also I don’t know if there are problems with using such a simple nginx config. Maybe I’ll learn about them! ### bonus: I can find 404s more easily Another nice bonus of this setup is that it’s easier to see what’s happening with my site – I can just look at the nginx logs! I ran `grep 404 /var/log/nginx/access.log` to figure out if I’d broken anything during the migration, and I actually ended up finding a lot of links that had been broken for many years, but that I’d just never noticed. Netlify’s analytics has a “Top resources not found” that shows you the most common 404s, but I don’t think there’s any way to see _all_ 404s. ### a small factor: costs Part of my motivation for this switch was – I was getting close to the Netlify free tier’s bandwidth limit (100GB/month), and Netlify charges $20/100GB for additional bandwidth. Digital Ocean charges $1/100GB for additional bandwidth (20x less), and my droplet comes with 1TB of bandwidth. So the bandwidth pricing feels a lot more reasonable to me. ### we’ll see how it goes! All my static sites are running on my own server now. I don’t really know what this will be like to maintain, we’ll see how it goes – maybe I’ll like it! maybe I’ll hate it! I definitely like the faster build times and that I can easily look at my nginx logs. -------------------------------------------------------------------------------- via: https://jvns.ca/blog/2022/01/24/hosting-my-static-sites-with-nginx/ 作者:[Julia Evans][a] 选题:[lujun9972][b] 译者:[译者ID](https://github.com/译者ID) 校对:[校对者ID](https://github.com/校对者ID) 本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 [a]: https://jvns.ca/ [b]: https://github.com/lujun9972 [1]: https://blog.wesleyac.com/posts/how-i-run-my-servers [2]: https://wizardzines.com [3]: https://github.com/jvns/tinybuild/ [4]: https://gist.github.com/jvns/06754e9e65b49dd461fefa071dd4aace [5]: https://support.cloudflare.com/hc/en-us/articles/200168236-Configuring-Cloudflare-IP-Geolocation