一个线上服务问题,测试环境能复现,本地开发机死活跑不出来。折腾半天才发现,原来是本地Python版本和测试环境差了0.1个小版本,某个依赖库的行为有细微差异。这种“我机器上好好的”问题,在团队协作里太常见了。今天咱们就彻底解决它——用Docker把运行环境打包成镜像,让代码在哪跑都一样。
一、别急着写Dockerfile,先想清楚你要什么
很多人一上来就FROM ubuntu,然后开始apt-get install一大堆。等构建完才发现镜像体积好几个G,传输慢启动也慢。其实大部分时候,我们并不需要一个完整的操作系统。
比如你要跑一个Python Web服务,完全可以用官方精简镜像:
# 别用 python:latest 这种浮动标签,生产环境会出事的
FROM python:3.9-slim-buster # 指定具体版本和变体
# 设置工作目录,不然文件会散落在根目录
WORKDIR /app
slim版本基于Debian,只包含运行Python的最小环境,比完整版Ubuntu镜像小一半以上。如果是Go应用,可以用scratch(空镜像)或alpine(仅5MB),但要注意glibc兼容问题——这里踩过坑,有些动态链接的二进制在alpine里跑不起来。
二、Dockerfile的细节魔鬼
看看这个反例:
FROM ubuntu
RUN apt-get update
RUN apt-get install -y python3 python3-pip # 每行RUN都会生成镜像层
RUN pip3 install flask
RUN pip3 install requests
RUN pip3 install gunicorn
COPY . /app # 代码变动时,这行往后的缓存全部失效
问题在哪?第一,RUN指令太多,镜像层数爆炸(Dockerfile每行命令产生一个层)。第二,依赖安装和代码拷贝顺序不对,改一行代码就要重装所有依赖。
应该这么写:
# 先拷贝依赖声明文件
COPY requirements.txt /tmp/requirements.txt
# 安装依赖——这些变动较少,Docker会缓存这一层
RUN pip install --no-cache-dir -r /tmp/requirements.txt
# 最后拷贝代码,代码变动时不会触发依赖重装
COPY . /app
还有个容易忽略的点:清理缓存。apt-get install后记得跟&& apt-get clean && rm -rf /var/lib/apt/lists/*,否则安装包还留在镜像里,白占空间。
三、多阶段构建:镜像瘦身神器
以前给客户部署一个Go服务,二进制文件就10MB,镜像却带了完整Go工具链,硬生生拉到800MB。后来用多阶段构建:
# 第一阶段:构建环境
FROM golang:1.19 AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download # 单独下载依赖,利用缓存
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app .
# 第二阶段:运行环境
FROM alpine:latest
RUN apk --no-cache add ca-certificates # 加个证书库,不然可能HTTPS报错
WORKDIR /root/
COPY --from=builder /build/app . # 只从上一阶段拷贝编译结果
CMD ["./app"]
这样最终镜像只有十几MB,而且更安全——运行环境里连编译器都没有,攻击面小多了。Java用Maven构建、前端用Node打包,都可以照这个思路来。
四、那些实际调试时才会遇到的坑
时区问题:容器里默认是UTC时间,日志时间对不上。可以在Dockerfile里设:
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
权限问题:用root跑应用不安全,但直接切用户可能遇到目录没权限:
RUN groupadd -r appuser && useradd -r -g appuser appuser
RUN chown -R appuser:appuser /app
USER appuser # 从这里开始切换用户
环境变量注入:配置文件别写死在镜像里:
# 这样写,启动时能覆盖
ENV DB_HOST=localhost DB_PORT=5432
构建时用docker build -t myapp:v1 .,注意最后那个点表示当前目录是构建上下文。曾经有同事把整个.git目录都打包进去了,因为Docker默认会把上下文目录所有文件发给守护进程——记得写.dockerignore文件,排除.git、pycache、node_modules这些。
五、个人经验包
-
标签别偷懒:不用
latest标签,而是用myapp:2023-12-01或myapp:git-commit-id。某次线上回滚,因为大家都用latest,根本不知道当前跑的是哪个版本。 -
镜像扫描别忘了:用
docker scan查漏洞,特别是基础镜像。有次安全扫描发现一个两年前的OpenSSL漏洞,就因为我们一直用着旧的基础镜像。 -
构建缓存有时是敌人:
docker build --no-cache在依赖更新时很有用。遇到过pip从缓存装了旧版本包,调试了三小时才发现不是代码问题。 -
本地开发别过度优化:生产镜像要小,但开发镜像可以适当大点,把调试工具、vim、curl都装进去,排查问题方便。可以用同一个Dockerfile,不同target区分阶段。
-
理解镜像层原理:经常变动的指令放后面,基础配置放前面。这样重建镜像时,前面几层缓存还能用,节省的不只是时间,还有团队带宽。
最后说个真事:以前团队里有个镜像叫project:latest,用了两年没人敢删。后来磁盘满了才发现,这镜像从来没人真正用过,是某个实习生测试时建的。Docker镜像像房间里的杂物,得定期清理——docker image prune该用就用。
镜像打包好了,怎么跑起来?下一篇我们聊容器运行时的那些门道。


被折叠的 条评论
为什么被折叠?



