site logo

Marico's space

用 Docker 和 Kubernetes 部署你的首个 Go 应用

编程技术 2026-06-09 11:27:51 4

最近折腾了用 Docker 和 Kubernetes 部署 Go 应用,踩了几个坑,这篇把问题说清楚。

我们先写一个真实的 Go HTTP 服务,用 Docker 容器化它,然后部署到本地 Kubernetes 集群。这里用的是 kind(Kubernetes in Docker)——它把 Kubernetes 节点跑在 Docker 容器里,在阿里云、腾讯云这种云服务上跑之前,拿来本地练手非常方便,不需要注册什么云账号。

学完这篇,你将掌握滚动更新和扩缩容——全在自己的机器上搞定。

前置条件

  • Go 1.21 或更高版本。
  • Docker Desktop 已安装并运行。
  • kubectl 已安装——这是操作 Kubernetes 集群的 CLI 工具。

开始之前先验证下环境:

go version
docker version
kubectl version --client

第一步:创建 Go 应用

新建项目目录并初始化 Go 模块:

mkdir go-k8s-demo
cd go-k8s-demo
go mod init go-k8s-demo

创建 main.go:

package main import ( "encoding/json" "fmt" "log" "net/http" "os" "time"
) type Response struct { Message string `json:"message"` Hostname string `json:"hostname"` Version string `json:"version"` Time time.Time `json:"time"`
} func handler(w http.ResponseWriter, r *http.Request) { hostname, _ := os.Hostname() resp := Response{ Message: "Hello from Kubernetes!", Hostname: hostname, Version: os.Getenv("APP_VERSION"), Time: time.Now(), } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(resp); err != nil { log.Printf("encode response: %v", err) }
} func healthcheck(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "ok")
} func main() { port := os.Getenv("PORT") if port == "" { port = "8080" } http.HandleFunc("/", handler) http.HandleFunc("/healthcheck", healthcheck) log.Printf("Server listening on :%s", port) if err := http.ListenAndServe(":"+port, nil); err != nil { log.Fatalf("Listen error: %v", err) }
}

几个关键点:

  • os.Hostname() 在 Kubernetes 里会返回 Pod 名字,方便我们看哪个副本处理了请求。
  • APP_VERSION 通过环境变量注入,部署时换版本不用改镜像。
  • /healthcheck 是存活探针和就绪探针的端点,Kubernetes 通过它判断容器是否健康。

本地跑一下:

go run main.go

另一个终端:

curl http://localhost:8080

预期返回:

{ "message": "Hello from Kubernetes!", "hostname": "your-machine-name", "version": "", "time": "2026-06-02T10:00:00Z"
}

第二步:用 Docker 容器化

在项目根目录创建 Dockerfile:

# Build stage
FROM golang:1.25.3-alpine AS builder
WORKDIR /app
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server . # Runtime stage
FROM alpine:3.21
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=builder /app/server .
USER app
EXPOSE 8080
CMD ["./server"]

这是多阶段构建:

  • 第一个阶段(builder)用完整的 Go 工具链镜像编译二进制。
  • 第二个阶段只把编译好的二进制拷贝到极简的 Alpine 镜像。
  • 最终镜像只有几兆,不是几百兆。没有 Go 编译器,没有源码——只有应用需要的二进制。
  • CGO_ENABLED=0 产出纯静态二进制,不依赖 C 库,这样才能跑在精简的 Alpine 基础镜像上。
  • adduser -S app 用非 root 用户跑进程——安全基线。

构建并本地测试镜像

docker build -t go-k8s-demo:v1 .

本地跑一下验证:

docker run -p 8080:8080 -e APP_VERSION=v1 go-k8s-demo:v1
curl http://localhost:8080

现在返回里包含了 "version": "v1",因为环境变量在运行时注入了。

第三步:在 Docker Desktop 里创建 Kubernetes 集群

目前只用 Docker 直接操作:构建镜像、验证容器能在机器上跑。

Kubernetes 是下一层。以前用 docker run 手动起一个容器,现在让 Kubernetes 帮你保活。它会创建 Pod、重启不健康的容器、通过 Service 暴露应用,后面还能做滚动更新。

本地教程用 Docker Desktop 的 kind 来创建小型 Kubernetes 集群。kind 把节点跑在 Docker 容器里,所以集群还是本地的,但对 kubectl 来说行为和真实 Kubernetes 集群一样。

进入 设置 → Kubernetes → 创建 Kubernetes 集群

有两个集群类型可选:

  • kind——推荐。用 kind 创建集群,需要 containerd 镜像存储。本地构建的镜像需要用 kind load docker-image 显式加载到集群里,Kubernetes 才能使用。
  • Kubeadm——用 kubeadm 创建单节点集群。同样需要手动加载镜像或推送到镜像仓库。

kind,节点数保持 1,点 创建。首次创建会拉取集群组件,要一两分钟。等状态指示灯变绿,验证集群是否就绪:

