BuildBot

From Source to Service

Ship a static binary

Lesson 5 of 5

What you'll learn

  • Build a fully static binary with CGO_ENABLED=0 and cross-compile with GOOS/GOARCH
  • Bake static assets into the binary with //go:embed
  • Run it in production under systemd, behind a Caddy reverse proxy

This is Go's deployment story, and it's the whole reason the course ends here: the artifact is one file. No node_modules, no runtime version manager on the server, no Dockerfile required. Because we chose the pure-Go SQLite driver in lesson 3, cgo is off and the binary is fully static:

// From your Mac, build for a Linux server:
// CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o notesvc ./cmd/notesvc

GOOS/GOARCH are just environment variables — the cross-compiler is built into the toolchain, so linux/amd64, linux/arm64, and darwin/arm64 builds all come from the same machine with no extra install. -s -w strips debug symbols and shaves megabytes.

Web services usually have some static files. embed compiles them into the binary, so "deploy the assets" stops being a step:

import "embed"

//go:embed static
var staticFS embed.FS

// mux.Handle("GET /static/", http.FileServerFS(staticFS))

Running it: systemd + Caddy

Copy the binary up (scp notesvc server:/usr/local/bin/) and let systemd own the process — restarts, boot startup, logs into journald:

// /etc/systemd/system/notesvc.service
// [Unit]
// Description=notesvc
// After=network.target
// [Service]
// ExecStart=/usr/local/bin/notesvc
// Restart=on-failure
// User=notesvc
// [Install]
// WantedBy=multi-user.target

// systemctl enable --now notesvc

Don't terminate TLS in your app. Put Caddy in front: it provisions and renews certificates automatically, and its entire config for this job is two lines:

// Caddyfile
// notes.example.com {
//     reverse_proxy localhost:8080
// }

Your service keeps listening on plain :8080 on localhost; Caddy handles HTTPS, HTTP/2, and redirects. (nginx works too — Caddy just makes certificates a non-event.)

cgo silently re-enters

CGO_ENABLED=0 will fail the build if any dependency needs cgo — which is a feature. If you'd picked the mattn SQLite driver, this cross-compile would demand a Linux C cross-toolchain. Check with ldd notesvc on the server: a static binary says "not a dynamic executable".

The challenge models the release step: one loop over GOOS/GOARCH targets, flagging which combinations a cgo dependency would break.

Cross-compile matrix (JS model)

Run it. This models a release script looping over GOOS/GOARCH: pure Go builds every target from one machine, while a cgo dependency (flip cgoNeeded to true) breaks every cross-compile.

Loading editor…
Knowledge check

What does CGO_ENABLED=0 buy you at deploy time?

That's the full arc: from go mod init to a routed, tested, SQLite-backed service running as one static binary behind Caddy — you now ship Go, not just read it.

Saved on this device. Sign in to sync your progress everywhere.