IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。
之前我们一直在用别人构建好的镜像。docker pull nginx、docker pull redis ------ 这些镜像都是社区或官方团队提前构建并发布到 Docker Hub 上的。虽然方便,但真正的价值在于:你能把自己的应用也打包成一个标准的、可移植的镜像。
当一个新的开发同事入职,不再需要花一两天配置环境,一条命令就能把你构建好的应用跑起来;当你要部署到服务器,不再担心"明明在我机器上能跑";当你要做水平扩展,直接基于同一个镜像启动更多容器------这就是编写 Dockerfile 的意义。
在这一篇,我们将从零开始,把贯穿本系列的 Flask + Redis 计数器应用 变成一个标准的 Docker 镜像。我们会逐行拆解 Dockerfile 的每一个指令,然后执行构建,亲眼见证一个镜像的诞生过程。
同时,本文还会补充介绍在编写 Dockerfile 时涉及到的相关技巧------从 PYTHONUNBUFFERED 变量对容器日志的深远影响,到 pip wheel 预编译依赖和 .dockerignore 文件如何加速构建,再到常规优化常识与 trivy 漏洞扫描工具的初步使用------这些技巧能让你的镜像更贴近现代生产实践。
一、前置准备:认识今天的"容器化对象"
1.1 贯穿案例回顾
整个系列围绕一个贯穿案例展开:一个基于 Flask 的 Web 应用,结合 Redis 做页面访问计数。第 2 篇我们跑过它的极简版本,第 4 篇我们正式把它容器化。
项目目录结构如下:
bash
flask-redis-counter/
├── app.py # Flask 应用主程序
├── requirements.txt # Python 依赖清单
├── Dockerfile # 我们将要编写的镜像构建文件
└── .dockerignore # 排除不需要打包到镜像中的文件
1.2 准备 app.py
创建 app.py,内容如下:
bash
import time
import redis
from flask import Flask
app = Flask(__name__)
# 连接 Redis,host='redis' 后续在 Compose/K8s 中通过服务名解析
cache = redis.Redis(host='redis', port=6379, decode_responses=True)
def get_hit_count():
"""带重试机制的 Redis 计数器获取,容器启动时可容忍 Redis 暂未就绪"""
retries = 5
while True:
try:
return cache.incr('hits')
except redis.exceptions.ConnectionError as exc:
if retries == 0:
raise exc
retries -= 1
time.sleep(0.5)
@app.route('/')
def hello():
count = get_hit_count()
return f'Hello World! I have been seen {count} times.\n'
@app.route('/health')
def health():
"""健康检查端点,K8s 探针将调用此接口"""
return {'status': 'ok'}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
代码解读:
-
redis.Redis(host='redis', port=6379):连接 Redis 时,主机名写的是redis,这本质上是一个环境约定。在 Docker Compose 中,redis是 Compose 文件里定义的服务名;在 Kubernetes 中,则对应一个名为redis的 Service 对象。下一行decode_responses=True确保从 Redis 读取的数据自动解码为 Python 字符串,省去手动 decode。 -
get_hit_count()中包含重试逻辑:当 Redis 尚未就绪时,应用不会直接崩溃,而是等待并重试,最多 5 次。这在实际环境中非常实用。 -
我们还预留了一个
/health端点,后面配置探针时会用到。
1.3 准备 requirements.txt
bash
flask==3.1.1
redis==6.4.0
两个依赖足以让这个计数器应用运行起来。Pinned 版本避免后续构建时的版本漂移。
1.4 准备 .dockerignore
在项目根目录创建 .dockerignore,避免将无关文件发送到 Docker 构建上下文:
bash
__pycache__
*.pyc
*.pyo
*.log
.env
.git
.gitignore
*.md
.vscode
.idea
venv
.venv
*.tar
*.gz
Dockerfile
.dockerignore
构建上下文大小影响 :docker build 第一步会把当前目录下的所有文件打包发送给 Docker Daemon。不写 .dockerignore,本地虚拟环境 venv/(可能有数百 MB)也会被发送过去,白白浪费数秒甚至数十秒。写上 .dockerignore,构建上下文从几百 MB 降到几十 KB,Sending build context 这一步几乎瞬间完成。
二、动手写第一个 Dockerfile
2.1 关键补充:构建上下文与构建缓存
在进入 Dockerfile 核心指令前,有两点需要提前说明------它们直接决定了你对 Dockerfile 执行过程的整体理解。
构建上下文
当你执行 docker build -t myapp . 时,末尾的 . 就是构建上下文(Build Context)的路径,表示当前目录。Docker 客户端会将这个目录下的所有文件递归打包,发送给 Docker Daemon(守护进程)。然后,Daemon 在构建过程中就可以使用 COPY 或 ADD 指令从这个上下文中引用文件。
这也解释了 .dockerignore 之所以如此关键:它直接控制了上下文大小,影响构建的第一步耗时。
构建缓存
Docker 构建是逐层进行的。每一条指令(如 FROM、RUN、COPY)都会产生一个新的镜像层,并被缓存。下一次构建时,如果某条指令及其依赖的上下文内容未发生变化,Docker 会直接复用缓存中的层,输出 Using cache。一旦某条指令的输入发生变化,该层及之后的所有层缓存全部失效,必须重新构建。这就是为什么依赖文件要单独 COPY 并提前安装------让"变更频率低"的指令排在前面,充分利用缓存。
有了这两个概念打底,再看下面的 Dockerfile 示例,你就能理解为什么指令要按这个顺序写。
2.2 完整 Dockerfile
创建 Dockerfile(无扩展名),内容如下:
bash
# syntax=docker/dockerfile:1
# ============================================================
# Flask + Redis 计数器应用 ------ Dockerfile
# 本系列贯穿案例:从 Docker 容器化到 Kubernetes 生产部署
# ============================================================
# ---- 阶段 1:构建阶段(pip wheel 预编译依赖)----
FROM python:3.12-slim AS builder
# 设置工作目录
WORKDIR /build
# 安装构建依赖(gcc 等,用于编译含 C 扩展的 Python 包)
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc python3-dev && \
rm -rf /var/lib/apt/lists/*
# 单独复制依赖清单(利用 Docker 层缓存)
COPY requirements.txt .
# 使用 pip wheel 预编译所有依赖为 .whl 文件
# --no-cache-dir: 不缓存下载的包,减小镜像体积
# --wheel-dir: 指定 .whl 输出目录
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
# ---- 阶段 2:运行阶段(最终镜像)----
FROM python:3.12-slim
# === 容器元数据 ===
LABEL maintainer="IT策士" \
description="Flask + Redis 访问计数器,本系列贯穿案例" \
version="1.0"
# === 环境变量 ===
# PYTHONUNBUFFERED=1:禁用 Python 标准输出缓冲,确保容器日志实时输出
# PYTHONDONTWRITEBYTECODE=1:禁止 Python 生成 .pyc 文件
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
# === 创建非 root 用户 ===
# -m:创建家目录 /home/appuser
# -u 1000:指定 UID 为 1000(Linux 惯例,普通用户从 1000 开始)
RUN groupadd -r appuser && \
useradd -r -m -u 1000 -g appuser appuser
WORKDIR /app
# === 安装运行时依赖 ===
# 从构建阶段复制预编译的 .whl 文件并安装
COPY --from=builder /wheels /wheels
COPY requirements.txt .
# --no-index 表示不从 PyPI 下载,只从本地 /wheels 目录安装
# --find-links 指定本地 wheel 查找路径
RUN pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt && \
rm -rf /wheels requirements.txt
# === 复制应用代码 ===
# --chown 将文件所有权设置为 appuser(避免 root 拥有的文件被容器内进程访问时产生权限问题)
COPY --chown=appuser:appuser . .
# === 切换到非 root 用户 ===
# 在此之后的 RUN/CMD/ENTRYPOINT 都以 appuser 身份运行
USER appuser
# === 声明监听端口 ===
# EXPOSE 仅是文档性质的声明,实际端口映射依赖 docker run -p 或 K8s Service
EXPOSE 5000
# === 健康检查 ===
# 每 30 秒使用 curl 访问 /health 端点,超时 3 秒,启动后等 5 秒再开始检查,失败 3 次标记为不健康
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1
# === 容器启动命令 ===
# CMD 指定镜像默认的启动命令(可被 docker run 后面的参数覆盖)
CMD ["python", "app.py"]
三、逐行拆解 Dockerfile 核心指令
现在我们逐行拆解每个指令的含义、用法和最佳实践。
3.1 FROM ------ 指定基础镜像
bash
FROM python:3.12-slim AS builder
-
FROM是每个 Dockerfile 必须有的第一条指令(注释和ARG除外)。 -
它指定了构建的基础镜像,后续所有操作都在这个镜像之上进行。
-
python:3.12-slim:基于 Debian 的精简版 Python 镜像,约 120MB。相比完整版(约 1GB),slim 去掉了编译工具和文档,但仍保留了标准 glibc,兼容绝大多数 Python 包。Slim 镜像是最稳妥的选择;Alpine 虽小,但因使用 musl libc,部分 Python 包(尤其是含 C 扩展的)可能遇到兼容性问题。 -
AS builder:给这个阶段起一个别名builder,便于后续阶段通过COPY --from=builder引用。
3.2 WORKDIR ------ 设置工作目录
-
WORKDIR设置后续所有RUN、CMD、ENTRYPOINT、COPY、ADD指令的默认工作目录。 -
如果目录不存在,Docker 会自动创建。
-
推荐用
WORKDIR而不是RUN cd /app:后者在每条RUN指令之间不保留状态(每条RUN都从一个新的 shell 开始),而WORKDIR是持久化的。
3.3 COPY ------ 复制文件到镜像
bash
COPY requirements.txt .
COPY . .
-
COPY <src> <dest>从构建上下文中将<src>复制到镜像文件系统的<dest>路径。 -
COPY requirements.txt .中的.代表当前WORKDIR所指定的目录(此处为/app)。 -
先单独
COPY依赖清单,再COPY源码------这是镜像构建中最重要的优化手段之一:只要requirements.txt不变,依赖安装层就走缓存,无需每次因为代码改动就重新下载依赖。 -
ADD指令也能复制文件,但ADD还支持从 URL 下载和自动解压 tar 包,语义更复杂。Docker 官方明确建议:除非需要自动解压功能,否则始终使用COPY,以保证 Dockerfile 的可读性与可预测性。 -
--chown=appuser:appuser:设定复制后文件的所有者为appuser用户和appuser用户组,确保文件权限正确。
3.4 RUN ------ 执行构建命令
bash
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc && \
rm -rf /var/lib/apt/lists/*
-
RUN在镜像构建过程中执行命令,通常用于安装软件、下载依赖、编译代码等。 -
注意这里用了
&&将多个操作串联为一条RUN:apt-get update、apt-get install和rm -rf /var/lib/apt/lists/*(清理 apt 缓存文件),全部打包在一个镜像层中。如果把更新、安装和清理写成三个RUN,中间层的缓存文件会持续占用存储,即使上层删除也无法回收该空间。 -
--no-install-recommends:只安装必须的依赖包,不安装建议的附加包,进一步减小镜像体积。
补充说明 :构建阶段最后还用到了 RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt。这与直接在最终阶段执行 pip install 不同------pip wheel 会提前将依赖编译为 .whl 二进制包,最终阶段只需本地安装这些 wheel,无需再次从 PyPI 下载,也无需在最终镜像中保留 gcc 等编译工具,从而实现构建依赖与运行依赖的彻底分离。
3.5 CMD ------ 默认启动命令
-
CMD定义了容器启动时的默认命令。每个 Dockerfile 只有最后一个CMD生效。 -
当用户执行
docker run myimage时,Docker 会自动执行CMD中指定的命令。 -
可以被覆盖:
docker run myimage /bin/bash会用/bin/bash替换掉CMD。
CMD 的三种写法:
3.6 CMD vs ENTRYPOINT ------ 如何选择?
CMD 和 ENTRYPOINT 都用于定义容器启动时的行为,但它们有本质区别:
一句话总结 :ENTRYPOINT 指定"跑什么程序",CMD 指定"传什么参数"。
当 ENTRYPOINT 和 CMD 同时存在时,CMD 的内容会作为参数追加给 ENTRYPOINT。例如:
bash
ENTRYPOINT ["python"]
CMD ["app.py"]
# 等价于执行:python app.py
# docker run myimage test.py 会覆盖 CMD,实际执行:python test.py
我们的示例用的是 CMD ["python", "app.py"],因为这只是一个简单的开发/实验环境。到第 5 篇你会看到如何用 ENTRYPOINT 配合 gunicorn 实现更生产化的启动方式。
3.7 ENV ------ 设置环境变量
bash
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
ENV在整个容器生命周期内有效,包括后续的RUN、CMD以及容器运行时的进程。
核心补充:PYTHONUNBUFFERED 与容器日志的深层关系
在标准 Linux 系统中,当 stdout 指向终端时,C 标准库(glibc)默认使用行缓冲 (line buffered)------每输出一行就立即刷新;但当 stdout 被重定向到管道或文件时,C 标准库会自动切换为全缓冲(fully buffered)------必须攒够数 KB 的数据才做一次 write() 系统调用。
容器环境恰好命中后一种情况:Docker 通过管道捕获容器内进程的 stdout/stderr,Python 进程的 stdout 在 Python 层面也是行缓冲的,但如果 Python 底层调用的 C 扩展(如某些数据库驱动)直接向 C stdout 写入,就会受到 glibc 全缓冲的影响,导致日志卡住、延迟数分钟才输出。这就是不少开发者在容器中遇到 docker logs 迟迟看不到输出的深层原因。
PYTHONUNBUFFERED=1 将 Python 层的 stdout 和 stderr 设为无缓冲模式 ------每产生一个字节就立即输出,彻底根治上述缓冲延迟问题。因此业界普遍将 ENV PYTHONUNBUFFERED=1 视为 Python 容器镜像的必备设置之一。
PYTHONDONTWRITEBYTECODE=1 则是禁止 Python 生成 .pyc 字节码缓存文件。.pyc 文件在本地开发中有加速第二次启动的作用,但在容器场景下毫无意义------容器本身就是不可变的,docker build 只执行一次,运行时不会重新编译 Python 源码。禁用 .pyc 可减小镜像体积,也让镜像层更加干净。
3.8 EXPOSE ------ 声明端口
-
EXPOSE声明容器运行时监听的端口。 -
重要提示 :
EXPOSE本身不会自动将端口映射到宿主机!端口映射需要通过docker run -p或 Compose/K8s 配置来实现。 -
EXPOSE的真正作用更多是文档性质------告诉使用者"这个容器内的应用监听 5000 端口"。在 Kubernetes 中,EXPOSE的值可以被某些工具用于自动发现端口。
3.9 HEALTHCHECK ------ 健康检查
bash
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1
HEALTHCHECK告诉 Docker 如何判断容器是否正常工作。Docker 会在容器内定期执行CMD中定义的命令,根据退出状态码判断健康状态。
参数详解:
-
--interval=30s:每 30 秒检查一次 -
--timeout=3s:单次检查超过 3 秒视为失败 -
--start-period=5s:容器启动后等待 5 秒再开始检查(给应用启动时间) -
--retries=3:连续失败 3 次才标记为 unhealthy
返回值 :0 表示健康,1 表示不健康。此处的 || exit 1 是典型的 shell 短路逻辑------当 curl -f 成功(返回 0)时,exit 1 不会被执行;只有当 curl -f 失败(返回非 0)时,才执行 exit 1 告诉 Docker "我不健康了"。
四、动手构建:从 Dockerfile 到可运行镜像
4.1 docker build 基础
Dockerfile 写好了,接下来用 docker build 把它"编译"成可运行的镜像。
基本语法:
bash
docker build -t <镜像名>:<标签> <构建上下文路径>
-
-t:给镜像打标签(名称 + 版本)。 -
<构建上下文路径>:通常是.(当前目录),Docker 会将该目录下的所有文件发送给 Docker Daemon 用作构建上下文。
注意 :构建由 Docker Daemon 执行,而非 CLI,因此 COPY 指令只能引用构建上下文内的文件,不能引用上下文路径之外的文件。
常用参数:
4.2 执行构建
确认目录结构:
bash
flask-redis-counter/
├── app.py
├── requirements.txt
├── Dockerfile
└── .dockerignore
在项目根目录下执行:
bash
docker build -t flask-redis-counter:1.0 .
让我们逐段解读完整输出:
bash
[+] Building 45.2s (17/17) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 1.8kB 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 412B 0.0s
=> [internal] load metadata for docker.io/library/ 2.1s
python:3.12-slim
第一步 :Docker 加载 Dockerfile 和 .dockerignore,然后拉取基础镜像 python:3.12-slim 的元数据。注意 .dockerignore 让上下文只有 412B------如果没写它,venv/ 和 __pycache__/ 全打包进去,上下文可能膨胀到几百 MB。
bash
=> [builder 1/4] FROM docker.io/library/python:3.12- 12.3s
slim@sha256:a04c2f8c...
=> => resolve docker.io/library/python:3.12-slim@sha256 0.0s
=> => sha256:2d35ec5109b2... 7.38MB / 7.38MB 2.1s
=> => sha256:8b8e7c31c414... 35.5MB / 35.5MB 8.7s
=> => extracting 3.5s
第二步:拉取基础镜像层。这里每一行都是一层------底层 Debian 文件系统、Python 运行时、系统库等。Extracting 是将各层解压并联合挂载。
bash
=> [builder 2/4] WORKDIR /build 0.1s
=> [builder 3/4] RUN apt-get update && ... 15.2s
=> [builder 4/4] RUN pip wheel --no-cache-dir ... 8.3s
第三步 :builder 阶段执行 apt-get 安装编译工具,然后用 pip wheel 将所有 Python 依赖预编译为 .whl 文件。
bash
=> [stage-1 4/11] RUN groupadd -r appuser && ... 0.4s
=> [stage-1 5/11] WORKDIR /app 0.0s
=> [stage-1 6/11] COPY --from=builder /wheels /wheels 0.2s
=> [stage-1 7/11] COPY requirements.txt . 0.1s
=> [stage-1 8/11] RUN pip install --no-cache-dir ... 3.1s
=> [stage-1 9/11] COPY --chown=appuser:appuser . . 0.1s
=> [stage-1 10/11] USER appuser 0.0s
=> [stage-1 11/11] HEALTHCHECK ... 0.0s
第四步 :运行阶段的构建,逐层执行------创建用户、设置工作目录、从 builder 阶段复制预编译的 wheel 包、安装依赖(这一步因为使用 --no-index --find-links=/wheels,仅从本地安装,无需访问 PyPI,速度极快)、复制应用代码、切换用户、设置健康检查。
bash
=> exporting to image 2.8s
=> => exporting layers 2.8s
=> => writing image sha256:e8f9a0b1c2... 0.0s
=> => naming to docker.io/library/flask-redis-counter:1.0 0.0s
最后一步 :将所有层打包导出为一个完整镜像,并打上标签 flask-redis-counter:1.0。
4.3 验证镜像
bash
docker images flask-redis-counter
输出:
bash
REPOSITORY TAG IMAGE ID CREATED SIZE
flask-redis-counter 1.0 e8f9a0b1c2d3 10 seconds ago 138MB
138MB 的镜像包含了 Python 运行时、Flask、Redis 客户端以及我们的应用代码。
4.4 运行容器并测试
bash
# 启动 Redis
docker run -d --name my-redis redis:alpine
# 创建自定义网络,让两个容器互访(第 8 篇会深入讲网络)
docker network create counter-net
docker network connect counter-net my-redis
# 启动 Flask 应用,加入同一网络,让 Redis 主机名可解析
docker run -d --name my-flask --network counter-net -p 5000:5000 flask-redis-counter:1.0
# 验证
curl http://localhost:5000
输出:
bash
Hello World! I have been seen 1 times.
再执行几次:
bash
Hello World! I have been seen 2 times.
Hello World! I have been seen 3 times.
每刷新一次,计数器就加 1------Flask 通过容器网络连接 Redis 并成功读写数据。
验证健康检查:
输出中会包含健康检查状态:
bash
CONTAINER ID IMAGE STATUS PORTS
a1b2c3d4e5f6 flask-redis-counter:1.0 Up 2 minutes (healthy) 0.0.0.0:5000->5000/tcp
(healthy) 表示 Docker 已经执行了 HEALTHCHECK 中定义的命令并验证通过。
验证非 root 运行:
bash
docker exec my-flask whoami
输出:
容器内的进程以 appuser 用户身份运行,而不是 root。这降低了安全风险------如果容器内的应用被攻破,攻击者只能以非特权用户身份操作,无法直接获得宿主机的 root 访问权限。
bash
docker inspect my-flask --format='{{.Config.User}}'
输出:
docker inspect 确认容器级别的用户设置为 appuser。
4.5 查看镜像分层
bash
docker history flask-redis-counter:1.0
输出(部分):
bash
IMAGE CREATED CREATED BY SIZE
e8f9a0b1c2d3 2 minutes ago HEALTHCHECK ... CMD curl -f ... 0B
<missing> 2 minutes ago USER appuser 0B
<missing> 2 minutes ago COPY --chown=appuser:appuser . . 3.5kB
<missing> 2 minutes ago RUN pip install ... && rm -rf /wheels 12MB
<missing> 2 minutes ago COPY requirements.txt . 28B
<missing> 2 minutes ago RUN groupadd -r appuser && ... 1.2kB
<missing> 3 weeks ago CMD ["python3"] 0B
<missing> 3 weeks ago ENV PYTHON_VERSION=3.12.8 0B
<missing> 3 weeks ago /bin/sh -c set -eux; ... 35.5MB
<missing> 3 weeks ago ADD file:... in / 120MB
第一行是我们自己构建的层,越往下越接近基础镜像。最底层是 Debian 基础文件系统 + Python 运行时(约 120MB),顶层是我们的应用代码(仅 3.5kB)。与早期示例中庞大臃肿的镜像形成鲜明对比------通过多阶段构建和 pip wheel 预编译,构建依赖(gcc、python3-dev)被隔离在 builder 阶段,最终镜像体积约 138MB,而如果将所有构建工具打入最终镜像,体积可能膨胀到 500MB 以上。
五、镜像安全扫描
在将镜像推送到仓库或部署到生产环境之前,扫描已知漏洞是一个基本安全实践。
5.1 使用 Trivy 扫描镜像
Trivy 是一个开源的容器镜像漏洞扫描工具,轻量且无需 Docker Desktop 即可运行:
bash
# 安装 Trivy(以 Ubuntu 为例)
sudo apt-get install -y wget apt-transport-https
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | \
sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install -y trivy
# 扫描镜像
trivy image flask-redis-counter:1.0
输出示例:
bash
2026-05-27T10:00:00.000+0800 INFO Vulnerability scanning is enabled
2026-05-27T10:00:01.500+0800 INFO Detected OS: debian
2026-05-27T10:00:02.200+0800 INFO Number of language-specific files: 1
2026-05-27T10:00:02.200+0800 INFO Detecting pip vulnerabilities...
flask-redis-counter:1.0 (debian 12.8)
======================================
Total: 3 (CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 0)
┌──────────────┬────────────────┬──────────┬────────┬───────────────────┐
│ Library │ Vulnerability │ Severity │ Status │ Installed Version │
├──────────────┼────────────────┼──────────┼────────┼───────────────────┤
│ libssl3 │ CVE-2025-... │ HIGH │ fixed │ 3.1.5-1 │
│ libcrypto3 │ CVE-2025-... │ MEDIUM │ fixed │ 3.1.5-1 │
│ flask │ CVE-2025-... │ MEDIUM │ fixed │ 3.1.1 │
└──────────────┴────────────────┴──────────┴────────┴───────────────────┘
关于扫描结果:Trivy 会根据其漏洞数据库比对镜像中各软件包的版本。如果发现已知 CVE 漏洞,会列出严重级别和修复版本。上例仅作示意,实际扫描结果取决于镜像中的具体包版本和当前 Trivy 数据库版本。若扫描出 HIGH 或 CRITICAL 级别漏洞,建议尽快升级对应软件包并重新构建镜像。
在实际项目中,建议将漏洞扫描集成到 CI/CD 流水线中,并设置质量门禁------例如,存在 CRITICAL 或 HIGH 级别漏洞的镜像不允许推送到生产仓库。由于 Python 的 Flask 与 Redis 客户端均不涉及编译型语言的二进制安全风险,在生产中更常见的做法是结合定期重新构建镜像 + Trivy CI 扫描来持续控制供应链风险。
六、本篇总结
6.1 核心知识点回顾
-
镜像的本质 :通过
docker build从 Dockerfile 构建的可运行模板,由只读层叠加而成。 -
Dockerfile 核心指令 :
FROM(基础镜像)、WORKDIR(工作目录)、RUN(构建时命令)、COPY(复制文件,优先于 ADD)、ENV(环境变量,特别注意 PYTHONUNBUFFERED)、EXPOSE(声明端口)、CMD(默认启动命令,可被覆盖)、ENTRYPOINT(固定入口)、HEALTHCHECK(容器健康检查)。 -
CMD vs ENTRYPOINT :
CMD可被docker run后的参数覆盖,适合提供默认命令;ENTRYPOINT定义固定入口点,CMD作为其默认参数。 -
构建上下文 :
docker build .末尾的.指定构建上下文路径;.dockerignore排除不需要的文件,加速构建并避免敏感信息泄露。 -
构建缓存 :利用"变更频率低的指令写前面"原则,先
COPY依赖清单再COPY源码,命中缓存可显著加速二次构建。 -
多阶段构建 :
FROM ... AS builder+FROM ...+COPY --from=builder,构建依赖与运行依赖分离,镜像体积可缩小 50-70%。 -
pip wheel 预编译 :构建阶段提前将所有依赖编译为
.whl二进制包,最终阶段仅需pip install --no-index --find-links=/wheels本地安装,无需访问 PyPI,也无需在最终镜像中保留 gcc 等编译工具。 -
非 root 用户运行 :通过
RUN groupadd/useradd+USER降低安全风险。 -
镜像安全扫描:使用 Trivy 扫描已知 CVE 漏洞,集成到 CI/CD 流水线中形成质量门禁。
6.2 命令速查表
6.3 本篇完成后的项目结构
bash
flask-redis-counter/
├── app.py # Flask 应用主程序
├── requirements.txt # Python 依赖清单(flask, redis)
├── Dockerfile # 多阶段构建的 Docker 镜像构建文件
├── .dockerignore # 构建上下文排除规则
└── docker-compose.yml # 待第 11 篇编写(多容器编排)
下一篇文章------第 5 篇:Dockerfile 最佳实践与多阶段构建 ,我们将进一步深入多阶段构建的理论与实践,对比单阶段与多阶段的镜像体积差异,并学习更多生产级优化技巧:.dockerignore 进阶、构建缓存调优、镜像标签管理等。准备好让你的镜像"瘦身"了吗?
想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维!