Skip to content

Docker 基础与 Go 容器化入门

本篇是 Docker + Go 系列教程的第一篇,面向已经掌握 Go 基础语法、希望把 Go 应用部署到容器中的开发者。我们将从 Docker 的核心概念讲起,解释为什么 Go 语言特别适合容器化,然后带你完成 Docker 安装、编写第一个 Dockerfile、掌握常用命令、理解镜像分层与缓存机制,最后用一个完整的 Gin Web 应用容器化示例收尾。学完本篇,你将具备把任意 Go 应用打包成 Docker 镜像并运行的能力。

一、Docker 是什么

Docker 是一个开源的容器化平台,它允许开发者把应用及其所有依赖打包到一个标准化的、可移植的「容器」中,然后在任何安装了 Docker 的环境中一致地运行。简单说,Docker 解决的核心问题是:「在我的机器上能跑,在你的机器上跑不起来」。

容器化相比传统虚拟机有以下优势:

特性传统虚拟机Docker 容器
启动速度分钟级秒级
资源占用GB 级MB 级
隔离性完整操作系统级隔离进程级隔离
部署密度一台机器几个虚拟机一台机器几十上百容器
一致性较好极好

Docker 在 2013 年开源后迅速成为云原生的事实标准,Kubernetes、Docker Swarm、Containerd 等编排与运行时工具都建立在容器这一概念之上。

二、Docker 基本概念

要理解 Docker,必须先掌握四个核心概念:镜像、容器、仓库、Dockerfile。

1. 镜像(Image)

镜像是一个只读的模板,包含了运行应用所需的所有内容:代码、运行时、库、环境变量、配置文件。可以把镜像理解成面向对象编程中的「类」,它是静态的、不可变的。

镜像的一个重要特性是分层存储。一个镜像并不是一个单独的大文件,而是由多个只读层(layer)叠加而成。每一层对应 Dockerfile 中的一条指令。例如:

dockerfile
FROM golang:1.22          # 第 1 层:基础镜像
WORKDIR /app              # 第 2 层:设置工作目录
COPY . .                  # 第 3 层:拷贝源码
RUN go build -o app .     # 第 4 层:编译
CMD ["./app"]             # 第 5 层:启动命令

每一层都是对前一层的增量修改,多层之间可以共享。这就是为什么你拉取多个基于 golang:1.22 的镜像时,基础层只会下载一次。

2. 容器(Container)

容器是镜像的运行实例,类似于「对象」与「类」的关系。容器在镜像之上增加了一个可写层(writable layer),所有对文件系统的修改都发生在这个可写层中,镜像本身保持不变。

容器是临时的:你可以随时创建、启动、停止、删除容器,而不会影响镜像。同一个镜像可以同时启动多个互不影响的容器实例。

3. 仓库(Registry)

仓库是存放镜像的地方,类似 Git 仓库的概念。最常见的仓库有:

  • Docker Hubhttps://hub.docker.com,Docker 官方公共仓库,包含大量官方镜像。
  • GHCR(GitHub Container Registry):GitHub 提供的镜像仓库,与 GitHub 仓库集成。
  • 私有 Registry:企业自建的内部仓库,如 Harbor、Nexus。

镜像的完整名称格式为 仓库地址/组织/镜像名:标签,例如 docker.io/library/golang:1.22。如果不指定仓库地址,默认是 Docker Hub;不指定标签,默认是 latest

4. Dockerfile

Dockerfile 是一个文本文件,包含了一系列指令,用来描述「如何构建镜像」。每条指令对应镜像中的一层。常见的指令有:

指令作用
FROM指定基础镜像
WORKDIR设置工作目录
COPY拷贝本地文件到镜像
ADD类似 COPY,但支持解压 tar 和下载 URL
RUN在构建时执行命令
CMD容器启动时默认执行的命令
ENTRYPOINT容器启动时的入口程序
ENV设置环境变量
EXPOSE声明容器监听的端口
VOLUME声明数据卷
USER指定运行用户
ARG构建时变量

三、为什么 Go 特别适合容器化

在所有主流语言中,Go 几乎是最适合容器化的。原因有以下几点:

1. 编译为单文件

Go 是静态编译语言,go build 会把所有依赖打包进一个独立的可执行文件。这个文件不依赖任何外部库(在 CGO_ENABLED=0 的情况下),可以扔到任何同架构的 Linux 机器上直接运行。这意味着容器里不需要装 Go 运行时、不需要装依赖库,只需要这个二进制文件就够了。

