Single binary 配布が個人開発者に有利な理由
Go の single binary 配布が、運用・サポート・OSS 信頼の 3 軸でなぜ個人開発の現実に合うか。向かない場面も含めて実例で書く。
配布形態は最初に決めておくと後で効く
個人開発者がプロダクトの配布形態を選ぶなら、可能な限り single binary に倒す。Go を選んでいるならほぼ無料で手に入る性質で、これを捨てる理由はあまりない。理由は 3 つあって、運用が軽くなる、サポートが軽くなる、OSS としての信頼を取りやすい。順に書く。
ここで言う single binary は、Go の静的リンクとクロスコンパイルで作る「実行ファイル 1 個で動く」形態のこと。gpdf-api のようなサーバープロセスでも、付属の CLI ツールでも、配布物が 1 ファイルに収まるなら、その後のあらゆる工程が単純になる。逆に言えば、配布物が「バイナリ + 共有ライブラリ + 設定ファイル群 + 特定バージョンのランタイム」になった瞬間、運用とサポートのコストが跳ね上がる。兼業で時間が薄い人間にとって、ここは最初に潰しておきたい変数だった。
ざっくり並べるとこうなる。
| 配布形態 | デプロイ | サポートで聞くこと | 試すまでの摩擦 |
|---|---|---|---|
| Go の single static binary | バイナリ 1 個を置いて再起動 | OS と arch だけ | バイナリ 1 個落として実行 |
| コンテナ前提 (Python や Node 系) | イメージを build / push / pull | ランタイム版、依存、ベースイメージ | Docker を入れて docker run |
| ネイティブ + 共有ライブラリ | バイナリ + .so 群 + 設定 |
ライブラリの有無とバージョン | パッケージマネージャで依存解決 |
左に行くほど、自分が夜に対応する作業が減る。個人開発者にとってこの差は地味に効いてくる。
運用: デプロイもロールバックも scp で終わる
single binary だと、デプロイは「ビルド成果物を 1 個サーバーに置いて、古いプロセスを止めて、新しいのを起動する」で終わる。systemd の unit ファイル 1 個と、置き換えるバイナリ 1 個。ロールバックも前のバイナリに差し替えるだけだ。
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 が効いているのは、これがあるとビルドホストの C ライブラリに一切依存しないバイナリが出るからだ。Alpine の musl と Ubuntu の glibc の違いに引っかからない、ldd で出てくる依存がない、scratch イメージにそのまま放り込める。Pure Go / zero-dependency にこだわっているのは半分この運用のためで、設計の物語のほうは Pure Go で 0 dependency の PDF ライブラリを作った話 に書いた。
これがコンテナ前提の Python や Node のアプリだと、requirements.txt の pin、ベースイメージの更新、ネイティブ拡張のビルド、という層が全部乗ってくる。チームがいればそれを回す人がいるが、一人だと全部自分の夜の時間で回すことになる。
サポート: 「環境依存ですね」を言わなくて済む
OSS を出すと「動きません」という issue が来る。single binary だと、その issue の原因が「環境」である確率がぐっと下がる。配るものが 1 ファイルで、それが静的リンクされていれば、相手の OS とアーキテクチャさえ合っていれば動く。「Python のバージョンは?」「libfoo は入ってる?」「PATH は通ってる?」という往復が消える。
ライブラリ側 (gpdf や gsql) でも同じ理屈が効く。gpdf は CGO を使わない Pure Go なので、gpdf を import して作ったアプリは、利用者の側でそのまま single static binary にビルドできる。「gpdf を入れたらクロスコンパイルできなくなった」という相談が構造的に起きない。配布形態の話とライブラリ設計の話は、実は「依存を増やさない」という一本の方針の両面だった。
クロスコンパイルのマトリクスも単純で、リリース時はこれを回すだけになる。
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
CGO を使っていたら、この for ループは各ターゲット用のクロスコンパイラとライブラリを揃える作業に化ける。やったことがある人は分かると思うが、あれは一日が溶ける作業だった。
OSS としての「すぐ試せる」が信頼になる
single binary は「30 秒で試せる」状態を作りやすい。リリースページからバイナリを 1 個落として、chmod +x して、叩く。go install でも go run でも、外部の共有ライブラリを解決しに行く必要がない。試すまでの摩擦が小さいプロジェクトは、star も付きやすいし、issue や PR をくれる人も増える。
これは AIO や検索の文脈でも効く。「Go の single binary で動く PDF サーバー」みたいな探され方をしたとき、README の最初に ./gpdf-api で動くと書けるプロジェクトは、引用される側に回りやすい。複数プロダクトを並走させる前提で nadai.dev を作者ハブにしているのも近い発想で、その全体像は nadai ecosystem: 複数 micro SaaS を一人で運営する戦略 に書いた。
コンテナにするのも、逆に簡単になる
「single binary か Docker か」みたいな対立で語られることがあるが、実際は逆で、single static binary だと Dockerfile が 3 行で済む。
FROM scratch
COPY gpdf-api /gpdf-api
ENTRYPOINT ["/gpdf-api"]
scratch (空のイメージ) に静的バイナリを 1 個置いて終わり。イメージサイズは数 MB から十数 MB で、CVE スキャンに引っかかる OS パッケージがそもそも入っていない。apt-get update も pip install も、ベースイメージの定期更新も要らない。脆弱性対応の半分は「使っていないものを入れない」ことで消える、というのは個人開発者にとってはありがたい性質だった。
つまり single binary は「Docker を使わない選択」ではなく、「Docker を使うときも使わないときも、どちらも軽くなる選択」だ。Kubernetes に載せるならイメージが軽い、scp で済ませるならバイナリ 1 個、どちらに転んでも詰まらない。配布形態を 1 個に絞っておくと、後でデプロイ先を変えるときの選択肢が広いまま残る、というのが「最初に決めておくと後で効く」の中身だった。
single binary が向かない場面
正直に書くと、何でも single binary がいいわけではない。重い静的アセット (フォントファイル数百 MB、機械学習モデル) を embed で全部抱え込むとバイナリが肥大して、起動も配布も逆に重くなる。プラグイン機構を本気でやるなら動的ロードが要るので、Go の plugin パッケージの面倒くささに付き合うか、別プロセス + IPC にするかの判断が出てくる。GUI も、Tauri 想定の gpdf-app のように WebView を使う構成だと「完全に 1 ファイル」とはいかない (OS の WebView に依存する)。
gpdf の場合、CJK フォントをどこまでバイナリに同梱するかは今も悩みどころで、結局「最小セットだけ embed、残りは実行時に読む」という折衷にしている。これが正解かはまだ分かっていない。single binary は方針であって、教義ではない。
私の場合: gpdf と gsql でどう効いているか
gpdf を 2026 年 1 月に書き始めて 3 月に v1.0.0 を MIT で出したとき、「ライブラリは Pure Go / zero-dependency、その上に乗るサーバーやツールは single static binary」という形は最初から決めていた。決めていたというより、Go を選んだ時点でほぼ確定していた、と言うほうが近い。gsql も同じで、SQL ビルダーという小さなライブラリだからこそ、依存を増やさないことそのものが機能だと考えている。
gsql のほうは配布物がないライブラリなので「single binary」という言葉が直接は当てはまらないが、効いている方向は同じだった。go.mod の require が空だと、gsql を import する側のプロジェクトのビルド時間も依存解決の不安定さも増やさない。「入れても何も壊れない」というのは、小さなライブラリが採用される理由の半分を占めると思っている。配布形態とライブラリ設計が「依存を増やさない」の両面だ、というのはここでもう一度確認できた。
実感として効いているのは、運用よりむしろサポートと心理のほうだった。issue を開く前に「これ環境のせいかも」と疑う回数が減る。リリース作業が for ループ 1 個で終わるので、リリースを面倒くさがらなくなる。兼業だと「面倒だから後回し」が積もって死ぬので、各工程の摩擦が小さいことが効いてくる。最初は運用負荷の話として考えていたが、実際にいちばん効いたのは「自分が動き続けられる」ことのほうだった。
詳しい現在地は Pure Go の micro SaaS を兼業で出している現在地 に書いた。あわせて gpdf と gsql のプロダクトページも置いてある。
次のステップ
配布形態は決まっているので、次に詰めるのは gpdf-api のリリースパイプライン (GoReleaser を使うか、上の for ループのままにするか) と、CJK フォントの同梱範囲だ。フォントの折衷案がうまくいくかは、出してみてから書く。