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等指令都在这个目录下执行。如果目录不存在会自动创建。

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

设置的环境变量在容器运行时可用,也可以在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时如果指定了命令,会覆盖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"]

CMD类似,但ENTRYPOINT不会被docker run的参数覆盖,而是把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

为什么要用它:

  1. 减小构建上下文——Docker会把当前目录所有文件发送给Daemon,.dockerignore排除不需要的文件,加快传输
  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能大幅减小镜像体积。但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运行

下一篇我们会深入镜像的分层机制和构建缓存,搞懂Docker为什么构建这么快、怎么让它更快。