Super Unicorn Inkmi Logo

The Simplicity of Single-File Golang Deployments

Use Systemd with Go

Inkmi is written as a decoupled monolith in Go, HTMX, Alpinejs, NATS.io and Postgres. I document my adventures and challenges in writing the application here on this blog, tune in again.

I’ve gained some experience with deployments over the last three decades Starting without a VCS, copying files by zip to servers, sharing files by zip and manually tracking changes.

Fast-forward some years and we deployed large Java applications with build scripts manually with a release train and release notes. Each releases a huge event with coordinating many people in the company. Fast-forward and we were automatically deploying Scala applications from CI bundled in Docker in the startup of my wife.

Last forward and I have deployed a Golang application to a cloud server. The result of the build is one executable. The Go web application had all files like configurations (no credentials), static css and html templates embedded with embedfs (and proxied through a CDN). So no jar or ear files, no npm or pip and no Docker. Just one binary copied to the server, started and monitored with Systemd. Systemd also restarts the app daily to make sure it works properly long term. As Go starts up tremendously fast, and as Systemd is keeping the TCP connections, there is no impact to users.

The build pipeline is simple; deployments are syncing a binary file to a server, with Systemd holding connections and restarting the new binary. Systemd is also used to degrade privileges for the running binary to make it more secure.

The binary checks if it has changed and restarts

go func() {
  watcher, err := fsnotify.NewWatcher()
	if err != nil {
	  logger.Error().Err(err)
	}
	defer watcher.Close()
	// Check for our binary
	_ = watcher.Add(os.Args[0])
	<-watcher.Events
	loh.Info().Str("File", os.Args[0]).Msg("executable changed, exiting...")
	// shutdown
	shutdown()
}()

The binary checks itself if it’s working, otherwise exits

for {
  client := &http.Client{}
  req, _ := http.NewRequest("GET", "http://127.0.0.1:4000", nil)
	// if we don't set the host to www
	// we'll get a REDIRECT in production
	req.Host = "www.example.com"
	_, err := client.Do(req)
	if err == nil {
	   // notify Systemd we're ok
	   _, _ = daemon.SdNotify(false, daemon.SdNotifyWatchdog)
		 logger.Info().Msg("Health check OK.")
	} else {
	  logger.Error().Msg("No response - ERROR - going to restart.")
	}
	time.Sleep(interval)
}

Standing here it looks like Docker was invented to manage dependencies for Python, Javascript and Java. It looks strange from a platform that deploys as one single binary.

This one binary feels pure and simple, compared to large NPM or JAR deployments inside a Docker container. It feels manageable and not overwhelmed by complexity. Radical Simplicity

When sharing this on Mastodon, others chimed in with “yeah, like copy a single file and that’s the command to run”, “I see you are slowly joining the fan club :)” and “yeah, so nice and simple! I still mostly package in containers for ease of deployment to the cloud, but it’s a pretty thin wrapper.”

So I’m not alone with my joy and happiness.

About Inkmi

Inkmi is a website with Dream Jobs for CTOs. We're on a mission to transform the industry to create more dream jobs for CTOs. If you're a seasoned CTO looking for a new job, or a senior developer ready for your first CTO calling, head over to https://www.inkmi.com

Other Articles

©️2024 Inkmi - Dream ❤️ Jobs for CTOs | Impressum