Appearance
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 Hub:https://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:
| 语言 | 运行时依赖 | 典型镜像大小 |
|---|---|---|
| Java | JRE | 200MB+ |
| Python | Python 解释器 + 依赖包 | 100MB+ |
| Node.js | Node 运行时 + node_modules | 200MB+ |
| 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 原生支持交叉编译,通过 GOOS 和 GOARCH 环境变量就可以在 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-docker2. 编写 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.02. 容器相关
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 hello4. 清理命令
bash
# 删除所有停止的容器
docker container prune
# 删除所有悬空镜像(dangling images)
docker image prune
# 删除所有未使用的镜像
docker image prune -a
# 一键清理(慎用!删除所有未使用的容器、网络、镜像、缓存)
docker system prune -a5. 系统信息
bash
# Docker 整体信息
docker info
# 磁盘占用
docker system df
# 事件流
docker events七、镜像分层原理与缓存机制
理解镜像分层和缓存机制,对于写出高效的 Dockerfile 至关重要。
1. 分层结构
每个 Docker 镜像由若干只读层组成,每层对应 Dockerfile 中的一条指令(FROM、RUN、COPY、ADD 会产生新层,WORKDIR、ENV、LABEL 等元数据指令不产生新层)。
当容器启动时,Docker 在镜像顶部添加一个可写层(container layer)。所有对文件的修改都发生在这一层,下层镜像保持不变。这种技术叫 Copy-on-Write(写时复制):
- 读文件:从上往下查找,找到即返回。
- 写文件:先把文件从下层复制到可写层,再修改。
- 删除文件:在可写层标记为删除。
2. 缓存机制
Docker 构建镜像时会使用缓存。如果某层的输入没有变化,Docker 会直接使用缓存,跳过实际执行。缓存命中的判断规则:
- 对于
RUN、COPY、ADD指令,Docker 检查指令本身的内容和上一层是否变化。 - 对于
COPY、ADD,还会检查源文件的内容(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.mod 和 go.sum 不变,go mod download 这一层就命中缓存,构建速度大幅提升。
4. 强制禁用缓存
调试时如果需要强制重建:
bash
docker build --no-cache -t hello-docker:1.0 .八、.dockerignore 文件
.dockerignore 的作用类似 .gitignore,用来指定哪些文件不发送给 Docker 守护进程作为构建上下文。它的作用:
- 加快构建速度:减少上下文体积。
- 避免泄露敏感信息:防止
.env、密钥文件被打进镜像。 - 避免缓存失效:防止无关文件变化导致
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.md2. 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
*.exe6. 构建与运行
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-app7. 传入环境变量
可以通过 -e 参数覆盖环境变量:
bash
docker run -d -p 9090:9090 -e PORT=9090 --name gin-app2 gin-docker:1.0
# 现在服务监听 9090
curl http://localhost:90908. 查看容器内进程
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. 容器启动后立即退出
最常见原因是 CMD 或 ENTRYPOINT 指定的命令是前台运行还是后台运行。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 download4. 镜像太大
如果发现镜像有几百 MB 甚至更大,多半是因为没有用多阶段构建,直接用了 golang:1.22 这种包含完整工具链的镜像作为运行镜像。下一篇会专门讲多阶段构建和镜像优化。
十一、小结
本篇涵盖了 Docker 的核心概念、Go 容器化的优势、Docker 安装、Dockerfile 编写、常用命令、分层与缓存机制、.dockerignore,并通过一个完整的 Gin 应用实战演练了整个流程。
关键要点回顾:
- 四个核心概念:镜像(静态模板)、容器(运行实例)、仓库(镜像存储)、Dockerfile(构建脚本)。
- Go 是容器化的天选语言:静态编译、单文件、无依赖、镜像小、启动快。
- Dockerfile 写法:合理利用缓存,把不常变化的步骤(如
go mod download)放在前面。 .dockerignore不可忽视:既加快构建,又防止敏感信息泄露。- 常用命令:
build、run、ps、logs、exec、stop、rm是日常最高频的命令。
下一篇我们将深入多阶段构建与镜像优化,把镜像体积从几百 MB 压到 10MB 以内,并介绍多平台构建和镜像安全扫描。容器化的核心收益——轻量、快速、安全——将在下一篇得到充分体现。