Chuyển đến nội dung chính
Tất cả bài viết

Biên dịch ứng dụng Go bằng Docker: bộ công cụ khả chuyển và khép kín

bởi Ghazi Triki · 10 phút đọc

Biên dịch ứng dụng Go bằng Docker
Trong bài viết này

Đừng cài Go lên máy chủ của bạn nữa. Không làm bẩn GOPATH, không còn chuyện "máy tôi chạy được", không lệch phiên bản. Đây chính là mô hình chúng tôi dùng để viết lại các agent Java cho báo cáo BigBlueButton Learning Analytics và xây dựng Piler Go Agent bảo mật. Sau đây là cách phát hành binary tĩnh chỉ với Docker.

Bài viết của Ghazi Triki, RIADVICE

Vấn đề của lời khuyên "cứ cài Go là xong"

Khi còn quản lý các agent viết bằng Java cho báo cáo BigBlueButton Learning Analytics, chúng tôi đã gặp phải "địa ngục phụ thuộc" kinh điển. Chuyển sang Go hứa hẹn sự đơn giản, nhưng chỉ khi chúng tôi không lặp lại những sai lầm cũ. Chúng tôi cần một quy trình biên dịch khép kín như chính những binary mà nó tạo ra.

Sớm hay muộn, nhóm nào cũng vấp phải những bức tường sau:

  • Lệch phiên bản. Ahmed dùng 1.24, Omar dùng 1.26, còn máy chủ production chạy 1.25. Một thay đổi nhỏ trong trình biên dịch làm hỏng bản build, nhưng chỉ trên máy của Ahmed.
  • Máy chủ bị làm bẩn. GOPATH, GOMODCACHE và các công cụ cài đặt toàn cục (staticcheck, gofumpt) khác nhau trên từng máy trạm.
  • Khó khăn khi tiếp nhận người mới. Một kỹ sư mới mất trọn ngày đầu tiên vật lộn với biến môi trường và bộ công cụ C cho cgo.

Tại RIADVICE, chúng tôi coi trọng kỷ luật và trật tự. Chúng tôi muốn một quy trình biên dịch phản ánh điều đó: gọn gàng, có thể dự đoán và dùng chung.

Câu trả lời quen thuộc là "hãy dùng Docker", nhưng phần lớn các bài hướng dẫn chỉ cho bạn cách build một container image. Chúng không chỉ cho bạn cách có được một binary liên kết tĩnh thuần túy ngay trên máy của bạn, tức sản phẩm bạn thực sự cần cho một dịch vụ systemd hay một môi trường triển khai cách ly mạng (air-gapped), mà không cần cài trình biên dịch Go cục bộ.

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

Image golang chính thức đóng vai trò trình biên dịch. Mã nguồn của bạn được bind-mount vào /app. Binary được ghi ra dist/myapp trên máy của bạn. Khi lệnh kết thúc, container bị hủy (--rm), máy của bạn vẫn sạch sẽ như trước.

Kinh nghiệm thực tế: từ BigBlueButton đến Piler

Đây không chỉ là lý thuyết. Chúng tôi đã áp dụng mô hình này cho hai dự án quan trọng:

  1. BigBlueButton Learning Analytics Reports: Chúng tôi đã viết lại các agent Java nặng nề thành những binary Go gọn nhẹ. Nhờ biên dịch bằng Docker, chúng tôi có thể nhắm chính xác đến môi trường Debian của các máy chủ BBB mà không cần môi trường đó trên laptop của mình.
  2. Piler Go Agent: Với giải pháp lưu trữ email của mình, chúng tôi đã xây dựng một agent được gia cố bảo mật. Quy trình biên dịch phải an toàn như chính mã nguồn. Bằng cách cố định trình biên dịch vào một SHA cụ thể, chúng tôi bảo đảm kiểm soát được từng bit trong binary.

Vì sao cách này tốt hơn cài Go

Vấn đềgo install trên máy chủdocker run golang:...
Phiên bản GoBản nào bạn cài gần nhấtĐược cố định, giống hệt nhau cho mọi người
GOPATH / cacheLàm bẩn ~/goCác lớp container khép kín
Bộ công cụ cgoCần thiết lập trên máy chủCó sẵn trong image chính thức
Đồng nhất CI"Máy tôi chạy được"Image giống hệt, binary giống hệt
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: nền tảng của kỷ luật

Chúng tôi dùng một Makefile để mã hóa các bước biên dịch. Hãy để ý cách GO_IMAGE được cố định ở một phiên bản cụ thể. Chúng tôi không bao giờ dùng latest.

Những chi tiết kỹ thuật quan trọng

1. CGO_ENABLED=0 cho binary tĩnh

Với agent Piler, chúng tôi cần những binary có thể chạy trên mọi hệ thống, từ Debian hiện đại đến máy chủ đời cũ. Tắt CGO bảo đảm binary chứa mọi thứ nó cần, không phụ thuộc động vào thư viện của máy chủ.

2. -ldflags -X để truy xuất nguồn gốc

Binary của bạn phải cho bạn biết chính xác nó đến từ đâu. Chúng tôi chèn commit git và ngày build vào lúc liên kết. Điều này cực kỳ quan trọng khi gỡ lỗi các agent BigBlueButton trên hàng chục cụm máy chủ khác nhau.

3. Cố định image trình biên dịch

Trong văn hóa của chúng tôi, sự nhất quán là nguyên tắc. Cố định golang:1.26.5-trixie thay vì dùng latest nghĩa là nếu hôm nay Ahmed build binary và sáu tháng sau Omar build lại, kết quả vẫn giống hệt nhau đến từng bit.

Kết luận

Trình biên dịch là một công cụ, và như mọi công cụ khác, nó cần được đặt đúng chỗ. Khi chuyển bộ công cụ Go vào Docker, chúng tôi đã loại bỏ cả một nhóm lỗi kiểu "máy tôi chạy được" và đơn giản hóa việc tiếp nhận mọi kỹ sư mới gia nhập RIADVICE.

Dù bạn đang viết lại các agent Java hay xây dựng một API bảo mật cho Piler, mô hình này mang lại kỷ luật và độ tin cậy mà các hệ thống production đòi hỏi.


Ghazi Triki là người sáng lập RIADVICE. Chúng tôi xây dựng các giải pháp tích hợp hiệu năng cao, bảo mật cho BigBlueButton, Piler và nhiều nền tảng khác. Hãy cùng nhau xây dựng điều tuyệt vời.

🔗 Tải golang-docker-boilerplate trên GitHub

Chia sẻ bài viết này