Building This Site
Leaving this here as a reminder of how this site works since I will inevitably forget. In a nutshell, it is:
- Built using Hugo
- Checked into GitHub
- Run using the seemingly fashionable serverless approach via Google Cloud Run
The goal is to have the site automatically rebuilt/deployed every time a commit
is pushed to a GitHub repo’s main branch.
The reason for the more-complex-than-necessary setup is that I wanted to learn a little more about a few things I’d never used, mainly Docker. In reality this is overkill and the site should just be served from GCS/S3, but this works for now.
Hugo
Things have changed since the GeoCities ‘type your raw HTML into this
textarea’ days. Static-site generators seem to be a popular replacement -
there are lots of options, but I chose Hugo since it is written in Go (which I’d
like to use more) and is easier to spell than Jekyll.
My setup is nothing fancy - I followed the Quick Start , added new pages with:
$ hugo new posts/some-post.md
And served locally with:
$ hugo server -D
The command hugo alone builds the site and is used by the later steps.
GitHub
Storing the site on GitHub serves two purposes:
- Version control, etc., etc.
- Common platform for various serving options (Cloud Run, Netlify, GitHub Pages, etc.)
I plan to experiment with a few options just for fun, so having it on GitHub simplifies that.
Docker
My Docker understanding is fledgling at best, but containerizing will allow it to run on Cloud Run (or another system in the future).
While trying to write my own Dockerfile, I came across the
klakegg/hugo
image, which demonstrated
I was way out of my depth and also solved everything. This allows for building
the site and then serving it with nginx:
FROM klakegg/hugo:0.82.0-onbuild AS hugo
FROM nginx
COPY --from=hugo /target /usr/share/nginx/html
With this a container can be built and run:
$ docker build . -t tb-com
$ docker run --rm -p 80:80 tb-com:latest
To jump ahead a bit, Cloud Run sends requests to the port $PORT on the
container, defaulting to 8080
(
docs
).
I assumed that a later step would require specifying the arguments to pass to a
docker run command, at which point we could replace the above command with:
$ docker run --rm -p $PORT:80 tb-com:latest
However that seems to be wrong. Based on the
examples
, the
$PORT needs to be used in application code, which in this case is nginx.
I made a number of mistakes setting this up, all due to my lack of nginx
knowledge. The trickiest one was overriding the port - passing the value through
wasn’t complicated (see run.sh below), but I wasn’t overriding the correct
files. I ended up with this config:
daemon off;
events {}
http {
include /etc/nginx/mime.types;
server {
listen ${PORT} default_server;
listen [::]:${PORT} default_server;
server_name localhost;
location / {
root /usr/share/nginx/html;
}
}
}
And used the following to replace ${PORT} and then override
/etc/nginx/nginx.conf:
#!/bin/bash
set -eux
# Cloud Run will provide `${PORT}`, but allow this to be run in other contexts.
export PORT=${PORT:-8080}
# Swap the `$PORT` into the nginx config.
envsubst '$PORT' < /build/nginx.template > /etc/nginx/nginx.conf
exec nginx
I put these files in a new build directory alongside the other Hugo
directories.
Now the Dockerfile can be extended to copy the build directory into the
container and run the script:
# Hugo Docker image. `onbuild` builds the site and places it in `/target`.
FROM klakegg/hugo:0.82.0-onbuild AS hugo
# Serve the generated files with `nginx`.
FROM nginx
COPY --from=hugo /target /usr/share/nginx/html
# Copy the /build/ directory into the container. This brings in the nginx
# template and the script to do the replacement.
COPY build /build/
# Serve the files.
CMD ["/build/run.sh"]
EXPOSE 8080
At this point, everything should be good to go:
- The container builds the site with
hugo - Files are copied into
/usr/share/nginx/html nginxstarts/listens on${PORT}, which will be substituted at runtime
To test locally:
$ docker build . --rm -t tb-com:latest
$ docker run . --rm -t tb-com:latest
# Run with a different port environment variable.
$ docker run --rm -p 1234:9876 --env PORT=9876 tb-com:latest
Cloud Run
In case a disclaimer is necessary: I work for Google but not in Cloud - part of choosing Cloud Run was just to learn more about it.
With the site checked into GitHub, the next step is automating the deployment. I followed the continuous deployment docs , and after a few hiccups (accidentally created two build triggers, created a build trigger with the incorrect Build type, etc.), all worked.
To test prior to submitting, I used Cloud Build to locally build the container
and then manually deployed it. This required a cloudbuild.yaml:
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'--no-cache',
'--pull',
'--file', 'Dockerfile',
'--tag', '$IMAGE',
'.']
images:
- '$IMAGE'
Which was built with:
$ gcloud builds submit \
--config cloudbuild.yaml \
--substitutions IMAGE=gcr.io/${PROJECT_ID}/${SERVICE}:${TAG}
And deployed with:
$ gcloud beta run deploy \
${SERVICE} \
--platform managed \
--region us-central1 \
--image ${IMAGE} \
--allow-unauthenticated
After confirming everything worked ‘manually’, I made some local tweaks and then pushed everything to GitHub, which also worked.
Takeaways
This project had one of the highest documentation_read / code_generated ratios
of anything I’ve worked on. Most of that is due to my lack of knowledge of the
space (I knew/know nothing about Hugo, Docker, serverless, etc.), but also
analysis paralysis - because there are so many options it is never clear whether
what you’re doing is ‘correct’ or laughably wrong. At some point I just stopped
caring in order to get something done, and overall it turned out fine.