对比一下 Java、Python、Node.js:

语言运行时依赖典型镜像大小
JavaJRE200MB+
PythonPython 解释器 + 依赖包100MB+
Node.jsNode 运行时 + node_modules200MB+
Go无(静态编译)10~20MB

2. 无外部依赖

Go 的标准库非常丰富,HTTP 服务、JSON 处理、加密、数据库驱动等大部分功能都内置或可通过纯 Go 实现的第三方库完成。这意味着不需要在容器里安装系统级的 C 库、OpenSSL 等依赖,进一步缩小镜像体积。

3. 镜像可以做到极致小

配合多阶段构建(下一篇会详细讲),Go 应用的镜像可以做到 10~20MB,甚至使用 scratch 基础镜像做到 5MB 以下。这对部署速度、CI/CD 流水线效率、镜像拉取时间都有显著好处。

4. 启动速度极快

Go 编译出的二进制启动几乎是瞬时的(毫秒级),没有 JVM 预热、没有解释器初始化。这使得 Go 容器特别适合:

  • Serverless 场景(冷启动敏感)
  • 自动伸缩场景(快速拉起实例)
  • 滚动更新场景(减少新老实例切换时间)

5. 交叉编译方便

Go 原生支持交叉编译,通过 GOOSGOARCH 环境变量就可以在 macOS 上编译出 Linux/amd64 的二进制。这在构建多平台 Docker 镜像时非常方便,不需要在容器里装 Go 工具链。

四、安装 Docker 与验证

1. 安装 Docker

不同操作系统安装方式不同:

Windows / macOS

下载 Docker Desktop:https://www.docker.com/products/docker-desktop

安装后启动 Docker Desktop 应用即可。Windows 上需要启用 WSL 2 后端,性能更好。

Linux(Ubuntu 为例)

bash
# 卸载旧版本
sudo apt-get remove docker docker-engine docker.io containerd runc

# 安装必要依赖
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

# 添加 Docker 官方 GPG key
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# 添加 Docker 仓库
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 安装 Docker Engine
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

安装完成后,把当前用户加入 docker 组,避免每次都加 sudo

bash
sudo usermod -aG docker $USER
# 重新登录后生效

2. 验证安装

执行以下命令验证 Docker 是否安装成功:

bash
docker --version
# Docker version 24.0.7, build afdd53b

docker info
# 输出 Docker 系统信息

docker run hello-world
# 拉取并运行官方测试镜像

如果 docker run hello-world 输出了 Hello from Docker!,说明 Docker 已经正常工作。

3. 配置镜像加速(国内用户)

国内访问 Docker Hub 可能比较慢,建议配置镜像加速器。编辑 /etc/docker/daemon.json(Linux)或在 Docker Desktop 设置中配置:

json
{
  "registry-mirrors": [
    "https://docker.mirrors.ustc.edu.cn",
    "https://hub-mirror.c.163.com"
  ]
}

修改后重启 Docker:

bash
sudo systemctl restart docker

五、第一个 Dockerfile:Go 应用容器化

下面用一个最简单的 Go 程序演示完整的容器化流程。

1. 准备 Go 应用

先创建一个项目目录 hello-docker,在其中创建 main.go

go
package main

import (
	"fmt"
	"net/http"
	"os"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		hostname, _ := os.Hostname()
		fmt.Fprintf(w, "Hello, Docker! I am running on %s\n", hostname)
	})

	http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("OK"))
	})

	fmt.Printf("Server starting on port %s\n", port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		fmt.Printf("Server failed: %v\n", err)
		os.Exit(1)
	}
}

初始化 Go module:

bash
go mod init hello-docker

2. 编写 Dockerfile

在项目根目录创建 Dockerfile

dockerfile
# 使用 golang 官方镜像作为基础
FROM golang:1.22

# 设置工作目录
WORKDIR /app

# 拷贝 go.mod 和 go.sum(如果有)
COPY go.mod ./

# 下载依赖
RUN go mod download

# 拷贝源代码
COPY . .

# 编译
RUN go build -o hello .

# 声明容器监听端口
EXPOSE 8080

# 启动命令
CMD ["./hello"]

3. 构建镜像

bash
docker build -t hello-docker:1.0 .

参数说明:

  • -t hello-docker:1.0:给镜像打标签(名称:版本)。
  • .:构建上下文路径,Docker 会把这个目录下的所有文件发给守护进程。

