
最近折腾了用 Docker 和 Kubernetes 部署 Go 应用,踩了几个坑,这篇把问题说清楚。
我们先写一个真实的 Go HTTP 服务,用 Docker 容器化它,然后部署到本地 Kubernetes 集群。这里用的是 kind(Kubernetes in Docker)——它把 Kubernetes 节点跑在 Docker 容器里,在阿里云、腾讯云这种云服务上跑之前,拿来本地练手非常方便,不需要注册什么云账号。
学完这篇,你将掌握滚动更新和扩缩容——全在自己的机器上搞定。
开始之前先验证下环境:
go version
docker version
kubectl version --client
新建项目目录并初始化 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"
}
在项目根目录创建 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"]
这是多阶段构建:
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 直接操作:构建镜像、验证容器能在机器上跑。
Kubernetes 是下一层。以前用 docker run 手动起一个容器,现在让 Kubernetes 帮你保活。它会创建 Pod、重启不健康的容器、通过 Service 暴露应用,后面还能做滚动更新。
本地教程用 Docker Desktop 的 kind 来创建小型 Kubernetes 集群。kind 把节点跑在 Docker 容器里,所以集群还是本地的,但对 kubectl 来说行为和真实 Kubernetes 集群一样。
进入 设置 → Kubernetes → 创建 Kubernetes 集群。
有两个集群类型可选:
kind load docker-image 显式加载到集群里,Kubernetes 才能使用。选 kind,节点数保持 1,点 创建。首次创建会拉取集群组件,要一两分钟。等状态指示灯变绿,验证集群是否就绪:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
desktop-control-plane Ready control-plane 60s v1.34.3
Docker Desktop 自动把 kubectl 的上下文切到新集群,不需要手动切换。
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 或其他镜像仓库拉取。
创建存放资源清单的目录:
mkdir k8s
创建 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 是硬上限。两个都设是生产环境最佳实践。创建 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 关联起来。
应用两个资源清单:
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
扩到 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 服务器。这些 deployment.yaml 和 service.yaml 不改一行就能跑在生产 Kubernetes 集群(阿里云 ACK、腾讯云 TKE、华为云 CCE)上——主要区别是把镜像指向真实镜像仓库,以及通常把 imagePullPolicy 设为 Always 或者每个版本用唯一标签。这里练习的概念和工作流程是通用的。
现在你拥有了一个完整的本地开发环境,真实的 Kubernetes 节点跑在 Docker 里!