AI摘要
直到现在,我还记得第一次在自己的MacBook上成功运行docker run hello-world时,那个命令行里跳出来的“Hello from Docker!”带给我的那种奇妙的兴奋感。那感觉就像第一次学会骑自行车——你知道这只是一个开始,前方是全新的世界。
但很快,这种兴奋就被现实打得粉碎。我亲手打包的第一个Spring Boot应用,在Docker容器里怎么也连不上本地的MySQL。那个红色的连接异常日志,像一盆冷水浇醒了我的美梦。
三年过去了,我从一个连容器和虚拟机都分不清的新手,到能在团队里带着新人趟过容器化的坑。这篇记录,就是这三年里那些让我凌晨三点还在查文档、那些“啊哈!”顿悟瞬间的真实合集。这不是一篇完美的教程,而是一份“伤痕文学”——希望我的伤,能让你走得顺一点。
第一章:Docker篇——当“在我的机器上能运行”成为诅咒
坑1:镜像构建的“空间恐惧症”
我的第一个Dockerfile简单得近乎天真:
FROM ubuntu:latest
RUN apt-get update && apt-get install -y openjdk-8-jdk
COPY . /app
WORKDIR /app
CMD ["java", "-jar", "myapp.jar"]构建镜像,2.7GB。是的,你没看错,2.7GB。上传到仓库要20分钟,同事下载时看我的眼神都不对劲了。
我学到的:
ubuntu:latest是个巨兽。请用openjdk:8-jre-slim或alpine版本,它们能轻量到让你感动。- 每个
RUN命令都会产生一层镜像。一定要合并命令,并且记得清理apt缓存:
RUN apt-get update && apt-get install -y --no-install-recommends \
openjdk-8-jdk \
&& rm -rf /var/lib/apt/lists/*坑2:那个永远连不上的“localhost”
我在application.yml里写的数据库地址是jdbc:mysql://localhost:3306/mydb。容器启动成功,但应用死活连不上数据库。我花了整整一个下午,在Stack Overflow上各种尝试,直到看到一句话:“容器有自己的网络命名空间,localhost指向的是容器自己,不是你的宿主机。”
解决方案有几个,取决于场景:
- 开发时:用
host.docker.internal(Docker Desktop)或172.17.0.1(Linux docker0网桥)代替localhost - 更好的做法:永远不要在配置里写死主机名。用环境变量:
spring:
datasource:
url: jdbc:mysql://${DB_HOST:localhost}:3306/mydb然后运行容器时:docker run -e DB_HOST=host.docker.internal myapp
坑3:权限的幽灵——那些“Permission Denied”
我把一个本地的日志目录挂载到容器里:-v ./logs:/app/logs。应用启动失败,报“Permission denied”。原来,容器内的进程通常以非root用户运行(这是好事!),但宿主机上的./logs目录属于我的用户,容器用户没权限写。
解决办法:
- 简单粗暴但危险:让容器以root运行(
--user root),不推荐。 - 在Dockerfile里创建用户并设置合适的UID:
RUN groupadd -g 1000 appuser && useradd -u 1000 -g appuser appuser
USER appuser- 或者,在宿主机上设置目录权限为
777(仅用于开发)。
坑4:构建缓存的“薛定谔”状态
有时候我只是改了源码里的一行注释,重新构建镜像,却发现Docker又从apt-get update开始执行,慢得要死。有时候我希望它完全不使用缓存,它却固执地用了旧缓存。
我学到的构建缓存原理:
- Docker的构建是一层层的,每一层都有哈希。
- 如果某一层没变化,Docker就会复用缓存。
- 一旦某一层发生了变化,它之后的所有层都会重新构建。
所以,聪明的Dockerfile应该把不常变的内容放在前面:
# 1. 基础镜像
FROM openjdk:11-jre-slim
# 2. 安装依赖(不常变)
COPY requirements.txt .
RUN pip install -r requirements.txt # 这层不常变
# 3. 复制应用代码(常变)
COPY . /app # 这层一变,后面的层都会重建如果想完全禁用缓存:docker build --no-cache .
第二章:Kubernetes篇——从“这玩意真酷”到“这玩意真复杂”
当我第一次用minikube在本地启动Kubernetes集群,看到dashboard上那些跳动的Pod时,感觉像是拿到了未来科技的钥匙。但很快,现实就来教我做人。
坑5:YAML地狱与“复制粘贴工程师”
我的第一个Deployment YAML是从网上抄的。改了镜像名,应用了,然后Pod一直CrashLoopBackOff。我看日志,日志说“配置文件找不到”。原来我从网上抄的YAML里,挂载了一个不存在的configMap。
血的教训:
- 不要盲目复制YAML。理解每一行是干什么的。
- 从最简单的开始:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:latest
ports:
- containerPort: 8080- 用
kubectl explain命令!这是你的救星。不知道某个字段什么意思?kubectl explain deployment.spec.template.spec.containers
坑6:那些永远“Pending”的Pod
我写了一个完美的Deployment,kubectl apply之后,Pod状态一直是Pending。kubectl describe pod一看,事件里写着“Insufficient cpu/memory”。
原来:
- 我的minikube默认只给了2GB内存,而我的Pod请求了1.5GB内存,但节点上还有其他系统Pod在运行。
- 资源请求(requests)和限制(limits)是两回事:
resources:
requests:
memory: "512Mi"
cpu: "250m" # 250毫核
limits:
memory: "1Gi"
cpu: "500m"requests是调度依据,limits是运行限制。别乱写,写小了Pod可能被OOMKilled,写大了Pod可能永远调度不出去。
坑7:Service的“我有三种Type,但都不如人意”
我想让外部能访问我的应用。于是:
- 用
NodePort:kubectl get svc看到端口是32456,用minikube ip:32456访问,成功了!高兴了五分钟,然后发现重启minikube后IP变了,端口也变了。 - 用
LoadBalancer:在minikube里,这其实只是NodePort的别名,真正的云上LoadBalancer是要钱的! 最后发现开发环境的神器:
port-forwardkubectl port-forward svc/myapp-service 8080:80现在访问
localhost:8080就能访问服务了。但这只是临时的,断开命令就没了。
坑8:ConfigMap的“更新了但没完全更新”
我在ConfigMap里改了个配置,然后kubectl apply -f config.yaml。满怀期待地重启Pod,发现配置没变!原来,挂载为Volume的ConfigMap,更新后不会自动注入到已运行的Pod中。我必须删除Pod,让Deployment重建它。
但有个例外:如果通过环境变量envFrom引用ConfigMap,那Pod启动后就固定了,ConfigMap更新不会影响。
变通方案(开发环境可用):
- 直接
kubectl edit configmap my-config,改完保存。 - 删除Pod:
kubectl delete pod mypod-xxxxx - 新Pod会读取新的ConfigMap。
坑9:那些“消失”的日志
我kubectl logs mypod,发现日志只从一小时前开始。更早的日志呢?消失了。原来,容器默认的日志驱动是写到标准输出,但磁盘空间有限,会自动轮转清理。
几个选择:
- 用
kubectl logs --previous看上一个容器的日志(如果Pod重启了)。 - 配置日志收集(EFK/Loki栈),这对开发环境太重了。
- 对于开发,最重要的习惯是:在出问题时第一时间
kubectl logs,别等。
第三章:本地开发工作流——寻找甜蜜点
经历了这些坑后,我意识到需要一个顺手的本地开发工作流。
我的当前方案:
用Docker Compose处理依赖:MySQL、Redis、RabbitMQ这些中间件,用
docker-compose.yml一键启动。version: '3' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root ports: - "3306:3306"- 应用本身用Kubernetes,但用Telepresence或k8d:这让我能在本地IDE里调试代码,但代码能访问K8s集群里的其他服务。这是开发体验的质的飞跃。
用Skaffold自动化:在代码更改时自动重建镜像、更新K8s部署。在
skaffold.yaml里配置:apiVersion: skaffold/v2beta3 kind: Config build: artifacts: - image: myapp context: . deploy: kubectl: manifests: - k8s/*.yaml然后运行
skaffold dev,就可以专注于写代码了。
最后的一点真心话
三年容器化之路,我最深的体会是:容器化不是目标,而是一种手段。它的目标是可重复、可移植、可扩展的软件交付。
不要为了用Kubernetes而用Kubernetes。如果你的应用只是一个简单的Spring Boot服务,也许用Docker Compose就足够了。只有当你有多个服务需要编排、需要自动伸缩、需要复杂的部署策略时,K8s的光芒才会真正显现。
从今天开始,你可以:
- 从Docker开始,把你的应用容器化。
- 在本地用minikube或kind(Kubernetes in Docker)搭个集群玩玩。
- 尝试把应用部署上去,遇到坑,解决它。
- 优化你的开发工作流,让容器化不是负担,而是助力。
这条路有很多坑,但每填平一个坑,你就比昨天的自己更强了一点。而且,当你第一次看到你的应用在K8s上自动扩缩容,或者你只用一条命令就把整个环境复制给新同事时,你会觉得,这一切都是值得的。
现在,如果你要开始你的容器化之路,我的建议是:从一个小应用开始,给自己一个周末的时间,允许自己犯错,允许自己谷歌。因为每一个踩过这些坑的人,都曾是那个对着“Connection refused”一筹莫展的新手。