Przejdź do treści
Wszystkie artykuły

Budowanie aplikacji Go w Dockerze: przenośny, hermetyczny łańcuch narzędzi

autor: Ghazi Triki · 9 min czytania

Budowanie aplikacji Go w Dockerze
W tym artykule

Koniec z instalowaniem Go na hoście. Bez zaśmiecania GOPATH, bez „u mnie działa” i bez rozjazdu wersji. Według tego wzorca przepisaliśmy agentów Java dla raportów BigBlueButton Learning Analytics i zbudowaliśmy bezpiecznego agenta Piler Go. Oto jak dostarczać statyczne pliki binarne, korzystając wyłącznie z Dockera.

Ghazi Triki, RIADVICE

Problem z podejściem „po prostu zainstaluj Go”

Gdy utrzymywaliśmy agentów napisanych w Javie dla raportów BigBlueButton Learning Analytics, zmagaliśmy się z klasycznym „piekłem zależności”. Przejście na Go obiecywało prostotę, ale tylko pod warunkiem, że nie powtórzymy tych samych błędów. Potrzebowaliśmy procesu budowania równie hermetycznego jak pliki binarne, które z niego wychodzą.

Prędzej czy później każdy zespół napotyka te same przeszkody:

  • Rozjazd wersji. Ahmed ma 1.24, Omar ma 1.26, a serwer produkcyjny 1.25. Drobna zmiana w kompilatorze psuje build, ale tylko u Ahmeda.
  • Zaśmiecone hosty. GOPATH, GOMODCACHE i globalnie instalowane narzędzia (staticcheck, gofumpt), które na każdej stacji roboczej wyglądają inaczej.
  • Trudny start nowych osób. Nowy inżynier spędza pierwszy dzień na walce ze zmiennymi środowiskowymi i łańcuchami narzędzi C dla cgo.

W RIADVICE cenimy dyscyplinę i porządek. Chcieliśmy procesu budowania, który to odzwierciedla: czystego, przewidywalnego i wspólnego dla wszystkich.

Standardowa odpowiedź brzmi „użyj Dockera”, ale większość poradników pokazuje jedynie, jak zbudować obraz kontenera. Nie pokazują, jak uzyskać zwykły, statycznie linkowany plik binarny na hoście, czyli artefakt, którego faktycznie potrzeba dla usługi systemd lub wdrożenia w środowisku odizolowanym od sieci, bez instalowania kompilatora Go lokalnie.

Shell
docker run --rm -v "$(pwd):/app" -w /app golang:1.26.5-trixie \
    go build -o dist/myapp ./cmd/myapp

Oficjalny obraz golang pełni rolę kompilatora. Kod źródłowy jest montowany (bind mount) w /app. Plik binarny trafia do dist/myapp na hoście. Po zakończeniu polecenia kontener jest usuwany (--rm), a host pozostaje tak czysty jak wcześniej.

Doświadczenia z praktyki: od BigBlueButton do Pilera

To nie jest wyłącznie teoria. Zastosowaliśmy ten wzorzec w dwóch kluczowych projektach:

  1. BigBlueButton Learning Analytics Reports: przepisaliśmy ciężkich agentów Java na lekkie pliki binarne Go. Budowanie w Dockerze pozwoliło nam celować dokładnie w środowisko Debiana na serwerach BBB, bez potrzeby odtwarzania go na naszych laptopach.
  2. Piler Go Agent: dla naszego rozwiązania do archiwizacji poczty e-mail zbudowaliśmy agenta o wzmocnionych zabezpieczeniach. Proces budowania musiał być równie bezpieczny jak sam kod. Przypinając kompilator do konkretnego SHA, mieliśmy pewność, że każdy bit pliku binarnego ma znane pochodzenie.

Dlaczego to lepsze niż instalowanie Go

Kwestiago install na hościedocker run golang:...
Wersja GoTa, którą zainstalowano ostatnioPrzypięta, identyczna dla wszystkich
GOPATH / cacheZaśmieca ~/goHermetyczne warstwy kontenera
Toolchain cgoWymaga konfiguracji hostaZawarty w oficjalnym obrazie
Zgodność z CI„U mnie działa”Identyczny obraz, identyczny plik binarny
Makefile
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 ./...

Makefile: źródło dyscypliny

Kroki budowania zapisujemy w pliku Makefile. Proszę zwrócić uwagę, że GO_IMAGE jest przypięty do konkretnej wersji. Nigdy nie używamy latest.

Szczegóły techniczne, które mają znaczenie

1. CGO_ENABLED=0 dla statycznych plików binarnych

W agencie Piler potrzebowaliśmy plików binarnych, które uruchomią się wszędzie, od nowoczesnego Debiana po stary serwer. Wyłączenie CGO sprawia, że plik binarny zawiera wszystko, czego potrzebuje, bez dynamicznych zależności od bibliotek hosta.

2. -ldflags -X dla pochodzenia

Plik binarny powinien umieć dokładnie powiedzieć, skąd pochodzi. W momencie linkowania wstrzykujemy commit z gita i datę budowania. Okazało się to kluczowe przy debugowaniu agentów BigBlueButton w dziesiątkach różnych klastrów.

3. Przypinanie obrazu kompilatora

W naszej kulturze stawiamy na spójność. Przypięcie golang:1.26.5-trixie zamiast latest oznacza, że jeśli Ahmed zbuduje plik binarny dziś, a Omar za pół roku, wynik będzie identyczny co do bitu.

Podsumowanie

Kompilator to narzędzie, a każde narzędzie powinno mieć swoje miejsce. Przenosząc łańcuch narzędzi Go do Dockera, wyeliminowaliśmy całą klasę błędów typu „u mnie działa” i uprościliśmy wdrażanie każdego nowego inżyniera, który dołącza do RIADVICE.

Niezależnie od tego, czy przepisują Państwo agentów Java, czy budują bezpieczne API dla Pilera, ten wzorzec zapewnia dyscyplinę i niezawodność, jakich wymagają systemy produkcyjne.


Ghazi Triki jest założycielem RIADVICE. Budujemy wydajne i bezpieczne integracje dla BigBlueButton, Pilera i nie tylko. Zbudujmy razem coś wartościowego.

🔗 Repozytorium golang-docker-boilerplate w serwisie GitHub

Udostępnij ten artykuł