Why single-binary distribution wins for solo developers
Why single-binary distribution fits solo development on three axes — ops, support, OSS trust — with examples from gpdf and gsql, and where it doesn't fit.
The distribution format you pick pays off later
If you're a solo developer picking a distribution format for your product, lean as hard as you can toward a single binary. If you've already chosen Go, you get this property almost for free, and there's rarely a good reason to throw it away. There are three reasons: ops get lighter, support gets lighter, and OSS trust gets easier to earn. I'll go through them in order.
By "single binary" I mean the "runs as one executable file" form you get from Go's static linking and cross-compilation. Whether it's a server process like gpdf-api or a bundled CLI tool, once the artifact fits in one file, every step after that gets simpler. Conversely, the moment your artifact becomes "a binary plus shared libraries plus a pile of config files plus a specific runtime version," the cost of ops and support spikes. For someone on a side project with thin time, this was a variable I wanted to kill up front.
Laid out roughly, it looks like this:
| Distribution form | Deploy | What you ask in support | Friction to trying it |
|---|---|---|---|
| Go single static binary | Drop one binary, restart | Just OS and arch | Download one binary, run it |
| Container-first (Python, Node, etc.) | Build / push / pull an image | Runtime version, deps, base image | Install Docker, docker run |
| Native + shared libraries | Binary + a pile of .so + config |
Which libraries, which versions | Resolve deps via a package manager |
The further left you go, the less work you personally handle in the evening. For a solo developer, that gap quietly compounds.
Ops: deploy and rollback are just scp
With a single binary, deployment is "put one build artifact on the server, stop the old process, start the new one." One systemd unit file, one binary to swap. Rollback is just swapping back to the previous binary.
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o gpdf-api ./cmd/gpdf-api
scp gpdf-api server:/opt/gpdf-api/gpdf-api.new
ssh server 'mv /opt/gpdf-api/gpdf-api.new /opt/gpdf-api/gpdf-api && systemctl restart gpdf-api'
CGO_ENABLED=0 matters because with it, you get a binary that depends on none of the build host's C libraries. It doesn't trip over the difference between Alpine's musl and Ubuntu's glibc, ldd shows no dependencies, and you can drop it straight into a scratch image. Half the reason I'm committed to Pure Go and zero-dependency is this ops story; the design narrative is in Building a Pure Go zero-dependency PDF library: gpdf design philosophy.
With a container-first Python or Node app, you get the whole stack on top: pinning requirements.txt, updating base images, building native extensions. With a team, someone runs that. Solo, you run all of it in your own evening hours, and "I'll get to it later" turns into the thing that bites you in three months.
Support: you never have to say "sounds like an environment issue"
Ship OSS and you get "it doesn't work" issues. With a single binary, the odds that the cause is "the environment" drop sharply. If what you hand out is one file and it's statically linked, it runs as long as the OS and architecture match. The "what Python version?" / "is libfoo installed?" / "is your PATH set?" round trips disappear.
The same logic applies on the library side (gpdf and gsql). Because gpdf is Pure Go with no CGO, an app built by importing gpdf can be built straight into a single static binary on the user's side. You structurally don't get "I added gpdf and now I can't cross-compile" questions. The distribution-format story and the library-design story are really two sides of one policy: don't add dependencies.
The cross-compilation matrix is simple too — at release time you just run this:
for os in linux darwin windows; do
for arch in amd64 arm64; do
GOOS=$os GOARCH=$arch CGO_ENABLED=0 go build -o "dist/gpdf-api-$os-$arch" ./cmd/gpdf-api
done
done
If you were using CGO, that for loop turns into the job of assembling cross-compilers and matching libraries for each target. Anyone who's done it knows: that's a day-evaporates job.
"Try it in 30 seconds" becomes trust, as OSS
A single binary makes it easy to create a "try it in 30 seconds" state. Download one binary from the releases page, chmod +x, run it. Whether it's go install or go run, there's no need to go resolve external shared libraries. Projects with low friction-to-trying tend to pick up stars, and more people send issues and PRs.
This matters in the AIO and search context too. When someone searches for something like "a PDF server that runs as a single Go binary," a project that can put ./gpdf-api at the top of its README is more likely to end up on the cited side. Making nadai.dev an author hub on the premise of running multiple products in parallel comes from a similar instinct; the full picture is in nadai ecosystem: running multiple micro SaaS as one person.
Containerizing it, paradoxically, also gets easier
People sometimes frame this as "single binary versus Docker," but it's the other way around: with a single static binary, the Dockerfile is three lines.
FROM scratch
COPY gpdf-api /gpdf-api
ENTRYPOINT ["/gpdf-api"]
Put one static binary into scratch (the empty image) and you're done. Image size is single-digit to low-double-digit MB, and there are no OS packages in there for a CVE scanner to flag in the first place. No apt-get update, no pip install, no periodic base-image refresh. Half of vulnerability response disappears by "not installing things you don't use" — a property that, for a solo developer, was genuinely a relief.
So a single binary isn't "the choice not to use Docker"; it's "the choice that gets lighter whether or not you use Docker." Putting it on Kubernetes? The image is tiny. Settling for scp? One binary. Either way you don't get stuck. Narrowing the distribution format to one keeps your options wide for later, when you want to change where it deploys — that's the substance of "decide it up front and it pays off later."
Where a single binary doesn't fit
Honestly: a single binary isn't always the right call. Bundle heavy static assets (hundreds of MB of font files, machine-learning models) entirely via embed and the binary bloats, and startup and distribution get heavier instead. If you're serious about a plugin mechanism, you need dynamic loading, which means deciding between dealing with the awkwardness of Go's plugin package or going with separate processes plus IPC. GUIs too — a WebView-based setup like the Tauri-targeted gpdf-app won't be "fully one file" (it depends on the OS's WebView).
In gpdf's case, how much of the CJK fonts to bundle into the binary is still something I'm wrestling with; I've ended up with a compromise — embed only the minimal set, load the rest at runtime. Whether that's the right answer, I don't know yet. A single binary is a policy, not a doctrine.
In my case: how it's playing out with gpdf and gsql
When I started gpdf in January 2026 and shipped v1.0.0 under MIT in March, the shape "the library is Pure Go and zero-dependency, the servers and tools on top of it are single static binaries" was decided from the start. More precisely, it was basically locked in the moment I chose Go. gsql is the same — precisely because a SQL builder is a small library, I think of "not adding dependencies" as itself a feature.
gsql is a library with no distributed artifact, so "single binary" doesn't apply to it literally — but the direction it pays off in is the same. When the require block in go.mod is empty, you don't add build time or dependency-resolution flakiness to the projects that import gsql. "Adding it breaks nothing" is, I think, half the reason a small library gets adopted. That distribution format and library design are two faces of "don't add dependencies" got confirmed once more right here.
What's actually paid off, more than ops, is support and the psychological side. I doubt "maybe it's the environment" before opening an issue less often. Release work ends with one for loop, so I stop dreading releases. On a side project, "I'll do it later, it's a hassle" piles up and kills you, so low friction at each step is what carries. I'd thought of it as an ops-load matter at first, but what actually mattered most was "I can keep moving."
The full picture of where I stand is in Building Pure Go micro SaaS on the side: where I stand. The product pages for gpdf and gsql are up too.
Next steps
The distribution format is decided, so what I'll nail down next is the release pipeline for gpdf-api (use GoReleaser, or keep the for loop above) and the bundling scope for the CJK fonts. Whether the font compromise works out, I'll write once I've shipped it.