В этой статье
Перестаньте устанавливать Go на свою машину. Никакого засорения
GOPATH, никакого «у меня всё работает» и никакого расхождения версий. Именно этот подход мы применили, чтобы переписать агенты на Java для отчётов BigBlueButton Learning Analytics и создать защищённый Piler Go Agent. Вот как выпускать статические бинарные файлы, используя только Docker.
Автор: Ghazi Triki, RIADVICE
Чем плохо «просто установить Go»
Когда мы сопровождали агенты на Java для отчётов BigBlueButton Learning Analytics, мы столкнулись с классическим «адом зависимостей». Переход на Go обещал простоту, но только при условии, что мы не повторим прежних ошибок. Нам был нужен процесс сборки, столь же герметичный, как и создаваемые им бинарные файлы.
Рано или поздно каждая команда упирается в эти стены:
- Расхождение версий. У Ахмеда 1.24, у Омара 1.26, а на продуктивном сервере 1.25. Небольшое изменение в компиляторе ломает сборку, но только у Ахмеда.
- Засорённые машины.
GOPATH,GOMODCACHEи глобально установленные инструменты (staticcheck,gofumpt), которые на каждой рабочей станции свои. - Трудности при вводе в работу. Новый инженер проводит первый день в борьбе с переменными окружения и инструментарием C для
cgo.
В RIADVICE мы ценим дисциплину и порядок. Нам хотелось процесса сборки, который бы это отражал: чистого, предсказуемого и общего для всех.
Стандартный ответ звучит как «используйте Docker», но большинство руководств показывают лишь, как собрать образ контейнера. Они не показывают, как получить обычный статически скомпонованный бинарный файл на своей машине, то есть именно тот артефакт, который нужен для службы systemd или для развёртывания в изолированной сети, вообще не устанавливая компилятор Go локально.
docker run --rm -v "$(pwd):/app" -w /app golang:1.26.5-trixie \
go build -o dist/myapp ./cmd/myappОфициальный образ golang и есть компилятор. Исходный код подключается в /app через bind mount. Бинарный файл записывается в dist/myapp на вашей машине. Когда команда завершается, контейнер уничтожается (--rm), и ваша машина остаётся такой же чистой, как была.
Практический опыт: от BigBlueButton до Piler
Это не просто теория. Мы применили этот подход в двух важнейших проектах:
- Отчёты BigBlueButton Learning Analytics: мы переписали тяжёлые агенты на Java в лёгкие бинарные файлы Go. Сборка в Docker позволила ориентироваться на точную среду Debian серверов BBB, не имея этой среды на наших ноутбуках.
- Piler Go Agent: для нашего решения по архивированию электронной почты мы создали агент с усиленной защитой. Процесс сборки должен был быть так же защищён, как и сам код. Закрепив компилятор по конкретному SHA, мы гарантировали, что происхождение каждого бита бинарного файла известно.
Чем это лучше установки Go
| Аспект | go install на машине | docker run golang:... |
|---|---|---|
| Версия Go | Та, что установлена последней | Закреплена, одинакова у всех |
| GOPATH / кэш | Засоряет ~/go | Герметичные слои контейнера |
| Инструменты cgo | Требуют настройки машины | Входят в официальный образ |
| Паритет с CI | «У меня всё работает» | Тот же образ, тот же бинарный файл |
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: источник дисциплины
Мы используем Makefile, чтобы зафиксировать шаги сборки в коде. Обратите внимание, что GO_IMAGE закреплён на конкретной версии. Мы никогда не используем latest.
Технические детали, которые имеют значение
1. CGO_ENABLED=0 для статических бинарных файлов
Для агента Piler нам были нужны бинарные файлы, способные работать где угодно, от современного Debian до устаревшего сервера. Отключение CGO гарантирует, что бинарный файл содержит всё необходимое и не имеет динамических зависимостей от библиотек системы.
2. -ldflags -X для отслеживания происхождения
Бинарный файл должен уметь точно сообщить, откуда он взялся. Мы внедряем коммит git и дату сборки на этапе компоновки. Это было крайне важно для отладки агентов BigBlueButton в десятках разных кластеров.
3. Закрепление образа компилятора
В нашей культуре мы ценим единообразие. Закрепление golang:1.26.5-trixie вместо latest означает, что если Ахмед соберёт бинарный файл сегодня, а Омар через полгода, результат будет идентичен бит в бит.
Заключение
Компилятор является инструментом, и, как любой инструмент, он должен знать своё место. Перенеся инструментарий Go в Docker, мы избавились от целого класса ошибок типа «а у меня работает» и упростили ввод в работу каждого нового инженера, который приходит в RIADVICE.
Переписываете ли вы агенты на Java или создаёте защищённый API для Piler, этот подход обеспечивает дисциплину и надёжность, которых требуют продуктивные системы.
Ghazi Triki, основатель RIADVICE. Мы создаём высокопроизводительные и защищённые интеграции для BigBlueButton, Piler и других систем. Давайте создадим что-то выдающееся вместе.





