Docker 部署 Go 项目

一、分阶段构建

由于 go 的部署只需要编译后的可执行文件就可以了,并不需要 go 的环境。
因此可以先在有 go 环境的容器中对项目进行构建,得到构建后的可执行文件
然后再把这个可执行文件放到一个比较小的镜像容器里面。

1 准备一个 go 程序 和一个dockerfile

package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.GET("/", func(context *gin.Context) {
        context.String(200, "OK")
    })
    r.Run(":8080")
}
FROM golang:alpine AS builder  # 引入 go 的镜像,这样有 go 的环境

# 构建可执行文件
ENV CGO_ENABLED 0  # 关闭 cgo
ENV GOPROXY https://goproxy.cn,direct  # 设置国内代理
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories  # 镜像源地址改为阿里云,提升软件包下载速度

WORKDIR /build  # 设置工作目录
ADD go.mod .    # 将宿主机中的 go.mod 文件复制到容器的当前工作目录
ADD go.sum .
ADD main.go .   # 将宿主机中的源文件复制到容器的工作目录下
RUN go build -o main  # 在容器内执行编译,得到可执行文件名为 main

# 多阶段构建的第二阶段
FROM scratch  # 以一个特殊空白镜像为基础
WORKDIR /app  # 设置容器内的工作目录为 /app
COPY --from=builder /build/main /app  # 前一阶段的执行文件复制到当前工作目录
CMD ["./main"]  # 执行可执行程序

2 完整构建流程

阶段作用
第一阶段(builder)基于 Go 镜像,拷贝源码 → 编译生成 main 二进制文件
第二阶段(最终镜像)基于 scratch **空白镜像,**只拷贝编译好的 main → 生成极小的运行镜像

以一个空白镜像,只把上一阶段编译好的二进制文件拷贝进来,最终生成的镜像只有几 MB 到 十几MB,非常轻量,适合生产环境部署

注意:多阶段构建中,最终镜像只包含最后一个 FROM 阶段的内容,之前的所有阶段的文件不会被带入最终镜像。

3 构建镜像的具体操作

项目结构

my-go-app/
├── Dockerfile
├── go.mod
├── go.sum
└── main.go

在 Dockerfile 所在目录下执行

docker build -t my-go-app:latest .
  • -t my-go-app:latest:给镜像打标签,方便后续引用
  • .:指定构建上下文为当前目录

运行容器

docker run -d -p 8080:8080 my-go-app:latest
  • -d 后台运行
  • -p 8080:8080 将宿主机 8080 端口映射到容器的 8080 端口(假设 Go 程序监听 8080 端口)

最终生成的是一个 Docker 镜像(Image),它的特点是:

特性说明
内容只包含一个静态编译的 Go 可执行文件
大小通常 5~20 MB(取决于你的程序)
运行环境无需安装 Go、无需任何依赖库
可移植性可以在任何支持 Docker 的 Linux 机器上运行

二、运行时挂载注入配置文件

Dockerfile 不变,运行时通过**挂载卷 (Volume)**把配置文件注入容器

docker run -d -p 8088:8080 -v /host/path/config.yaml:/app/config.yaml my-go-app:latest

把宿主机绝对路径下的配置文件映射到镜像工作目录下

如果是配置中心

只需要在运行时把配置中心的地址等信息通过环境变量传进去

docker run -d -p 8080:8080 \ 
	-e CONFIG_CENTER_ADDR=http://nanos.example.com:8848 \
	-e CONFIG_CENTER_NAMESPACE=prod \
	-e APP_NAME=my-go-app \
	my-go-app:latest

Go 应用通过 os.Getenv("CONFIG_CENTER_ADDR") 就能读到这些值。

有一个极其关键的“坑”必须注意

  1. 如果配置中心地址是 http 不需要改 Dockerfile,容器内 Go 程序可以正常发起 http 请求

  2. 绝大多数生产环境的配置中心都强制开启 https,scratch 镜像里没有任何根证书(CA Certificates),Go 程序会因此拒绝连接。

    解决方案:修改 Dockerfile,在构建阶段把根证书复制进 scratch 镜像

    # 第一阶段:构建
    FROM golang:1.21-alpine AS builder
    # ...(构建代码)
    
    # 第二阶段:scratch
    FROM scratch
    WORKDIR /app
    
    # 关键:从 Alpine 镜像中复制 CA 证书和时区文件(解决 TLS 和时区问题)
    COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
    COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
    
    COPY --from=builder /build/main /app/
    
    # 设置环境变量指定配置中心地址(代码里读取)
    ENV CONFIG_CENTER_URL="https://config-server:8848"
    
    CMD ["./main"]
    

    服务发现与 DNS 问题
    scratch 虽然缺失很多文件,但 DNS 解析是依赖宿主机的 /etc/resolv.conf 的,Docker 默认会挂载宿主机的该文件,所以如果你的配置中心地址是域名(如 nacos.dev.svc.cluster.local),Go 程序依然可以正常解析,无需额外处理

  3. 如果配置中心依赖 gRPC,可能需要时区文件

    COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
    

