From Source to Service
Ship a static binary
Lesson 5 of 5
What you'll learn
- Build a fully static binary with
CGO_ENABLED=0and cross-compile withGOOS/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.
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.
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.