构建过程中你会看到类似输出:

text
[+] Building 12.3s (10/10) FINISHED
 => [internal] load build definition from Dockerfile    0.1s
 => [internal] load .dockerignore                       0.1s
 => [1/5] FROM docker.io/library/golang:1.22            2.3s
 => [internal] load metadata for docker.io/library/golang:1.22  2.0s
 => [2/5] WORKDIR /app                                  0.1s
 => [3/5] COPY go.mod ./                                0.1s
 => [4/5] RUN go mod download                           4.2s
 => [5/5] COPY . .                                      0.2s
 => exporting to image                                  0.5s
 => => writing image sha256:abc123...

4. 运行容器

bash
docker run -d -p 8080:8080 --name hello hello-docker:1.0

参数说明:

  • -d:后台运行。
  • -p 8080:8080:端口映射,宿主机 8080 映射到容器 8080。
  • --name hello:给容器命名为 hello。
  • hello-docker:1.0:使用的镜像。

测试:

bash
curl http://localhost:8080
# Hello, Docker! I am running on <容器ID>

curl http://localhost:8080/health
# OK

查看容器日志:

bash
docker logs hello
# Server starting on port 8080

六、Docker 常用命令

掌握 Docker 的关键是熟悉它的命令行工具。下面列出最常用的命令。

1. 镜像相关

bash
# 列出本地镜像
docker images
# REPOSITORY     TAG       IMAGE ID       CREATED        SIZE
# hello-docker   1.0       abc123def456   2 minutes ago  850MB

# 拉取镜像
docker pull golang:1.22

# 删除镜像
docker rmi hello-docker:1.0

# 给镜像打标签
docker tag hello-docker:1.0 hello-docker:latest

# 查看镜像构建历史(每一层)
docker history hello-docker:1.0

2. 容器相关

bash
# 列出运行中的容器
docker ps

# 列出所有容器(包括已停止的)
docker ps -a

# 启动容器
docker run -d -p 8080:8080 --name hello hello-docker:1.0

# 停止容器
docker stop hello

# 启动已停止的容器
docker start hello

# 重启容器
docker restart hello

# 删除容器(必须先停止)
docker rm hello

# 强制删除运行中的容器
docker rm -f hello

# 查看容器日志
docker logs hello
docker logs -f hello          # 实时跟踪
docker logs --tail 100 hello  # 最后 100 行
docker logs -t hello          # 显示时间戳

3. 进入容器与调试

bash
# 在容器内执行命令
docker exec -it hello sh
# 进入后可以执行 ls, ps, cat 等命令

# 拷贝文件(宿主机 -> 容器)
docker cp ./config.yaml hello:/app/config.yaml

# 拷贝文件(容器 -> 宿主机)
docker cp hello:/app/logs.log ./logs.log

# 查看容器详细信息
docker inspect hello

# 查看容器资源使用情况
docker stats hello

# 查看容器内进程
docker top hello

4. 清理命令

bash
# 删除所有停止的容器
docker container prune

# 删除所有悬空镜像(dangling images)
docker image prune

# 删除所有未使用的镜像
docker image prune -a

# 一键清理(慎用!删除所有未使用的容器、网络、镜像、缓存)
docker system prune -a

5. 系统信息

bash
# Docker 整体信息
docker info

# 磁盘占用
docker system df

# 事件流
docker events

七、镜像分层原理与缓存机制

理解镜像分层和缓存机制,对于写出高效的 Dockerfile 至关重要。

1. 分层结构

每个 Docker 镜像由若干只读层组成,每层对应 Dockerfile 中的一条指令(FROMRUNCOPYADD 会产生新层,WORKDIRENVLABEL 等元数据指令不产生新层)。

当容器启动时,Docker 在镜像顶部添加一个可写层(container layer)。所有对文件的修改都发生在这一层,下层镜像保持不变。这种技术叫 Copy-on-Write(写时复制):

  • 读文件:从上往下查找,找到即返回。
  • 写文件:先把文件从下层复制到可写层,再修改。
  • 删除文件:在可写层标记为删除。

2. 缓存机制

Docker 构建镜像时会使用缓存。如果某层的输入没有变化,Docker 会直接使用缓存,跳过实际执行。缓存命中的判断规则:

  • 对于 RUNCOPYADD 指令,Docker 检查指令本身的内容和上一层是否变化。
  • 对于 COPYADD,还会检查源文件的内容(checksum)是否变化。

