Skip to content

02 Dockerfile编写

可以通过手动进入容器安装依赖、修改配置,再使用docker commit保存结果。但这种方式难以重复,不便维护,也不利于审计。Dockerfile的价值在于把镜像构建过程写成文本文件,使不同人员和环境都能按照同一份定义构建出一致的镜像。

一、第一个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"]容器启动时执行的默认命令

构建镜像:

bash
docker build -t my-agent:1.0 .

其中,-t用于指定镜像名和标签,最后的.表示Dockerfile位于当前目录。

二、核心指令详解

2.1 FROM——选择基础镜像

每个Dockerfile都要从FROM开始,用它指定基础镜像。

dockerfile
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——设置工作目录

dockerfile
WORKDIR /app

后面的COPYRUNCMD等指令都会在这个目录下执行。如果目录不存在,Docker会自动创建。

应使用WORKDIR,而不是用RUN cd /app切换目录。 WORKDIR会影响后续所有指令,而RUN cd只在那一条RUN里有效。

2.3 COPY——复制文件

dockerfile
# 把宿主机的文件复制到镜像里
COPY requirements.txt ./
COPY src ./src

# 从其他构建阶段复制
COPY --from=builder /app/output ./

COPY有两个参数:源路径在宿主机上,目标路径在镜像里。源路径是相对于Dockerfile所在目录来算的。

2.4 RUN——执行命令

dockerfile
# 安装依赖
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——设置环境变量

dockerfile
ENV PYTHONUNBUFFERED=1
ENV APP_PORT=8080

通过ENV设置的环境变量在容器运行时可用,也可以在docker run时用-e覆盖。

2.6 EXPOSE——声明端口

dockerfile
EXPOSE 8080

EXPOSE只是声明容器会监听哪个端口,并不会实际发布端口。外部访问服务时,还需要在docker run中使用-p做端口映射。

2.7 CMD——默认启动命令

dockerfile
CMD ["python", "src/main.py"]

CMD指定容器启动时默认执行的命令。如果docker run时额外指定了命令,它会覆盖Dockerfile里的CMD

建议使用JSON数组格式,也就是exec格式。 不要写成CMD python src/main.py这种shell格式。exec格式的进程能正确接收信号;shell格式会多包一层shell,信号传递容易出问题。

2.8 USER——指定运行用户

dockerfile
# 创建非root用户
RUN addgroup -S app && adduser -S app -G app

# 切换到非root用户
USER app

容器默认用root运行,这会带来安全风险。 生产镜像应该创建并切换到非root用户。

2.9 ENTRYPOINT——入口点

dockerfile
ENTRYPOINT ["python", "src/main.py"]

ENTRYPOINTCMD类似,但ENTRYPOINT不会被docker run后面的参数直接覆盖。这些参数会被拼接到ENTRYPOINT后面。

常见的配合方式:

dockerfile
ENTRYPOINT ["s3cmd"]
CMD ["--help"]

运行docker run s3cmd时会显示帮助,运行docker run s3cmd ls s3://mybucket时则会执行实际命令。

三、.dockerignore文件

.dockerignore用于告诉Docker在构建镜像时忽略哪些文件,作用类似.gitignore

plaintext
.git
.env
__pycache__
*.pyc
node_modules
.venv
*.md

.dockerignore主要有三个作用:

  1. 减小构建上下文——Docker会把当前目录的文件发送给Daemon,排除无关文件后传输更快
  2. 避免敏感信息泄露——.env文件里可能有API Key,不应该被打进镜像
  3. 避免缓存失效——node_modules这类目录变化频繁,排除后不会因为它们变动导致重建

四、构建最佳实践

4.1 利用构建缓存

Docker会缓存每一层的构建结果。某一层的输入没变时,Docker会直接复用缓存,跳过执行。

关键思路是:把变化频率低的指令放前面,变化频率高的放后面。

dockerfile
# 差的写法——代码一改,依赖全重装
COPY . .
RUN pip install -r requirements.txt

# 好的写法——先装依赖,再复制代码
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .

先复制requirements.txt安装依赖,再复制整个项目。仅修改代码时,依赖安装这一层仍可命中缓存,不需要每次重新安装依赖。

4.2 合并RUN指令

每条RUN都会生成一个层。层数增加会带来额外开销,因此可以合并的命令应尽量合并:

dockerfile
# 差的写法——三层
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 updateapt-get install要放在同一条RUN里。如果分开写,apt-get update一旦被缓存,后面的apt-get install可能会拿到过期的包索引。

4.3 用Alpine镜像

Alpine Linux是一个极简的Linux发行版,基础镜像只有几MB。大多数官方镜像也都提供Alpine版本:

dockerfile
FROM python:3.12-alpine    # ~50MB
FROM node:22-alpine         # ~180MB
FROM nginx:alpine           # ~40MB

和动辄几百MB甚至1GB的完整版相比,Alpine能明显减小镜像体积。不过它用apk而不是apt-get安装包,用musl而不是glibc,少数情况下会遇到兼容问题。

4.4 不要安装不必要的包

镜像中安装的每个包都会带来额外体积和潜在风险。数据库镜像不需要文本编辑器,Web服务镜像也不应包含不必要的编译工具。

五、完整的Python Agent Dockerfile

结合前面的原则,一个更接近生产使用的Agent服务Dockerfile如下:

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时,可以遵循以下原则:

  1. 层序优化——变化少的指令放前面,变化多的放后面
  2. 层合并——能合并的RUN就合并,减少层数
  3. 最小化——用Alpine基础镜像、不装多余包、非root运行

镜像分层机制和构建缓存会直接影响构建速度、镜像体积和维护成本,是Dockerfile优化的基础。