三、设置 docker 网络连接外部服务

以 MySQL 为例,MySQL 可能在内网的服务器上,也可能单独起一个 MySQL 容器

第一种情况,因为 Docker 默认会将容器连接到宿主机的 Docker0 网桥上,容器和宿主机是可以直接通信的,所以直接连就行了

第二种情况,单独起 MySQL 容器和我们的项目容器是可以互相连接的

MySQL 在宿主机内网

只需通过环境变量把 IP 或内网域名传进去即可(如果没有配置文件的话)

docker run -d -p 8080:8080 \
  -e DB_HOST=192.168.1.100 \
  -e DB_PORT=3306 \
  -e DB_USER=root \
  -e DB_PASS=123456 \
  -e DB_NAME=mydb \
  my-go-app:latest

注意:如果 MySQL 在宿主机物理机上,且 Docker 使用默认桥接网络,千万不能用 localhost127.0.0.1(因为容器内的 localhost 指向容器自己)。必须填宿主机的内网 IP 或特殊的 host.docker.internal(Mac/Windows 适用,Linux 需加 --add-host)。

MySQL 是另一个独立的 Docker 容器(同一台机器)

自定义一个 Docker 网络,让两个容器通过容器名互相访问

方式1:使用 docker run 手动管理

第一步:创建网络并启动 MySQL 容器

docker network create my-network
# 启动 MySQL 容器,加入该网络
docker run -d --name mysql-container --network my-network -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=mydb mysql:8.0

第二部:运行 Go 应用,加入同一网络

docker run -d -p 8080:8080 \
  --network my-network \
  -e DB_HOST=mysql-container \   # 关键:直接用容器名,Docker 内置 DNS 会解析
  -e DB_PORT=3306 \
  -e DB_USER=root \
  -e DB_PASS=123456 \
  my-go-app:latest

方式2:使用 Docker Compose

创建一个 docker-compose.yaml

version: '3.8'

services:
  app:
  	# 挂载宿主机的 CA 证书 :ro read-only
    volumes:
      - ca-data:/etc/ssl/certs/ca-certificates.crt:ro
    image: my-go-app:latest
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=mysql
      - DB_PORT=3306
      - DB_USER=root
      - DB_PASSWORD=123456
    # 定义服务依赖关系,确保 MySQL 服务先启动
    depends_on:
      - mysql
    # 设置重启策略:容器退出时总是自动重启
    restart: always

  mysql:
    # 指定镜像版本
    image: mysql:8.0
    # 设置环境变量
    environment:
      - MYSQL_ROOT_PASSWORD=123456
      - MYSQL_DATABASE=mydb
    # 数据卷挂载:将宿主机的 ./mysql/data 目录挂载到容器的指定目录,实现数据持久化
    volumes:
      - mysql-data:/var/lib/mysql
    # 自定义 MySQL 启动命令
    command:
      [
        "--character-set-server=utf8mb4",
        "--collation-server=utf8mb4_unicode_ci",
        "--max_connections=3000",
      ]
    # 设置重启策略:容器退出时总是自动重启
    restart: always

volumes:
  ca-data: /etc/ssl/certs/ca-certificates.crt
  mysql-data: ./mysql/data

然后启动

docker compose up -d

Docker Compose 会自动创建默认网络,把app和mysql两个容器加入同一网络,容器之间可以通过服务名 mysql 互相访问。

但这里有一个需要注意的地方,MySQL 容器启动需要几秒钟,虽然 depends_on 让 Docker 先起 MySQL,但可能还没准备好,需要在 Go 中重试等待 MySQL 启动。

四、小结

本文档围绕「使用 scratch 空白镜像部署 Go 项目」这一核心思路,从镜像构建配置管理网络连接三个维度给出了完整的实践方案。核心要点总结如下:

维度关键结论
镜像构建采用多阶段构建,第一阶段在 golang:alpine 中编译出静态二进制文件,第二阶段复制到 scratch 中,最终镜像仅包含一个可执行文件,体积约 5~20 MB,极其轻量且安全。
配置文件推荐运行时通过 -v 挂载配置文件,或通过 -e 注入环境变量;若使用配置中心,务必注意 HTTPS 场景下需在 Dockerfile 中复制 CA 证书,否则 scratch 无法验证 TLS 证书。
外部服务(如 MySQL)通过自定义 Docker 网络或 Docker Compose 实现容器间互联,使用**容器名(服务名)**作为 DB_HOST;必须注意 MySQL 启动耗时,Go 代码中应实现连接重试逻辑,避免容器启动即崩溃。
Docker Compose使用 Compose 可大幅简化多容器编排,自动创建网络和服务发现;配合 healthcheck + condition: service_healthy 可确保依赖服务就绪后再启动应用。

一句话总结FROM scratch 是构建极简 Go 生产镜像的最佳选择,但必须补齐 CA 证书(如需 HTTPS)、引入连接重试(如需依赖外部服务)这两项“能力补丁”,才能让它在生产环境中稳定运行。

更多推荐