一旦某一层缓存未命中,它之后的所有层都不会使用缓存,全部重新构建。

3. 利用缓存的最佳实践

为了让缓存最大化生效,Dockerfile 中指令的顺序非常重要。原则是:变化频率低的放前面,变化频率高的放后面

反面示例(缓存失效严重)

dockerfile
FROM golang:1.22
WORKDIR /app
COPY . .                 # 任何代码改动都导致这一层缓存失效
RUN go mod download      # 后续全部重新执行
RUN go build -o app .
CMD ["./app"]

每次改一行代码,go mod download 都会重新执行,非常慢。

正面示例(推荐写法)

dockerfile
FROM golang:1.22
WORKDIR /app

# 先拷贝依赖文件
COPY go.mod go.sum ./
RUN go mod download      # 依赖不变就命中缓存

# 再拷贝源代码
COPY . .
RUN go build -o app .

CMD ["./app"]

这样,只要 go.modgo.sum 不变,go mod download 这一层就命中缓存,构建速度大幅提升。

4. 强制禁用缓存

调试时如果需要强制重建:

bash
docker build --no-cache -t hello-docker:1.0 .

八、.dockerignore 文件

.dockerignore 的作用类似 .gitignore,用来指定哪些文件不发送给 Docker 守护进程作为构建上下文。它的作用:

  1. 加快构建速度:减少上下文体积。
  2. 避免泄露敏感信息:防止 .env、密钥文件被打进镜像。
  3. 避免缓存失效:防止无关文件变化导致 COPY . . 缓存失效。

在项目根目录创建 .dockerignore

text
# 版本控制
.git
.gitignore

# IDE 配置
.idea
.vscode
*.swp

# 构建产物
*.exe
*.exe~
*.dll
*.so
*.dylib
hello
app

# 测试与覆盖率
*_test.go
coverage.out
coverage.html

# 环境变量与密钥
.env
.env.local
*.pem
*.key

# 日志
*.log
logs/

# 文档
README.md
docs/

# Docker 自身文件
Dockerfile
docker-compose*.yml
.dockerignore

特别要强调:COPY . . 会把构建上下文里所有文件都拷进镜像,所以 .dockerignore 是一道重要的安全防线。如果不小心把 .env 文件拷进镜像并发布到公共仓库,会导致密钥泄露。

九、完整示例:Gin Web 应用容器化

下面用一个完整的 Gin Web 应用演示真实项目的容器化流程。

1. 项目结构

text
gin-docker/
├── main.go
├── go.mod
├── go.sum
├── Dockerfile
├── .dockerignore
└── README.md

2. main.go

go
package main

import (
	"net/http"
	"os"
	"time"

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

type Response struct {
	Message   string    `json:"message"`
	Time      time.Time `json:"time"`
	Hostname  string    `json:"hostname"`
	Version   string    `json:"version"`
}

var version = "dev"

func main() {
	gin.SetMode(gin.ReleaseMode)
	r := gin.New()
	r.Use(gin.Logger(), gin.Recovery())

	hostname, _ := os.Hostname()

	r.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, Response{
			Message:  "Hello from Gin in Docker",
			Time:     time.Now(),
			Hostname: hostname,
			Version:  version,
		})
	})

	r.GET("/health", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	r.GET("/api/users", func(c *gin.Context) {
		users := []gin.H{
			{"id": 1, "name": "Alice"},
			{"id": 2, "name": "Bob"},
			{"id": 3, "name": "Charlie"},
		}
		c.JSON(http.StatusOK, gin.H{"users": users})
	})

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	r.Run(":" + port)
}

3. go.mod

go
module gin-docker

go 1.22

require github.com/gin-gonic/gin v1.9.1

执行 go mod tidy 生成 go.sum

4. Dockerfile

dockerfile
FROM golang:1.22 AS builder

WORKDIR /build

# 利用缓存:先拷依赖
COPY go.mod go.sum ./
RUN go mod download

# 再拷源码
COPY . .

# 静态编译,方便后续用更小的基础镜像
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .

# 运行阶段:使用 alpine 减小体积
FROM alpine:3.19

# 安装最小化运行依赖(ca-certificates 用于 HTTPS)
RUN apk --no-cache add ca-certificates tzdata

WORKDIR /app

# 从构建阶段拷贝编译产物
COPY --from=builder /build/server /app/server

