Go (Golang) 语言因其编译成静态二进制文件、天然支持并发以及方便的跨平台编译等特性,非常适合容器化部署。本篇将重点介绍如何使用多阶段构建 (Multi-stage builds) 来为 Go 应用创建优化、小巧且安全的 Docker 镜像。
背景介绍
Go 语言的特点使其非常适合容器化:
- 静态编译:生成的二进制文件包含所有依赖,不需要额外的运行时
- 高效并发:通过 goroutine 和 channel 机制提供简单易用的并发模型
- 跨平台能力:轻松实现不同操作系统和架构的编译
我们将遵循 设计篇: 04-Dockerfile设计原则与镜像管理规范 中的逻辑分层思想,采用标准化的多阶段构建方式:
- 编译阶段:使用上篇构建的 debian:bullseye 镜像作为基础系统,在编译阶段使用 golang:1.24-bullseye 镜像作为编译环境,提供完整的 Go 开发工具链
- 运行阶段:构建极简的运行基础镜像,基础镜像使用 debian:bullseye 镜像,并使用 nonroot 用户运行应用
- 应用镜像:将编译好的二进制文件从编译阶段拷贝到运行阶段镜像,实现精简部署
这种方法可以大幅减小最终镜像体积(通常从几百 MB 减小到几十 MB 甚至几 MB),同时提高安全性和部署效率。
构建 Go 应用的编译环境
编译环境负责提供完整的 Go 语言开发工具链和必要的编译依赖。我们基于 Debian 创建一个功能齐全的 Go 编译环境。
编译环境 Dockerfile 详解
创建 Go 编译环境的目录
mkdir -p common/tools/golang
cd common/tools/golang
下面是一个基于 Debian 的 Go 编译环境镜像示例:
#syntax=harbor.leops.local/library/docker/dockerfile:1
FROM harbor.leops.local/common/os/debian:bullseye
ARG GOLANG_VERSION=1.24.2
LABEL GOLANG_VERSION="${GOLANG_VERSION}" \
org.opencontainers.image.maintainer="ops@leops.local" \
org.opencontainers.image.source="http://git.leops.local/ops/dockerfiles-base/common/tools/golang/Dockerfile" \
org.opencontainers.image.description="golang ${GOLANG_VERSION} compiler environment."
ENV GOLANG_VERSION=$GOLANG_VERSION \
GOPATH=/go \
CGO_ENABLED=1 \
GO111MODULE=on \
GOPROXY=http://goproxy.leops.local,direct
ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH
# install cgo-related dependencies
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
git \
g++ \
gcc \
make \
cmake \
pkg-config \
binutils \
libc6-dev \
libstdc++6 \
libssl-dev \
libsasl2-dev \
libzstd-dev \
libcurl4-openssl-dev \
; \
apt-get clean; \
rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* /tmp/* /var/tmp/*; \
truncate -s 0 /var/log/*log;
# install golang
RUN set -eux; \
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
url=; \
case "$arch" in \
'amd64') \
url="https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz"; \
;; \
'armel') \
export GOARCH='arm' GOARM='5' GOOS='linux'; \
;; \
'armhf') \
url="https://dl.google.com/go/go${GOLANG_VERSION}.linux-armv6l.tar.gz"; \
;; \
'arm64') \
url="https://dl.google.com/go/go${GOLANG_VERSION}.linux-arm64.tar.gz"; \
;; \
'i386') \
url="https://dl.google.com/go/go${GOLANG_VERSION}.linux-386.tar.gz"; \
;; \
'mips64el') \
export GOARCH='mips64le' GOOS='linux'; \
;; \
'ppc64el') \
url="https://dl.google.com/go/go${GOLANG_VERSION}.linux-ppc64le.tar.gz"; \
;; \
's390x') \
url="https://dl.google.com/go/go${GOLANG_VERSION}.linux-s390x.tar.gz"; \
;; \
*) echo >&2 "error: unsupported architecture '$arch' (likely packaging update needed)"; exit 1 ;; \
esac; \
build=; \
if [ -z "$url" ]; then \
# https://github.com/golang/go/issues/38536#issuecomment-616897960
build=1; \
url="https://dl.google.com/go/go${GOLANG_VERSION}.src.tar.gz"; \
echo >&2; \
echo >&2"warning: current architecture ($arch) does not have a compatible Go binary release; will be building from source"; \
echo >&2; \
fi; \
\
wget -O go.tgz "$url" --progress=dot:giga; \
\
# https://github.com/golang/go/issues/14739#issuecomment-324767697
GNUPGHOME="$(mktemp -d)"; export GNUPGHOME; \
\
tar -C /usr/local -xzf go.tgz; \
rm go.tgz; \
\
if [ -n "$build" ]; then \
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
apt-get install -y --no-install-recommends golang-go; \
\
( \
cd /usr/local/go/src; \
# set GOROOT_BOOTSTRAP + GOHOST* such that we can build Go successfully
export GOROOT_BOOTSTRAP="$(go env GOROOT)" GOHOSTOS="$GOOS" GOHOSTARCH="$GOARCH"; \
./make.bash; \
); \
\
apt-mark auto '.*' > /dev/null; \
apt-mark manual $savedAptMark > /dev/null; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
apt-get clean; \
rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* /tmp/* /var/tmp/*; \
truncate -s 0 /var/log/*log; \
\
# remove a few intermediate / bootstrapping files the official binary release tarballs do not contain
rm -rf \
/usr/local/go/pkg/*/cmd \
/usr/local/go/pkg/bootstrap \
/usr/local/go/pkg/obj \
/usr/local/go/pkg/tool/*/api \
/usr/local/go/pkg/tool/*/go_bootstrap \
/usr/local/go/src/cmd/dist/dist \
; \
fi; \
\
go version
# install dependencies
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
libprotobuf-dev; \
PROTOC_VERSION="30.2"; \
PROTOC_ZIP="protoc-${PROTOC_VERSION}-linux-x86_64.zip"; \
curl -OL "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/${PROTOC_ZIP}"; \
unzip -o $PROTOC_ZIP -d /usr/local bin/protoc; \
unzip -o $PROTOC_ZIP -d /usr/local 'include/*'; \
go install honnef.co/go/tools/cmd/staticcheck@latest; \
go install github.com/go-delve/delve/cmd/dlv@latest; \
go install gotest.tools/gotestsum@latest; \
go install github.com/boumenot/gocover-cobertura@latest; \
go install github.com/elliots/protoc-gen-twirp_swagger@latest; \
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest; \
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest; \
go clean -cache; \
go clean -modcache; \
apt-get clean; \
rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* /tmp/* /var/tmp/*; \
truncate -s 0 /var/log/*log; \
rm -rf $GOPATH/pkg/* $PROTOC_ZIP;
WORKDIR $GOPATH
Dockerfile 关键点解析
这个编译环境 Dockerfile 有以下几个重要特点:
- 基于 Debian:使用了我们之前创建的标准化 Debian 基础镜像
- 动态版本:通过ARG GOLANG_VERSION参数化 Go 版本,便于维护和更新
- 环境配置:设置了 Go 开发所需的环境变量(GOPATH、GO111MODULE 等)
- CGO 支持:安装了 C/C++编译工具链,支持 CGO 功能
- 多架构支持:检测当前 CPU 架构并安装相应版本的 Go
- 工具链安装:包含了 protobuf、代码检查、调试等实用工具
- 缓存清理:每个步骤后清理不必要的缓存文件,减小镜像体积
编译环境构建脚本
使用以下脚本 (build.sh) 来构建和推送编译环境镜像:
#!/bin/bash
set -e
# 配置
REGISTRY="harbor.leops.local"
IMAGE_BASE_NAME="common/tools/golang"
VERSION="1.24.2"
# 声明镜像地址数组
declare -a IMAGE_PATHS
IMAGE_PATHS+=(
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION}"
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION%.*}"
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION}-debian11"
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION%.*}-debian11"
)
build_image() {
echo "Building and pushing image:"
for img in "${IMAGE_PATHS[@]}"; do echo -n " $img"; done
# 构建镜像
docker buildx build \
$(for img in "${IMAGE_PATHS[@]}"; do echo -n "-t $img "; done) \
--label "org.opencontainers.image.created=$(date --rfc-3339=seconds)" \
--build-arg "GOLANG_VERSION=${VERSION}" \
--add-host goproxy.leops.local=192.168.77.140 \
--provenance=false \
--pull \
--push \
.
echo "Build complete."
}
# 参数处理
case "$1" in
"list-tags")
# 输出镜像标签列表
printf '%s\n'"${IMAGE_PATHS[@]}"
;;
*)
build_image
;;
esac
脚本支持创建多个标签版本,包括完整版本号、主版本号以及带有系统标识的组合标签,确保镜像引用的灵活性。
构建 Go 应用的运行镜像
编译环境负责构建应用,而运行镜像则专注于高效安全地运行 Go 应用。我们需要创建一个精简的运行环境,仅包含必要的组件。
为什么需要专门的运行镜像?
好的运行镜像应该具备以下特点:
- 最小化攻击面:减少不必要的组件和工具
- 合理的权限:避免使用 root 用户运行应用
- 适当的文件结构:提供标准化的应用目录结构
- 体积小:更快的分发和部署速度
运行镜像 Dockerfile
创建 Go 运行环境的目录
mkdir -p common/runtime/golang
cd common/runtime/golang
下面是一个基于 Debian 的 Go 应用运行镜像示例:
#syntax=harbor.leops.local/library/docker/dockerfile:1
FROM harbor.leops.local/common/os/debian:bullseye
LABEL org.opencontainers.image.authors="ops@leops.local" \
org.opencontainers.image.source="http://git.leops.local/ops/dockerfiles-base/common/runtime/golang/Dockerfile" \
org.opencontainers.image.description="Minimal base runtime for Go applications with non-root user."
RUN \
groupadd -r nonroot \
&& useradd -r -g nonroot nonroot \
&& mkdir -p /app/logs \
&& chown nonroot:nonroot -R /app
USER nonroot:nonroot
运行镜像重点解析
这个运行镜像的关键特点:
- 基于精简 Debian:继承了我们优化过的 Debian 基础镜像
- 非 root 用户:创建了专用的 nonroot 用户,提高安全性
- 标准目录结构:预先创建了应用和日志目录,并设置了正确的权限
- 最小依赖:不包含编译工具、开发库等不必要组件
- 权限切换:通过 USER 指令切换到非特权用户,防止容器内进程获取过高权限
运行镜像构建脚本
使用以下脚本(build.sh)构建运行环境镜像:
#!/bin/bash
set -e
# 配置
REGISTRY="harbor.leops.local"
IMAGE_BASE_NAME="common/runtime/golang"
VERSION="debian11"
# 声明镜像地址数组
declare -a IMAGE_PATHS
IMAGE_PATHS+=(
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION}" # 完整版本
)
build_image() {
echo "Building and pushing image:"
for img in "${IMAGE_PATHS[@]}"; do echo -n " $img"; done
# 构建镜像
docker buildx build \
$(for img in "${IMAGE_PATHS[@]}"; do echo -n "-t $img "; done) \
--label "org.opencontainers.image.created=$(date --rfc-3339=seconds)" \
--provenance=false \
--pull \
--push \
.
echo "Build complete."
}
# 参数处理
case "$1" in
"list-tags")
# 输出镜像标签列表
printf '%s\n'"${IMAGE_PATHS[@]}"
;;
*)
build_image
;;
esac
构建应用镜像 - 多阶段构建实战
在准备好编译环境和运行环境后,我们可以通过多阶段构建来创建最终的应用镜像。多阶段构建允许我们在同一个 Dockerfile 中使用多个 FROM 指令,每个阶段可以独立运行并复用结果。
Docker 多阶段构建简介
多阶段构建的主要优势:
- 分离构建和运行环境:编译阶段可以包含所有开发工具,而运行阶段仅包含必要组件
- 减小最终镜像体积:最终镜像只包含运行所需文件,不含编译工具和中间产物
- 优化缓存:合理使用构建缓存加速重复构建
- 简化流程:将多个构建步骤整合到单个 Dockerfile 中
准备示例应用
首先,我们获取一个简单的 Go Web 应用示例:
git clone https://github.com/lework/ci-demo-go.git
cd ci-demo-go
应用 Dockerfile 详解
下面是一个典型的 Go 应用多阶段构建 Dockerfile:
#syntax=harbor.leops.local/library/docker/dockerfile:1
# ---- 编译环境 ----
FROM harbor.leops.local/common/tools/golang:1.24 AS builder
ARG APP_ENV=test \
APP=undefine \
GIT_BRANCH= \
GIT_COMMIT_ID=
ENV APP_ENV=$APP_ENV \
APP=$APP \
GIT_BRANCH=$GIT_BRANCH \
GIT_COMMIT_ID=$GIT_COMMIT_ID
WORKDIR /app_build
# 编译
COPY . .
RUN --mount=type=cache,id=gomod,target=/go/pkg/mod \
--mount=type=cache,id=gobuild,target=/root/.cache/go-build \
go build -tags 'osusergo,netgo' \
-ldflags "-X main.branch=$GIT_BRANCH -X main.commit=$GIT_COMMIT_ID" \
-v -o bin/${APP} *.go \
&& cp -rf etc bin/etc \
&& chown 999.999 -R bin
#
# ---- 运行环境 ----
FROM harbor.leops.local/common/runtime/golang:debian11 AS running
ARG APP_ENV=test \
APP=undefine
ENV APP_ENV=$APP_ENV \
APP=$APP
WORKDIR /app
COPY --from=builder --link /app_build/bin /app/
CMD ["bash", "-c", "exec /app/${APP} -f /app/etc/app_${APP_ENV}.yaml"]
多阶段构建关键点解析
这个 Dockerfile 分为两个明确的阶段:
- 编译阶段 (builder):
• 使用我们的 Go 编译环境镜像
• 接收构建参数(环境、应用名称、Git 信息等)
• 复制源代码到工作目录
• 使用 BuildKit 缓存加速依赖下载和编译
• 通过 ldflags 注入版本信息
• 为生成的二进制文件和配置设置正确权限 - 运行阶段 (running):
• 使用我们的精简运行环境镜像
• 仅从编译阶段复制编译结果
• 使用–link确保构建缓存的高效利用
• 通过 CMD 指定启动命令,支持不同环境配置
构建脚本详解
使用以下脚本(app-build/build-app.sh)来简化应用镜像的构建过程:
#!/bin/bash
set -e
APP_ENV="$1"
APP="$2"
REGISTRY="harbor.leops.local"
APP_GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
APP_GIT_COMMITID=$(git rev-parse --short HEAD)
APP_IMAGE="${REGISTRY}/${APP_ENV}/${APP}"
APP_IMAGE_TAG="${APP_GIT_BRANCH}-${APP_GIT_COMMITID}-$(date +%Y%m%d%H%M)"
docker buildx build \
-t "${APP_IMAGE}:${APP_IMAGE_TAG}" \
--add-host goproxy.leops.local=192.168.77.140 \
--build-arg APP=${APP} \
--build-arg APP_ENV=${APP_ENV} \
--build-arg GIT_BRANCH=${APP_GIT_BRANCH} \
--build-arg GIT_COMMIT_ID=${APP_GIT_COMMITID} \
--push .
if [[ "$APP_ENV" == "prd" && ${APP_GIT_BRANCH} == "master" ]]; then
docker tag "${APP_IMAGE}:${APP_IMAGE_TAG}""${APP_IMAGE}:latest"
docker push "${APP_IMAGE}:latest"
fi
脚本实现了以下功能:
- 接收环境和应用名称参数
- 自动获取 Git 分支和提交信息
- 生成包含所有相关信息的镜像标签
- 构建并推送镜像
- 对生产环境主分支构建添加 latest 标签
实际构建应用
执行以下命令构建示例应用:
bash /data/dockerfiles-base/app-build/build-app.sh dev ci-demo-go
构建完成后,会生成如下格式的镜像标签:
harbor.leops.local/dev/ci-demo-go:master-93f3aa7-202504250717
版本控制
完成构建后,将所有文件提交到 Git 仓库进行版本控制:
git add -A .
git commit -m "feat: add golang"
git push
运行 Go 应用容器
最终,我们可以运行构建好的 Go 应用容器,并验证其功能。
容器运行与验证
# 运行容器
docker run --rm -d --name ci-demo-go -p 18080:8080 harbor.leops.local/dev/ci-demo-go:master-93f3aa7-202504250717
# 访问应用
curl http://localhost:18080/health
# 查看日志
docker logs ci-demo-go
# 停止容器
docker stop ci-demo-go
生产环境最佳实践
在生产环境中部署 Go 应用容器时,可以考虑以下最佳实践:
- 资源限制:使用–memory和–cpus设置容器资源上限
- 健康检查:添加–health-cmd配置容器健康检查
- 日志管理:配置日志驱动将日志发送到集中式日志系统
- 网络安全:仅暴露必要端口,使用网络隔离
- 存储持久化:对需要持久化的数据使用卷挂载
- 容器编排:在生产环境中使用 Kubernetes 等工具进行编排
总结
通过采用多阶段构建和遵循最佳实践,我们成功为 Go 应用程序创建了优化、小巧且安全的 Docker 镜像。这种方法具有以下优势:
- 镜像体积小:最终镜像通常只有几十 MB,甚至更小
- 构建速度快:合理利用缓存加速重复构建
- 安全性高:使用非 root 用户运行,减少攻击面
- 标准化流程:可重复的构建过程,适用于 CI/CD 系统
- 资源效率:优化的镜像意味着更高效的资源利用和更快的部署速度
通过这种方式构建的 Go 应用容器既保留了 Go 语言的高性能特性,又充分发挥了容器技术的优势,为微服务架构和云原生应用提供了理想的部署方案。