The server powering practicalgobook.net
I write my blog posts in Markdown format, and then use Hugo static site generator to generate HTML files. The topic of this blog post is how those HTML files are served to you.
If you happened to come across my GopherCon 2021 lightning talk, this blog post describes the topic in all the gory detail!
- Background
- Content delivery - Proof of Concept
- Content Delivery - Caddy as a reverse proxy
- Automating restart on updates
- Zero downtime updates
- Summary
Background
When you type in https://practicalgobook.net in your browser (or click it from somewhere), a translation, commonly known as DNS resolution happens. As a result of this translation, the browser gets back an IP address:
# Ran this command on a Linux system
$ dig +short practicalgobook.net # this is a command to perform DNS resolution
172.105.175.12
This IP address refers to a virtual machine running in the cloud. As of a few months back, I am running this virtual machine on a cloud provider. When the browser’s request reaches this virtual machine, a program then sends back the HTML files that were generated by Hugo. This program is written using the Go programming language. I use the standard libraries and two third party packages for implementing advanced features.
Let’s dive in!
Content delivery - Proof of Concept
The first working iteration of the server looked as follows:
package main
import (
"embed"
"log"
"net/http"
"os"
)
//go:embed posts code
//go:embed index.html index.xml sitemap.xml
//go:embed categories css images
var siteData embed.FS
func main() {
listenAddr := ":80"
if len(os.Getenv("LISTEN_ADDR")) != 0 {
listenAddr = os.Getenv("LISTEN_ADDR")
}
mux := http.NewServeMux()
staticFileServer := http.FileServer(http.FS(siteData))
mux.Handle("/", staticFileServer)
log.Fatal(http.ListenAndServe(listenAddr, mux))
}
The key standard libraries used in this iteration were embed and net/http.
The embed
package allowed me build an executable Go application containing all the blog content. As you can see in the //go:embed
directives, I include all the directories that hugo
generated in the default, public
directory inside the application as
a variable siteData
of embed.FS type.
Once this was done, I use the http.FileServer handler to serve the files that were embedded
and available to the application via the siteData
variable.
http.FileServer
expects an argument of a type which implements the http.FileSystem interface.
siteData
which is of type embed.FS
implements the fs.FS interface and hence, we use the http.FS function to convert siteData
to a value of type http.FileSystem
and thus, we have the following code snippet above:
mux := http.NewServeMux()
staticFileServer := http.FileServer(http.FS(siteData))
mux.Handle("/", staticFileServer)
Then, we call the ListenAndServe()
function to start the HTTP server on the address specified in listenAddr
.
To summarize, at this stage, I had:
- I a Go server containing all my blog’s files. All I had to do is build my application and copy it to the host using
scp
. I didn’t need to copy the contents separately! My blog was just an executable. - It ran on address specified,
:8080
which I specified viaLISTEN_ADDR
environment variable - I used a cloud provider’s load balancer to give me free HTTPS, which means, I had https://practicalgobook.net website working (Traffic flow: Browser -> Load balancer -> Go server (running on port 8080)
Next, I set out to implement support for making the website acessible over HTTPS and without paying for the very expensive load balancer.
Content Delivery - Caddy as a reverse proxy
During this stage, I made a couple of changes:
- I stopped using the cloud provider’s load balancer as it was proving too expensive
- I pointed the DNS record of practicalgobook.net to a self-managed virtual machine
Hence, I would need a way to manage the TLS certificates on the virtual machine myself.
I knew that I am going to use https://letsencrypt.org/ for TLS certificates. I considered managing TLS certificates in the Go server itself using one of the options listed here. However, quickly realizing it may not be a battle for the day, I gave Caddy a go.
I installed Caddy on my CentOS VM, and wrote the following Caddyfile
:
practicalgobook.net {
reverse_proxy localhost:8080
handle_errors {
respond "I will be back soon!"
}
}
And that’s it! I started caddy
using the systemd service, started my Go application using a systemd service and I had HTTPS working again without
the expense of running a cloud managed load balancer.
To summarize, at this stage, I had:
- I have a Go server containing all my blog’s files. All I had to do is build my application and copy it to the host using
scp
. I didn’t need to copy the contents separately! My blog was just an executable. - It ran on address specified,
:8080
which I specified viaLISTEN_ADDR
environment variable - I used Caddy which ran on port 443 and port 80 to give me free HTTPS and forwarded traffic to the Go server on port 8080. I had https://practicalgobook.net website working (Traffic flow: Browser -> Virtual Machine -> Caddy (443) -> Go server (running on port 8080)
- No third party libraries
I was happy with the progress at this stage, but then I found an issue.
When I wanted to update my blog, I had to stop and start my application manually and which meant there was downtime.
And considering that this is a very important website, I couldn’t have that. So I set out to fix that.
Automating restart on updates
My first plan was, “yeah, I will create a new executable and simply overwrite the existing one in place and then find a way to reload it”. You can’t, you will get a “Text file busy” error as explained here. Now, of course, even if I could copy it in place, reloading itself would need some work, notable have a way to indicate to the program and “please reload yourself”. And that exploration led me to slayer/autorestart package and I integrated it as follows:
package main
import (
"embed"
"log"
"net/http"
"os"
"github.com/slayer/autorestart"
)
//go:embed buy categories code css images posts support tags toc
//go:embed book_cover.jpg go.mod index.html index.xml sitemap.xml
var siteData embed.FS
func main() {
listenAddr := ":8080"
if len(os.Getenv("LISTEN_ADDR")) != 0 {
listenAddr = os.Getenv("LISTEN_ADDR")
}
mux := http.NewServeMux()
staticFileServer := http.FileServer(http.FS(siteData))
mux.Handle("/", staticFileServer)
// Notifier
restart := autorestart.GetNotifier()
go func() {
<-restart
log.Printf("Detected change in binary. Restarting.")
}()
autorestart.StartWatcher()
log.Fatal(http.ListenAndServe(listenAddr, mux))
}
At this stage, my blog updates looked like:
$ GOOS=linux GOARCH=amd64 go build
$ scp server root@<ip-address>:/usr/local/bin/practicalgo-website-1
$ ssh root@ip mv /usr/local/bin/practicalgo-website-1 /usr/local/bin/practicalgo-website
I get around the “Text file busy” error by creating a temporary file and then using the mv
command which uses
the atomic rename() system call.
And thanks to slayer/autorestart
, my application reloads itself and my new content becomes available to the world wide web!
To summarize, at this stage, I had:
- I have a Go server containing all my blog’s files. All I had to do is build my application and copy it to the host using
scp
. I didn’t need to copy the contents separately! My blog was just an executable. - It ran on address specified,
:8080
which I specified viaLISTEN_ADDR
environment variable - I used Caddy which ran on port 443 and port 80 to give me free HTTPS and forwarded traffic to the Go server on port 8080. I had https://practicalgobook.net website working (Traffic flow: Browser -> Virtual Machine -> Caddy (443) -> Go server (running on port 8080)
- Using slayer/autorestart, I had implemented an automatically updating running server
However, for the automatic update to be running, the old process was being terminated and a new process being created, which meant, connections were being dropped and that is simply not an option.
So, time to fix that.
Zero downtime updates
Now, I “knew” that on Linux, network sockets are file descriptors and that you child processes inherit parent’s opened file descriptors.
But I didn’t know enough to implement it myself. From the README of slayer/autorestart
, I saw a reference to an unmaintained project
facebookgo/grace which seemed very relevant to what I was looking for. So, I integrated my server with
the grace/gracehttp
package:
package main
import (
"embed"
"log"
"net/http"
"os"
"github.com/facebookgo/grace/gracehttp"
"github.com/slayer/autorestart"
)
//go:embed buy categories code css images posts support tags toc
//go:embed book_cover.jpg go.mod index.html index.xml sitemap.xml
var siteData embed.FS
func main() {
logger := log.New(os.Stdout, "INFO: ", log.Lshortfile)
listenAddr := ":8080"
if len(os.Getenv("LISTEN_ADDR")) != 0 {
listenAddr = os.Getenv("LISTEN_ADDR")
}
mux := http.NewServeMux()
staticFileServer := http.FileServer(http.FS(siteData))
mux.Handle("/", staticFileServer)
srv := http.Server{
Addr: listenAddr,
Handler: mux,
}
autorestart.RestartFunc = autorestart.SendSIGUSR2
restart := autorestart.GetNotifier()
go func() {
<-restart
logger.Printf("Detected change in binary. Restarting.")
}()
autorestart.StartWatcher()
gracehttp.SetLogger(logger)
logger.Fatalf("Server terminating: %v", gracehttp.Serve(&srv))
}
I alluded to running the Go server as a systemd service above, here’s the systemd service file:
[Unit]
Description=Practical Go Website
[Service]
Environment="LISTEN_ADDR=:8080"
ExecStart=/usr/local/bin/practicalgo-website
User=nobody
[Install]
WantedBy=multi-user.target
I also added in systemd socket activation for my server using the following
.socket
file:
[Socket]
ListenStream = 8080
BindIPv6Only = both
[Install]
WantedBy = sockets.target
The summary of the above changes results in the following behavior:
- When a request comes in, and if my application is not already running, systemd will automatically start it (thanks to socket activation!)
- When
slayer/autorestart
detects a change in binary, it sends itself theSIGUSR2
signal facebook/grace/gracehttp
, upon getting this signal activates its machinery of ensuring that the new process’s underlying TCP listeners are created from the already opened file descriptors and thus ensuring no current HTTP requests are dropped
Thus, as it stands today:
- I have a Go server containing all my blog’s files. All I had to do is build my application and copy it to the host using
scp
. I didn’t need to copy the contents separately! My blog was just an executable. - It ran on address specified,
:8080
which I specified viaLISTEN_ADDR
environment variable - I used Caddy which ran on port 443 and port 80 to give me free HTTPS and forwarded traffic to the Go server on port 8080. I had https://practicalgobook.net website working (Traffic flow: Browser -> Virtual Machine -> Caddy (443) -> Go server (running on port 8080)
- Using slayer/autorestart, I had implemented an automatically updating running server
- Using systemd socket activation and facebookgo/grace/gracehttp, I have a zero-downtime server application.
Summary
In the book, we make heavy use of net/http
standard library package and I allude to how useful the
embed
package can be. I hope you found this post useful and showed you a practical way to use them together. As I mentioned in my GopherCon 2021 lightning talk, there was no problem to solve here, but it proved to be a very fulfilling learning exercise for me.
While writing this post, especially, the last section, I sensed a disconnect in my understanding between why I needed the socket activation, other
than the initial automatic startup of the server. Was it also needed for the graceful restart? I will need to dig in a bit more again.
You can find the source code for the server in the file, server.go and other resources that are needed to deploy here.
If you are curious to learn more systemd socket activation and Go applications, please refer this post: https://mgdm.net/weblog/systemd-socket-activation/.
Finally, I got very curious about how facebook/grace/gracehttp
worked and was able to implement a proof of concept myself only for TCP network sockets from
scratch. Here’s some code, without any documentation here.
I have a plan to replace my use of both the third party libraries with my code for this server.