02 Dockerfile编写
手动进容器装依赖、改配置,然后docker commit保存——这种方式能用,但不可重复、不可维护、不可审计。Dockerfile就是来解决这个问题的:用一个文本文件描述镜像的构建过程,任何人都能用它构建出一模一样的镜像。
一、第一个Dockerfile
Dockerfile就是一个纯文本文件,文件名就叫Dockerfile(没有扩展名)。来看一个最简单的例子:
FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY src ./src
EXPOSE 8080
CMD ["python", "src/main.py"]逐行解释:
| 指令 | 作用 |
|---|---|
FROM python:3.12-alpine | 基础镜像,基于Python 3.12的Alpine版本 |
WORKDIR /app | 设置工作目录,后续命令都在这个目录下执行 |
COPY requirements.txt ./ | 把宿主机的文件复制到镜像里 |
RUN pip install ... | 在镜像里执行命令 |
EXPOSE 8080 | 声明容器监听的端口 |
CMD ["python", "src/main.py"] | 容器启动时执行的默认命令 |
构建镜像:
docker build -t my-agent:1.0 .-t指定镜像名和标签,.表示Dockerfile在当前目录。
二、核心指令详解
2.1 FROM——选择基础镜像
每个Dockerfile必须以FROM开头,指定基础镜像。
FROM python:3.12-alpine
FROM node:22-alpine
FROM ubuntu:24.04选择基础镜像的原则:
| 原则 | 说明 |
|---|---|
| 用官方镜像 | Docker Hub上有Docker Official Images标记的 |
| 用小镜像 | Alpine版本比完整版小很多,python:3.12-alpine约50MB,python:3.12约1GB |
| 指定版本 | 不要用latest,明确版本号保证构建一致 |
2.2 WORKDIR——设置工作目录
WORKDIR /app后续的COPY、RUN、CMD等指令都在这个目录下执行。如果目录不存在会自动创建。
用WORKDIR而不是RUN cd /app,因为WORKDIR对后续所有指令生效,RUN cd只在那一条RUN里有效。
2.3 COPY——复制文件
# 把宿主机的文件复制到镜像里
COPY requirements.txt ./
COPY src ./src
# 从其他构建阶段复制
COPY --from=builder /app/output ./COPY有两个参数:源路径(宿主机)和目标路径(镜像内)。源路径是相对于Dockerfile所在目录的。
2.4 RUN——执行命令
# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt
# 安装系统包(Debian/Ubuntu)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*RUN在构建时执行,每条RUN生成一个新的镜像层。多个命令用&&连接可以减少层数。
2.5 ENV——设置环境变量
ENV PYTHONUNBUFFERED=1
ENV APP_PORT=8080设置的环境变量在容器运行时可用,也可以在docker run时用-e覆盖。
2.6 EXPOSE——声明端口
EXPOSE 8080EXPOSE只是声明,不会实际发布端口。真正要从外部访问,还需要在docker run时用-p映射。
2.7 CMD——默认启动命令
CMD ["python", "src/main.py"]CMD指定容器启动时执行的默认命令。docker run时如果指定了命令,会覆盖CMD。
注意用JSON数组格式(exec格式),不要用CMD python src/main.py(shell格式)。exec格式的进程能正确接收信号,shell格式的进程是shell的子进程,信号传递有问题。
2.8 USER——指定运行用户
# 创建非root用户
RUN addgroup -S app && adduser -S app -G app
# 切换到非root用户
USER app容器默认用root运行,这有安全风险。 生产镜像应该创建并使用非root用户。
2.9 ENTRYPOINT——入口点
ENTRYPOINT ["python", "src/main.py"]和CMD类似,但ENTRYPOINT不会被docker run的参数覆盖,而是把docker run的参数拼接到ENTRYPOINT后面。
常见的配合方式:
ENTRYPOINT ["s3cmd"]
CMD ["--help"]这样docker run s3cmd显示帮助,docker run s3cmd ls s3://mybucket执行实际命令。
三、.dockerignore文件
.dockerignore告诉Docker构建时忽略哪些文件,类似.gitignore。
.git
.env
__pycache__
*.pyc
node_modules
.venv
*.md为什么要用它:
- 减小构建上下文——Docker会把当前目录所有文件发送给Daemon,
.dockerignore排除不需要的文件,加快传输 - 避免敏感信息泄露——
.env文件可能包含API Key,不应该打包进镜像 - 避免缓存失效——
node_modules变化频繁,排除后不会因为它的变化导致重建
四、构建最佳实践
4.1 利用构建缓存
Docker会缓存每一层的构建结果。如果某一层的输入没有变化,Docker会直接用缓存,跳过执行。
关键原则:把变化频率低的指令放前面,变化频率高的放后面。
# 差的写法——代码一改,依赖全重装
COPY . .
RUN pip install -r requirements.txt
# 好的写法——先装依赖,再复制代码
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .先复制requirements.txt安装依赖,再复制整个项目。这样改代码时依赖安装这层会命中缓存,不用重装。
4.2 合并RUN指令
每条RUN生成一个层,层越多镜像越大。能合并的就合并:
# 差的写法——三层
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# 好的写法——一层
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*apt-get update和apt-get install必须放同一条RUN里。如果分开,apt-get update被缓存后,后续的apt-get install可能用过期的包索引。
4.3 用Alpine镜像
Alpine Linux是一个极简的Linux发行版,基础镜像只有几MB。大多数官方镜像都有Alpine版本:
FROM python:3.12-alpine # ~50MB
FROM node:22-alpine # ~180MB
FROM nginx:alpine # ~40MB对比完整版动辄几百MB甚至1GB,Alpine能大幅减小镜像体积。但Alpine用apk而不是apt-get安装包,用musl而不是glibc,少数情况下可能有兼容问题。
4.4 不要安装不必要的包
镜像里装的每个包都是潜在的安全风险。数据库镜像不需要文本编辑器,Web服务镜像不需要编译工具。
五、完整的Python Agent Dockerfile
结合前面的知识,一个生产级的Agent服务Dockerfile:
FROM python:3.12-alpine
# 创建非root用户
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
# 先复制依赖文件,利用缓存
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# 再复制源代码
COPY src ./src
# 设置环境变量
ENV PYTHONUNBUFFERED=1
# 切换到非root用户
USER app
# 声明端口
EXPOSE 8080
# 启动命令
CMD ["python", "src/main.py"]六、总结
Dockerfile的核心就这些:
| 指令 | 作用 | 执行时机 |
|---|---|---|
FROM | 基础镜像 | 构建时 |
WORKDIR | 工作目录 | 构建时 |
COPY | 复制文件 | 构建时 |
RUN | 执行命令 | 构建时 |
ENV | 环境变量 | 运行时 |
EXPOSE | 声明端口 | 运行时 |
CMD | 默认命令 | 运行时 |
USER | 运行用户 | 运行时 |
编写Dockerfile记住三条原则:
- 层序优化——变化少的指令放前面,变化多的放后面
- 层合并——能合并的
RUN就合并,减少层数 - 最小化——用Alpine基础镜像、不装多余包、非root运行
下一篇我们会深入镜像的分层机制和构建缓存,搞懂Docker为什么构建这么快、怎么让它更快。