A whole web app in one binary: the standard library and an embedded UI

Part of my series on RiskRancher, the open-source vulnerability manager I build. Source: github.com/Kuebiko-LLC/risk-rancher-core.

Intro

Across this series I’ve said RiskRancher is one binary a few times: one binary and SQLite, pure-Go so it builds everywhere, air-gapped because it carries everything. This post is the capstone: how the web app itself fits in that one file with no framework and no loose static assets. The short version is that modern Go’s standard library is enough, and embed does the rest.

I. No framework, just the standard library’s mux

For years, the reflex in Go was to reach for a router like chi or gorilla because the standard http.ServeMux couldn’t match on method or path variables. That changed in Go 1.22. The standard mux now does method-and-path patterns, so I didn’t pull in a framework at all:

app := &App{ Router: http.NewServeMux() }

app.Router.Handle("GET /uploads/",
    http.StripPrefix("/testdata/", http.FileServer(http.Dir("./data/testdata"))))

That GET /uploads/ pattern is pure standard library. Methods, trailing-slash subtrees, path wildcards: all of it is in net/http now. For an app this size, a third-party router would only add a dependency and a layer to learn, and buy me nothing. Fewer dependencies is also a security posture, which matters more than usual when the product is itself a security tool.

II. The UI is baked into the binary

The other half of “single file” is that there are no templates or CSS sitting next to the binary waiting to get out of sync. The whole UI is embedded at compile time:

//go:embed templates/* templates/components/* static/*
var CoreUIFS embed.FS

That one directive pulls every template, component, and static asset into the binary as a read-only filesystem. The server renders from CoreUIFS and serves static files from it, so there’s nothing on disk to deploy, nothing to point a web server at, and no way for the running UI to drift from the binary it shipped with. The version of the app and the version of its UI are the same artifact, always.

III. Why this adds up to the whole pitch

None of these choices is exotic on its own. The standard mux, embed, html/template, a pure-Go SQLite driver. The point is what they add up to: a program where the web server, the UI, the database engine, and the data file are either compiled in or sitting in one folder. There’s no application server to configure, no asset pipeline, no framework version to track, no external database. “Download one file and run it” isn’t a trick, it’s just what you get when every piece is either standard library or embedded.

And it compounds with the earlier posts. Standard library plus embed plus no-CGO SQLite is exactly why the thing cross-compiles to every platform, runs air-gapped, and installs by double-clicking. The architecture is boring on purpose, and the boring is the product.

Conclusion

A real web app, UI and all, fits comfortably in a single Go binary now, without a framework and without loose files. Route with the standard library’s grown-up mux, embed your templates and assets so the UI can’t drift from the code, and let the result be one artifact you can hand someone. If you’ve been adding dependencies out of habit, check what the standard library does in current Go first. It’s probably more than you remember. Thanks for reading, and may your whole app fit in one file.

cd ../blog