kubectl get nodes
NAME STATUS ROLES AGE VERSION
desktop-control-plane Ready control-plane 60s v1.34.3

Docker Desktop 自动把 kubectl 的上下文切到新集群,不需要手动切换。

把镜像加载到 kind 集群

kind 集群创建好后,需要让镜像在集群内可见。

这是个容易困惑的点:镜像 go-k8s-demo:v1 已经在 Docker 本地镜像库里了,因为我们用 docker build 构建的,但 kind 集群的节点容器有自己的容器运行时。Kubernetes 不会自动看到你机器上的每个镜像。必须显式加载镜像后,Deployment 才能从它启动 Pod。

Mac/Linux——用 kind CLI:

kind load docker-image go-k8s-demo:v1

Windows(Git Bash + Docker Desktop)——kind 可能被 Windows 应用控制策略拦截。用下面三步法:Docker Desktop 的 kind 节点跑在 desktop-linux 上下文中,节点容器名叫 desktop-control-plane

把镜像保存成 tar 文件在当前目录(避免 /tmp——Git Bash 会把它映射到 Windows 临时路径,会出问题):

docker save go-k8s-demo:v1 -o go-k8s-demo-v1.tar

拷贝 tar 到 kind 节点容器(MSYS_NO_PATHCONV=1 防止 Git Bash 把 Linux 路径转成 Windows 路径):

MSYS_NO_PATHCONV=1 docker --context desktop-linux cp go-k8s-demo-v1.tar desktop-control-plane:/go-k8s-demo-v1.tar

在节点内用 containerd 导入镜像:

MSYS_NO_PATHCONV=1 docker --context desktop-linux exec desktop-control-plane ctr images import /go-k8s-demo-v1.tar

Windows(PowerShell 或命令提示符 + Docker Desktop)——用同样的手动导入方法,不需要 MSYS_NO_PATHCONV,因为 PowerShell 和命令提示符不会改写 Linux 风格的容器路径。

把镜像保存成 tar 文件在当前目录:

docker save go-k8s-demo:v1 -o go-k8s-demo-v1.tar

拷贝 tar 到 kind 节点容器:

docker --context desktop-linux cp go-k8s-demo-v1.tar desktop-control-plane:/go-k8s-demo-v1.tar

在节点内用 containerd 导入镜像:

docker --context desktop-linux exec desktop-control-plane ctr images import /go-k8s-demo-v1.tar

验证镜像是否可用。

Git Bash:

MSYS_NO_PATHCONV=1 docker --context desktop-linux exec desktop-control-plane ctr images ls

PowerShell 或命令提示符:

docker --context desktop-linux exec desktop-control-plane ctr images ls

应该能看到 docker.io/library/go-k8s-demo:v1。之后 Kubernetes 就能从这个本地镜像创建 Pod,不需要从 Docker Hub 或其他镜像仓库拉取。

第四步:编写 Kubernetes 资源清单

创建存放资源清单的目录:

mkdir k8s

Deployment

创建 k8s/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata: name: go-k8s-demo labels: app: go-k8s-demo
spec: replicas: 3 selector: matchLabels: app: go-k8s-demo template: metadata: labels: app: go-k8s-demo spec: containers: - name: go-k8s-demo image: go-k8s-demo:v1 imagePullPolicy: IfNotPresent ports: - containerPort: 8080 env: - name: APP_VERSION value: 'v1' livenessProbe: httpGet: path: /healthcheck port: 8080 initialDelaySeconds: 5 periodSeconds: 10 readinessProbe: httpGet: path: /healthcheck port: 8080 initialDelaySeconds: 3 periodSeconds: 5 resources: requests: cpu: '50m' memory: '32Mi' limits: cpu: '200m' memory: '64Mi'

关键配置说明:

  • replicas: 3——Kubernetes 会保持正好 3 个 Pod 在跑。挂了一个就起一个新的。
  • imagePullPolicy: IfNotPresent——Kubernetes 用节点上已有的镜像,只有缺失时才从仓库拉。因为第三步我们手动把镜像加载到了 kind 节点的 containerd 运行时,镜像已经在,不需要推送。
  • livenessProbe——如果 /healthcheck 不返回 200,Kubernetes 重启容器。
  • readinessProbe——只有 /healthcheck 返回 200 后,Kubernetes 才开始往 Pod 发流量。启动中或重启慢的时候,不健康的 Pod 自动被排除在负载均衡之外。
  • resources——requests 是调度器用来找有足够资源的节点的。limits 是硬上限。两个都设是生产环境最佳实践。

Service

创建 k8s/service.yaml:

apiVersion: v1
kind: Service
metadata: name: go-k8s-demo
spec: selector: app: go-k8s-demo ports: - protocol: TCP port: 80 targetPort: 8080 type: NodePort

Service 用 NodePort 类型,这样集群外部能访问。Service 上的 80 端口映射到每个 Pod 的 8080 端口。selector 把 Service 和所有标签为 app: go-k8s-demo 的 Pod 关联起来。

第五步:部署到 Kubernetes

