본문으로 건너뛰기
전체 글

Docker로 Go 애플리케이션 빌드하기: 이식 가능하고 격리된 툴체인

Ghazi Triki 작성 · 9분 분량

Docker로 Go 애플리케이션 빌드하기
목차

호스트에 Go를 설치하지 마십시오. GOPATH 오염도, "제 컴퓨터에서는 되는데"도, 버전 차이도 없습니다. BigBlueButton Learning Analytics 보고서용 Java 에이전트를 다시 작성하고 보안을 강화한 Piler Go Agent를 빌드할 때 저희가 실제로 쓴 패턴입니다. Docker만으로 정적 바이너리를 배포하는 방법을 소개합니다.

글: Ghazi Triki, RIADVICE

"그냥 Go를 설치하면 된다"의 문제

BigBlueButton Learning Analytics 보고서용 Java 기반 에이전트를 관리하던 시절, 저희는 전형적인 "의존성 지옥"을 겪었습니다. Go로 옮기면 단순해질 것 같았지만, 그것도 같은 실수를 반복하지 않을 때의 이야기였습니다. 저희에게는 결과물인 바이너리만큼이나 격리된(hermetic) 빌드 프로세스가 필요했습니다.

어느 팀이든 결국 다음과 같은 벽에 부딪힙니다.

  • 버전 차이. 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 이미지가 곧 컴파일러입니다. 소스 코드는 /app에 바인드 마운트됩니다. 바이너리는 호스트의 dist/myapp에 기록됩니다. 명령이 끝나면 컨테이너는 삭제되므로(--rm), 호스트는 이전과 똑같이 깨끗하게 유지됩니다.

실전 경험: BigBlueButton에서 Piler까지

이것은 이론에 그치지 않습니다. 저희는 이 패턴을 두 가지 핵심 프로젝트에 적용했습니다.

  1. BigBlueButton Learning Analytics 보고서: 무거운 Java 에이전트를 가벼운 Go 바이너리로 다시 작성했습니다. Docker 기반 빌드 덕분에, 노트북에 그 환경을 갖추지 않고도 BBB 서버의 Debian 환경을 정확히 겨냥할 수 있었습니다.
  2. Piler Go Agent: 이메일 아카이빙 솔루션을 위해 보안을 강화한 에이전트를 만들었습니다. 빌드 프로세스도 코드만큼 안전해야 했습니다. 컴파일러를 특정 SHA에 고정함으로써 바이너리의 모든 비트가 어디서 왔는지 설명할 수 있게 했습니다.

Go를 직접 설치하는 것보다 나은 이유

관심사go install on hostdocker 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 에이전트에는 최신 Debian부터 오래된 레거시 서버까지 어디서나 실행되는 바이너리가 필요했습니다. CGO를 끄면 바이너리가 필요한 모든 것을 자체적으로 포함하게 되어, 호스트 라이브러리에 대한 동적 의존성이 사라집니다.

2. 출처 추적을 위한 -ldflags -X

바이너리는 자신이 정확히 어디서 왔는지 알려 줄 수 있어야 합니다. 저희는 링크 시점에 git 커밋과 빌드 날짜를 주입합니다. 수십 개의 서로 다른 클러스터에 걸쳐 BigBlueButton 에이전트를 디버깅할 때 이 정보가 결정적인 역할을 했습니다.

3. 컴파일러 이미지 고정

저희 문화는 일관성을 중시합니다. latest 대신 golang:1.26.5-trixie로 고정하면, Ahmed가 오늘 바이너리를 빌드하고 Omar가 6개월 뒤에 빌드해도 결과물은 비트 단위까지 동일합니다.

결론

컴파일러는 도구이며, 모든 도구가 그렇듯 제자리에 두어야 합니다. Go 툴체인을 Docker 안으로 옮김으로써 저희는 "제 환경에서는 됩니다" 유형의 버그를 통째로 없앴고, RIADVICE에 합류하는 모든 새 엔지니어의 온보딩을 단순하게 만들었습니다.

Java 에이전트를 다시 작성하든 Piler용 보안 API를 구축하든, 이 패턴은 프로덕션 시스템에 필요한 규율과 신뢰성을 제공합니다.


Ghazi Triki는 RIADVICE의 설립자입니다. 저희는 BigBlueButton, Piler 등을 위한 고성능의 안전한 연동을 구축합니다. 함께 멋진 것을 만들어 보십시오.

🔗 GitHub에서 golang-docker-boilerplate 받기

이 글 공유하기

  • Open Source · 9분 분량

    Sukarix 0.4.0 출시: 실제 고부하 파이프라인을 위해 만든 고처리량 Redis 큐잉

    원자적 Lua 실행, 기본 내장된 멤버십 기반 중복 제거, 구조화된 진행 상황 텔레메트리, 엄격한 의존성 주입. Sukarix 0.4.0이 프레임워크의 군살 없이 엔터프라이즈급 성능을 제공하는 방법과, 저희의 실시간 BigBlueButton Analytics 파이프라인을 움직이는 방식을 소개합니다.

  • 7년에서 7일로
    Education · 8분 분량

    에이전틱 AI로 7년을 7일로

    팀이 스프린트 두 번 만에 출시한 프로젝트에 대해 COCOMO II 스프레드시트가 33개월을 예측하는 것을 보신 적이 있다면, 문제를 이미 알고 계실 것입니다. 추정 모델이 더 이상 존재하지 않는 세계에 맞춰 보정되어 있다는 것입니다.