Membangun Aplikasi Go dengan Docker – Toolchain yang Portabel dan Hermetis
Stop installing Go on your host. No
GOPATHpollution, no “works on my machine,” and zero version drift. This is the exact pattern we used to rewrite Java agents for BigBlueButton Learning Analytics reports and to build the secure Piler Go Agent. Here is how you can ship static binaries using only Docker.
By Ghazi Triki, RIADVICE
The problem with “just install Go”
When we were managing Java-based agents for BigBlueButton Learning Analytics reports , we faced the classic “dependency hell.” Moving to Go promised simplicity, but only if we didn’t repeat the same mistakes. We needed a build process that was as hermetic as the binaries it produced.
Every team eventually hits these walls:
- Version drift. Ahmed is on 1.24, Omar is on 1.26, and the production server is on 1.25. A small compiler change breaks the build, but only for Ahmed.
- Polluted hosts.
GOPATH,GOMODCACHE, and global tool installations (staticcheck,gofumpt) that differ across every workstation. - Onboarding friction. A new engineer spends their first day wrestling with environment variables and C toolchains for
cgo.
At RIADVICE, we value discipline and order. We wanted a build process that reflected that-clean, predictable, and shared.
The standard answer is “use Docker,” but most tutorials only show you how to build a container image. They don’t show you how to get a plain, statically linked binary on your host—the artifact you actually need for a systemd service or an air-gapped deployment—without ever installing the Go compiler locally.
docker run --rm -v "$(pwd):/app" -w /app golang:1.26.5-trixie \
go build -o dist/myapp ./cmd/myapp
The official golang image is the compiler. Your source is bind-mounted at /app. The binary is written to dist/myapp on your host. When the command finishes, the container is destroyed (--rm), leaving your host as clean as it was before.
Real-world experience: From BigBlueButton to Piler
This isn’t just theory. We applied this pattern to two critical projects:
- BigBlueButton Learning Analytics Reports : We rewrote heavy Java agents into lightweight Go binaries. Using Docker-based builds allowed us to target the exact Debian environment of the BBB servers without needing that environment on our laptops.
- Piler Go Agent: For our email archiving solution, we built a security-hardened agent. The build process had to be as secure as the code. By pinning the compiler to a specific SHA, we ensured that every bit in the binary was accounted for.
Why this is better than installing Go
| Concern | go install on host |
docker run golang:... |
|---|---|---|
| Go version | Whatever you last installed | Pinned, identical for everyone |
GOPATH / cache | Pollutes ~/go | Hermetic container layers |
cgo toolchain | Requires host setup | Bundled in the official image |
| CI parity | “Works on my machine” | Identical image, identical binary |
BINARY := mini-api
DIST := dist
# Version metadata derived from git
VERSION := $(shell tmp=$$(git describe --tags --always --dirty 2>/dev/null) && echo "$${tmp#v}" || echo dev)
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)
BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -X example.com/mini-api/internal/version.Version=$(VERSION) \
-X example.com/mini-api/internal/version.Commit=$(COMMIT) \
-X example.com/mini-api/internal/version.Branch=$(BRANCH) \
-X example.com/mini-api/internal/version.BuildDate=$(BUILD_DATE)
# Pinned toolchain image
GO_IMAGE := golang:1.26.5-trixie
# Run any command inside the Go toolchain container
define run_in_docker
docker run --rm -v "$(PWD):/app" -w /app $(GO_IMAGE)
endef
.PHONY: build test lint
build:
@mkdir -p $(DIST)
$(call run_in_docker) bash -c 'CGO_ENABLED=0 go build -buildvcs=false -ldflags="$(LDFLAGS)" -o $(DIST)/$(BINARY) ./cmd/$(BINARY)'
@echo "Built $(DIST)/$(BINARY) (version $(VERSION))"
test:
$(call run_in_docker) go test -race ./...
lint:
$(call run_in_docker) go vet ./...
The Makefile – The Source of Discipline
We use a Makefile to codify the build steps. Notice how GO_IMAGE is pinned to a specific version. We never use latest.
The technical details that matter
1. CGO_ENABLED=0 for static binaries
In the Piler agent, we needed binaries that could run on anything from a modern Debian to a legacy server. Disabling CGO ensures the binary contains everything it needs, with no dynamic dependencies on host libraries.
2. -ldflags -X for provenance
Your binary should be able to tell you exactly where it came from. We inject the git commit and build date at link time. This was vital for debugging BigBlueButton agents across dozens of different clusters.
3. Pinning the compiler image
In our culture, we believe in consistency. Pinning golang:1.26.5-trixie instead of using latest means that if Ahmed builds the binary today and Omar builds it in six months, the output is bit-for-bit identical.
Conclusion
The compiler is a tool, and like any tool, it should be kept in its place. By moving the Go toolchain into Docker, we've eliminated a whole class of "it works for me" bugs and simplified our onboarding for every new engineer who joins RIADVICE.
Whether you're rewriting Java agents or building a secure API for Piler, this pattern provides the discipline and reliability required for production systems.
Ghazi Triki is the founder of RIADVICE. We build high-performance, secure integrations for BigBlueButton, Piler, and more, let's build something great.
🔗 Get the golang-docker-boilerplate on GitHub