应用两个资源清单:

kubectl apply -f k8s/

检查 Deployment 和 Pod 是否在跑:

kubectl get deployments
kubectl get pods
NAME READY UP-TO-DATE AVAILABLE AGE
go-k8s-demo 3/3 3 3 20s NAME READY STATUS RESTARTS AGE
go-k8s-demo-6d8b9f5c4d-4xqjk 1/1 Running 0 20s
go-k8s-demo-6d8b9f5c4d-7pnl2 1/1 Running 0 20s
go-k8s-demo-6d8b9f5c4d-kztr9 1/1 Running 0 20s

检查 Service:

kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
go-k8s-demo NodePort 10.96.145.203 <none> 80:31234/TCP 20s

访问应用

kind 集群里 NodePort 服务不能直接在 localhost 访问。用 kubectl port-forward 把流量从本机隧道到 Service:

kubectl port-forward service/go-k8s-demo 8080:80

保持这个命令运行,在第二个终端:

curl http://localhost:8080

预期返回:

{ "message": "Hello from Kubernetes!", "hostname": "go-k8s-demo-7c7b7dd9f9-n7q56", "version": "v1", "time": "2026-06-02T16:36:47Z"
}

注意:kubectl port-forward 隧道到单个 Pod,所以 hostname 字段在多次请求中是一样的。在真正的生产集群里,用 LoadBalancer 服务类型(阿里云 SLB、腾讯云 CLB),流量会分发到所有 Pod,每次请求会看到不同的 hostname。

第六步:执行滚动更新

构建第二版镜像,改个消息,模拟应用升级。

修改 main.go——把消息改成 "Hello from Kubernetes v2!"

构建新镜像并加载到 kind 集群,用第三步同样的方法:

docker build -t go-k8s-demo:v2 .
docker save go-k8s-demo:v2 -o go-k8s-demo-v2.tar
MSYS_NO_PATHCONV=1 docker --context desktop-linux cp go-k8s-demo-v2.tar desktop-control-plane:/go-k8s-demo-v2.tar
MSYS_NO_PATHCONV=1 docker --context desktop-linux exec desktop-control-plane ctr images import /go-k8s-demo-v2.tar

更新 Deployment 使用新镜像:

kubectl set image deployment/go-k8s-demo go-k8s-demo=go-k8s-demo:v2

实时观察滚动过程:

kubectl rollout status deployment/go-k8s-demo
Waiting for deployment "go-k8s-demo" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "go-k8s-demo" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "go-k8s-demo" rollout to finish: 1 old replicas are pending termination...
deployment "go-k8s-demo" successfully rolled out

Kubernetes 一次停一个旧 Pod 起一个新 Pod,全程保持至少两个在跑。应用全程没下线。

回滚到 v1:

kubectl rollout undo deployment/go-k8s-demo

第七步:扩缩 Deployment

扩到 5 个副本:

kubectl scale deployment go-k8s-demo --replicas=5
kubectl get pods

缩回来:

kubectl scale deployment go-k8s-demo --replicas=2

Kubernetes 向多余的 Pod 发送 SIGTERM,等它们退出后再移除。

第八步:检查和排障

日常 Kubernetes 操作的常用命令:

# 查看 Pod 详情——显示事件、探针结果、资源使用
kubectl describe pod <pod-name> # 流式查看所有匹配标签的 Pod 日志
kubectl logs -l app=go-k8s-demo -f # 查看指定 Pod 日志
kubectl logs <pod-name> # 在运行的容器里打开 shell
kubectl exec -it <pod-name> -- sh # 监视 default 命名空间里的所有资源
kubectl get all

清理

用完清理:

kubectl delete -f k8s/

这会删除 Deployment 和 Service。kind 集群在后台继续运行。想删除集群,去 Docker Desktop 的 设置 → Kubernetes 删除,或者重建一个干净状态的集群。

项目结构总结

最终项目结构:

go-k8s-demo/
├── main.go
├── go.mod
├── Dockerfile
└── k8s/ ├── deployment.yaml └── service.yaml

内容回顾

  • 构建了一个带 /healthcheck 端点的 Go HTTP 服务器。
  • 用多阶段构建把应用打包成极简 Docker 镜像。
  • 把镜像加载到 Docker Desktop 里的本地 kind Kubernetes 集群。
  • 用 Deployment 声明期望状态(3 副本、存活/就绪探针、资源限制)和 Service(稳定 DNS、负载均衡)。
  • 执行了零宕机滚动更新和回滚。
  • 用一条命令扩缩副本数量。

这些 deployment.yamlservice.yaml 不改一行就能跑在生产 Kubernetes 集群(阿里云 ACK、腾讯云 TKE、华为云 CCE)上——主要区别是把镜像指向真实镜像仓库,以及通常把 imagePullPolicy 设为 Always 或者每个版本用唯一标签。这里练习的概念和工作流程是通用的。

现在你拥有了一个完整的本地开发环境,真实的 Kubernetes 节点跑在 Docker 里!