jon@stjohn
← All posts

Building This Site: Hugo, Go, and Cloud Run

· 2 min read · GoInfrastructure

This site is static content generated by Hugo and served by a small Go binary, packaged into a container and deployed to Cloud Run. No framework, no server-side rendering at request time — just a build step and a file server.

Why not just use a CDN bucket?

A static bucket behind a CDN would work fine for pure static hosting. I chose Cloud Run with a Go server instead for two reasons: it keeps deployment as a single artifact (one container image, one gcloud run deploy), and it gives me a real HTTP server I can extend later — custom redirects, headers, or a small API route — without having to bolt on a load balancer.

The Go server

The server itself is intentionally small:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
	"log"
	"net/http"
	"os"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	fs := http.FileServer(http.Dir("./public"))
	mux := http.NewServeMux()
	mux.Handle("/", fs)

	log.Printf("listening on :%s", port)
	if err := http.ListenAndServe(":"+port, mux); err != nil {
		log.Fatal(err)
	}
}

Cloud Run injects the PORT environment variable, so the server just needs to respect it.

The build

The Dockerfile is a three-stage build: compile the CSS and Hugo site, compile the Go binary, then copy both into a minimal runtime image. Nothing in the final image depends on Hugo, Node, or the Go toolchain being present — it’s just the binary and the public/ directory it serves.

That separation is the part I like most about this setup: content authoring (Markdown + Hugo templates) and serving (a ~20 line Go program) don’t need to know anything about each other. I can change the theme completely without touching the server, or swap Cloud Run for a bucket later without touching the content.