跳至主要內容
所有文章

用 Docker 建置 Go 應用程式:可攜、封閉的工具鏈

作者 Ghazi Triki · 閱讀時間 7 分鐘

用 Docker 建置 Go 應用程式
本文內容

別再把 Go 裝在主機上。沒有 GOPATH 汙染,沒有「在我的電腦上可以跑」,也完全沒有版本漂移。這正是我們用來改寫 BigBlueButton Learning Analytics 報告的 Java agent、並打造安全的 Piler Go Agent 的做法。以下說明如何只靠 Docker 交付靜態執行檔。

作者:Ghazi Triki,RIADVICE

「直接安裝 Go 就好」的問題

過去我們管理 BigBlueButton Learning Analytics 報告所使用的 Java agent 時,遇上了典型的「相依性地獄」。改用 Go 看似能讓一切變得簡單,但前提是不要重蹈覆轍。我們需要一套建置流程,它要和產出的執行檔一樣封閉、自給自足。

每個團隊遲早都會撞上這幾道牆:

  • 版本漂移。 Ahmed 用 1.24,Omar 用 1.26,正式環境伺服器則是 1.25。編譯器的一個小改動就讓建置失敗,而且只在 Ahmed 的電腦上失敗。
  • 主機遭到汙染。 GOPATH、GOMODCACHE,以及全域安裝的工具(staticcheck、gofumpt),在每台工作站上都不一樣。
  • 新人上手的阻力。 新進工程師的第一天,都耗在與環境變數和 cgo 所需的 C 工具鏈纏鬥。

在 RIADVICE,我們重視紀律與秩序。我們希望建置流程也能反映這一點:乾淨、可預期,而且全員共用。

標準答案是「用 Docker」,但大多數教學只示範如何建置 容器映像。它們沒有告訴您,如何在完全不於本機安裝 Go 編譯器的情況下,在主機上取得一個單純、靜態連結的執行檔,也就是 systemd 服務或離線隔離(air-gapped)部署實際需要的產出物。

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

官方的 golang 映像就是編譯器。您的原始碼以 bind mount 掛載在 /app,執行檔則寫入主機上的 dist/myapp。指令執行完畢後,容器隨即銷毀(--rm),主機也維持和原本一樣乾淨。

實戰經驗:從 BigBlueButton 到 Piler

這不只是理論。我們在兩個關鍵專案中套用了這個做法:

  1. BigBlueButton Learning Analytics 報告: 我們將笨重的 Java agent 改寫為輕量的 Go 執行檔。透過以 Docker 為基礎的建置,我們能精準鎖定 BBB 伺服器上的 Debian 環境,而不必在自己的筆電上準備同樣的環境。
  2. Piler Go Agent: 我們為電子郵件封存解決方案打造了一個經過安全強化的 agent。建置流程必須和程式碼一樣安全。我們將編譯器固定在特定的 SHA,確保執行檔中的每一個位元都有來歷可循。

為什麼這比安裝 Go 更好

考量項目在主機上 go installdocker run golang:...
Go 版本取決於您上次安裝的版本固定版本,所有人都相同
GOPATH/快取汙染 ~/go封閉的容器層
cgo 工具鏈需要在主機上設定已內建於官方映像
與 CI 的一致性「在我的電腦上可以跑」相同映像,相同執行檔
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:紀律的源頭

我們用 Makefile 將建置步驟明文化。請注意 GO_IMAGE 被固定在特定版本。我們從不使用 latest。

真正重要的技術細節

1. 以 CGO_ENABLED=0 產出靜態執行檔

在 Piler agent 中,我們需要的執行檔必須能在各種環境上執行,從新版 Debian 到老舊伺服器都不例外。停用 CGO 可確保執行檔包含所需的一切,不會動態相依於主機上的函式庫。

2. 以 -ldflags -X 標示來源

您的執行檔應該能清楚告訴您它從何而來。我們在連結階段注入 git commit 與建置日期。這在跨數十個不同叢集為 BigBlueButton agent 除錯時至關重要。

3. 固定編譯器映像版本

在我們的文化中,一致性是信念。固定使用 golang:1.26.5-trixie 而不是 latest,代表今天由 Ahmed 建置的執行檔,與六個月後由 Omar 建置的執行檔,會逐位元完全相同。

結論

編譯器是一項工具,而任何工具都該待在它該在的位置。將 Go 工具鏈移進 Docker 之後,我們消除了一整類「在我這裡可以跑」的錯誤,也讓每位加入 RIADVICE 的新工程師更容易上手。

無論您是要改寫 Java agent,還是為 Piler 打造安全的 API,這個做法都能為正式環境系統帶來所需的紀律與可靠性。


Ghazi Triki 是 RIADVICE 的創辦人。我們為 BigBlueButton、Piler 等系統打造高效能、安全的整合方案。讓我們一起打造出色的成果。

🔗 在 GitHub 取得 golang-docker-boilerplate

分享這篇文章