Docker 从 0 到 1 再到 Kubernetes 实战:第4篇 编写你的第一个 Dockerfile

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。

之前我们一直在用别人构建好的镜像。docker pull nginxdocker 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 在构建过程中就可以使用 COPYADD 指令从这个上下文中引用文件。

这也解释了 .dockerignore 之所以如此关键:它直接控制了上下文大小,影响构建的第一步耗时。

构建缓存

Docker 构建是逐层进行的。每一条指令(如 FROMRUNCOPY)都会产生一个新的镜像层,并被缓存。下一次构建时,如果某条指令及其依赖的上下文内容未发生变化,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 设置后续所有 RUNCMDENTRYPOINTCOPYADD 指令的默认工作目录。

  • 如果目录不存在,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 在镜像构建过程中执行命令,通常用于安装软件、下载依赖、编译代码等。

  • 注意这里用了 && 将多个操作串联为一条 RUNapt-get updateapt-get installrm -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 ------ 如何选择?

CMDENTRYPOINT 都用于定义容器启动时的行为,但它们有本质区别:

一句话总结ENTRYPOINT 指定"跑什么程序",CMD 指定"传什么参数"。

ENTRYPOINTCMD 同时存在时,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 在整个容器生命周期内有效,包括后续的 RUNCMD 以及容器运行时的进程。

核心补充: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 ENTRYPOINTCMD 可被 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 思维!

相关推荐
ZzzZZzzzZZZzzzz…7 分钟前
K8s---网络:从 Pod 网络模型到 Calico 三种模式
运维·网络·云原生·容器·kubernetes·calico·pod网络模型
xhaxy10 小时前
docker
docker·容器·eureka
上火的金鱼妹12 小时前
K8S基础组件作用和关系整理
linux·运维·服务器·kubernetes
一技安身12 小时前
【信创】Docker‑Compose V2 两种离线部署(独立模式、插件模式)简易教程
java·docker·eureka
鸿观工坊16 小时前
Docker 多阶段构建实践指南
docker
大牧师18 小时前
MySQL 学习教程
数据库·sql·mysql·docker·node·全栈·后端数据
MC丶科19 小时前
软考架构师90天冲刺|DAY44·Redis高级应用
数据库·数据仓库·redis·缓存·oracle·容器·规格说明书
天外天-亮20 小时前
docker + window 安装
运维·docker·容器
进击的小菜鸡dd20 小时前
docker 容器的挂载映射关系查看
运维·docker·容器
SLD_Allen20 小时前
从 HPA 到 KEDA:Kubernetes 弹性伸缩的进阶实践
云原生·容器·kubernetes