010、Docker镜像实战:从Dockerfile编写到构建自定义镜像

一个线上服务问题,测试环境能复现,本地开发机死活跑不出来。折腾半天才发现,原来是本地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这些。

五、个人经验包

  1. 标签别偷懒:不用latest标签,而是用myapp:2023-12-01myapp:git-commit-id。某次线上回滚,因为大家都用latest,根本不知道当前跑的是哪个版本。

  2. 镜像扫描别忘了:用docker scan查漏洞,特别是基础镜像。有次安全扫描发现一个两年前的OpenSSL漏洞,就因为我们一直用着旧的基础镜像。

  3. 构建缓存有时是敌人docker build --no-cache在依赖更新时很有用。遇到过pip从缓存装了旧版本包,调试了三小时才发现不是代码问题。

  4. 本地开发别过度优化:生产镜像要小,但开发镜像可以适当大点,把调试工具、vim、curl都装进去,排查问题方便。可以用同一个Dockerfile,不同target区分阶段。

  5. 理解镜像层原理:经常变动的指令放后面,基础配置放前面。这样重建镜像时,前面几层缓存还能用,节省的不只是时间,还有团队带宽。

最后说个真事:以前团队里有个镜像叫project:latest,用了两年没人敢删。后来磁盘满了才发现,这镜像从来没人真正用过,是某个实习生测试时建的。Docker镜像像房间里的杂物,得定期清理——docker image prune该用就用。

镜像打包好了,怎么跑起来?下一篇我们聊容器运行时的那些门道。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AILabNotes

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值