# 设置时区
ENV TZ=Asia/Shanghai

EXPOSE 8080

ENTRYPOINT ["/app/server"]

注意这里已经使用了多阶段构建(下一篇会详细讲),运行镜像只有十几 MB。

5. .dockerignore

text
.git
.idea
.vscode
*.md
Dockerfile
.dockerignore
server
*.exe

6. 构建与运行

bash
# 构建
docker build -t gin-docker:1.0 .

# 查看镜像大小
docker images gin-docker
# REPOSITORY   TAG   IMAGE ID       CREATED         SIZE
# gin-docker   1.0   abc123def456   5 seconds ago   23MB

# 运行
docker run -d -p 8080:8080 --name gin-app gin-docker:1.0

# 测试
curl http://localhost:8080
# {"message":"Hello from Gin in Docker","time":"...","hostname":"...","version":"dev"}

curl http://localhost:8080/health
# {"status":"ok"}

curl http://localhost:8080/api/users
# {"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"},{"id":3,"name":"Charlie"}]}

# 查看日志
docker logs -f gin-app

7. 传入环境变量

可以通过 -e 参数覆盖环境变量:

bash
docker run -d -p 9090:9090 -e PORT=9090 --name gin-app2 gin-docker:1.0
# 现在服务监听 9090
curl http://localhost:9090

8. 查看容器内进程

bash
docker exec gin-app ps aux
# PID   USER     TIME   COMMAND
#   1   root     0:00   /app/server

可以看到容器内只有我们的 Go 二进制在运行,没有其他进程。

9. 注入版本号

通过 --build-arg-ldflags 在构建时注入版本信息:

dockerfile
# Dockerfile 中增加
ARG VERSION=dev
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-s -w -X main.version=${VERSION}" \
    -o server .

构建:

bash
docker build --build-arg VERSION=v1.2.3 -t gin-docker:v1.2.3 .

docker run --rm -p 8080:8080 gin-docker:v1.2.3
# 访问根路径会看到 version 字段变成 v1.2.3

十、常见问题与排查

1. 容器启动后立即退出

最常见原因是 CMDENTRYPOINT 指定的命令是前台运行还是后台运行。Docker 容器的主进程必须是前台运行,否则容器会立即退出。

错误示例:

dockerfile
CMD ["./server", "&"]   # 错误!容器会立即退出

正确示例:

dockerfile
CMD ["./server"]        # 正确,server 在前台运行

排查方法:用 docker logs <容器名> 查看退出原因,用 docker ps -a 查看退出码。

2. 端口被占用

text
docker: Error response from daemon: driver failed programming external connectivity on endpoint ...: bind: address already in use.

说明宿主机端口已被占用。解决方法:

bash
# 查看端口占用
lsof -i :8080          # Linux/macOS
netstat -ano | findstr 8080   # Windows

# 换一个端口
docker run -p 9090:8080 ...

3. 构建时网络超时

国内拉取依赖可能超时,可以在 Dockerfile 中设置 Go 代理:

dockerfile
ENV GOPROXY=https://goproxy.cn,direct
RUN go mod download

4. 镜像太大

如果发现镜像有几百 MB 甚至更大,多半是因为没有用多阶段构建,直接用了 golang:1.22 这种包含完整工具链的镜像作为运行镜像。下一篇会专门讲多阶段构建和镜像优化。

十一、小结

本篇涵盖了 Docker 的核心概念、Go 容器化的优势、Docker 安装、Dockerfile 编写、常用命令、分层与缓存机制、.dockerignore,并通过一个完整的 Gin 应用实战演练了整个流程。

关键要点回顾:

  1. 四个核心概念:镜像(静态模板)、容器(运行实例)、仓库(镜像存储)、Dockerfile(构建脚本)。
  2. Go 是容器化的天选语言:静态编译、单文件、无依赖、镜像小、启动快。
  3. Dockerfile 写法:合理利用缓存,把不常变化的步骤(如 go mod download)放在前面。
  4. .dockerignore 不可忽视:既加快构建,又防止敏感信息泄露。
  5. 常用命令buildrunpslogsexecstoprm 是日常最高频的命令。

下一篇我们将深入多阶段构建与镜像优化,把镜像体积从几百 MB 压到 10MB 以内,并介绍多平台构建和镜像安全扫描。容器化的核心收益——轻量、快速、安全——将在下一篇得到充分体现。