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 篇我们正式把它容器化。项目目录结构如下flask-redis-counter/ ├── app.py# Flask 应用主程序├── requirements.txt# Python 依赖清单├── Dockerfile# 我们将要编写的镜像构建文件└── .dockerignore# 排除不需要打包到镜像中的文件1.2 准备 app.py创建app.py内容如下importtimeimportredis from flaskimportFlask appFlask(__name__)# 连接 Redishostredis 后续在 Compose/K8s 中通过服务名解析cacheredis.Redis(hostredis,port6379,decode_responsesTrue)def get_hit_count():带重试机制的 Redis 计数器获取容器启动时可容忍 Redis 暂未就绪 retries5whileTrue: try:returncache.incr(hits)except redis.exceptions.ConnectionError as exc:ifretries0: raise exc retries -1time.sleep(0.5)app.route(/)def hello(): countget_hit_count()returnfHello World! I have been seen {count} times.\napp.route(/health)def health():健康检查端点K8s 探针将调用此接口return{status:ok}if__name____main__:app.run(host0.0.0.0,port5000)代码解读redis.Redis(hostredis, port6379)连接 Redis 时主机名写的是redis这本质上是一个环境约定。在 Docker Compose 中redis是 Compose 文件里定义的服务名在 Kubernetes 中则对应一个名为redis的 Service 对象。下一行decode_responsesTrue确保从 Redis 读取的数据自动解码为 Python 字符串省去手动 decode。get_hit_count()中包含重试逻辑当 Redis 尚未就绪时应用不会直接崩溃而是等待并重试最多 5 次。这在实际环境中非常实用。我们还预留了一个/health端点后面配置探针时会用到。1.3 准备 requirements.txtflask3.1.1redis6.4.0两个依赖足以让这个计数器应用运行起来。Pinned 版本避免后续构建时的版本漂移。1.4 准备 .dockerignore在项目根目录创建.dockerignore避免将无关文件发送到 Docker 构建上下文__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 降到几十 KBSending build context这一步几乎瞬间完成。二、动手写第一个 Dockerfile2.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无扩展名内容如下# syntaxdocker/dockerfile:1# # Flask Redis 计数器应用 —— Dockerfile# 本系列贯穿案例从 Docker 容器化到 Kubernetes 生产部署# # ---- 阶段 1构建阶段pip wheel 预编译依赖----FROM python:3.12-slim AS builder# 设置工作目录WORKDIR /build# 安装构建依赖gcc 等用于编译含 C 扩展的 Python 包RUNapt-getupdate\apt-getinstall-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-rrequirements.txt# ---- 阶段 2运行阶段最终镜像----FROM python:3.12-slim# 容器元数据 LABELmaintainerIT策士\descriptionFlask Redis 访问计数器本系列贯穿案例\version1.0# 环境变量 # PYTHONUNBUFFERED1禁用 Python 标准输出缓冲确保容器日志实时输出# PYTHONDONTWRITEBYTECODE1禁止 Python 生成 .pyc 文件ENVPYTHONUNBUFFERED1\PYTHONDONTWRITEBYTECODE1# 创建非 root 用户 # -m创建家目录 /home/appuser# -u 1000指定 UID 为 1000Linux 惯例普通用户从 1000 开始RUNgroupadd-rappuser\useradd-r-m-u1000-gappuser appuser WORKDIR /app# 安装运行时依赖 # 从构建阶段复制预编译的 .whl 文件并安装COPY--frombuilder /wheels /wheels COPY requirements.txt.# --no-index 表示不从 PyPI 下载只从本地 /wheels 目录安装# --find-links 指定本地 wheel 查找路径RUN pipinstall--no-cache-dir --no-index --find-links/wheels-rrequirements.txt\rm-rf/wheels requirements.txt# 复制应用代码 # --chown 将文件所有权设置为 appuser避免 root 拥有的文件被容器内进程访问时产生权限问题COPY--chownappuser:appuser..# 切换到非 root 用户 # 在此之后的 RUN/CMD/ENTRYPOINT 都以 appuser 身份运行USERappuser# 声明监听端口 # EXPOSE 仅是文档性质的声明实际端口映射依赖 docker run -p 或 K8s ServiceEXPOSE5000# 健康检查 # 每 30 秒使用 curl 访问 /health 端点超时 3 秒启动后等 5 秒再开始检查失败 3 次标记为不健康HEALTHCHECK--interval30s--timeout3s --start-period5s--retries3\CMDcurl-fhttp://localhost:5000/health||exit1# 容器启动命令 # CMD 指定镜像默认的启动命令可被 docker run 后面的参数覆盖CMD[python,app.py]三、逐行拆解 Dockerfile 核心指令现在我们逐行拆解每个指令的含义、用法和最佳实践。3.1 FROM —— 指定基础镜像FROM python:3.12-slim AS builderFROM是每个 Dockerfile 必须有的第一条指令注释和ARG除外。它指定了构建的基础镜像后续所有操作都在这个镜像之上进行。python:3.12-slim基于 Debian 的精简版 Python 镜像约 120MB。相比完整版约 1GBslim 去掉了编译工具和文档但仍保留了标准 glibc兼容绝大多数 Python 包。Slim 镜像是最稳妥的选择Alpine 虽小但因使用 musl libc部分 Python 包尤其是含 C 扩展的可能遇到兼容性问题。AS builder给这个阶段起一个别名builder便于后续阶段通过COPY --frombuilder引用。3.2 WORKDIR —— 设置工作目录WORKDIR设置后续所有RUN、CMD、ENTRYPOINT、COPY、ADD指令的默认工作目录。如果目录不存在Docker 会自动创建。推荐用WORKDIR而不是RUN cd /app后者在每条RUN指令之间不保留状态每条RUN都从一个新的 shell 开始而WORKDIR是持久化的。3.3 COPY —— 复制文件到镜像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 的可读性与可预测性。--chownappuser:appuser设定复制后文件的所有者为appuser用户和appuser用户组确保文件权限正确。3.4 RUN —— 执行构建命令RUNapt-getupdate\apt-getinstall-y--no-install-recommends gcc\rm-rf/var/lib/apt/lists/*RUN在镜像构建过程中执行命令通常用于安装软件、下载依赖、编译代码等。注意这里用了将多个操作串联为一条RUNapt-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。例如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 —— 设置环境变量ENVPYTHONUNBUFFERED1\PYTHONDONTWRITEBYTECODE1ENV在整个容器生命周期内有效包括后续的RUN、CMD以及容器运行时的进程。核心补充PYTHONUNBUFFERED 与容器日志的深层关系在标准 Linux 系统中当 stdout 指向终端时C 标准库glibc默认使用行缓冲line buffered——每输出一行就立即刷新但当 stdout 被重定向到管道或文件时C 标准库会自动切换为全缓冲fully buffered——必须攒够数 KB 的数据才做一次 write() 系统调用。容器环境恰好命中后一种情况Docker 通过管道捕获容器内进程的 stdout/stderrPython 进程的 stdout 在 Python 层面也是行缓冲的但如果 Python 底层调用的 C 扩展如某些数据库驱动直接向 C stdout 写入就会受到 glibc 全缓冲的影响导致日志卡住、延迟数分钟才输出。这就是不少开发者在容器中遇到docker logs迟迟看不到输出的深层原因。PYTHONUNBUFFERED1将 Python 层的 stdout 和 stderr 设为无缓冲模式——每产生一个字节就立即输出彻底根治上述缓冲延迟问题。因此业界普遍将ENV PYTHONUNBUFFERED1视为 Python 容器镜像的必备设置之一。PYTHONDONTWRITEBYTECODE1则是禁止 Python 生成.pyc字节码缓存文件。.pyc文件在本地开发中有加速第二次启动的作用但在容器场景下毫无意义——容器本身就是不可变的docker build 只执行一次运行时不会重新编译 Python 源码。禁用.pyc可减小镜像体积也让镜像层更加干净。3.8 EXPOSE —— 声明端口EXPOSE声明容器运行时监听的端口。重要提示EXPOSE本身不会自动将端口映射到宿主机端口映射需要通过docker run -p或 Compose/K8s 配置来实现。EXPOSE的真正作用更多是文档性质——告诉使用者“这个容器内的应用监听 5000 端口”。在 Kubernetes 中EXPOSE的值可以被某些工具用于自动发现端口。3.9 HEALTHCHECK —— 健康检查HEALTHCHECK--interval30s--timeout3s --start-period5s--retries3\CMDcurl-fhttp://localhost:5000/health||exit1HEALTHCHECK告诉 Docker 如何判断容器是否正常工作。Docker 会在容器内定期执行CMD中定义的命令根据退出状态码判断健康状态。参数详解--interval30s每 30 秒检查一次--timeout3s单次检查超过 3 秒视为失败--start-period5s容器启动后等待 5 秒再开始检查给应用启动时间--retries3连续失败 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把它“编译”成可运行的镜像。基本语法dockerbuild-t镜像名:标签构建上下文路径-t给镜像打标签名称 版本。构建上下文路径通常是.当前目录Docker 会将该目录下的所有文件发送给 Docker Daemon 用作构建上下文。注意构建由 Docker Daemon 执行而非 CLI因此COPY指令只能引用构建上下文内的文件不能引用上下文路径之外的文件。常用参数4.2 执行构建确认目录结构flask-redis-counter/ ├── app.py ├── requirements.txt ├── Dockerfile └── .dockerignore在项目根目录下执行dockerbuild-tflask-redis-counter:1.0.让我们逐段解读完整输出[]Building45.2s(17/17)FINISHED[internal]load build definition from Dockerfile0.0stransferring dockerfile:1.8kB0.0s[internal]load .dockerignore0.0stransferring context: 412B0.0s[internal]load metadatafordocker.io/library/2.1s python:3.12-slim第一步Docker 加载 Dockerfile 和.dockerignore然后拉取基础镜像python:3.12-slim的元数据。注意.dockerignore让上下文只有 412B——如果没写它venv/和__pycache__/全打包进去上下文可能膨胀到几百 MB。[builder1/4]FROM docker.io/library/python:3.12-12.3s slimsha256:a04c2f8c...resolve docker.io/library/python:3.12-slimsha2560.0ssha256:2d35ec5109b2...7.38MB /7.38MB2.1ssha256:8b8e7c31c414...35.5MB /35.5MB8.7sextracting3.5s第二步拉取基础镜像层。这里每一行都是一层——底层 Debian 文件系统、Python 运行时、系统库等。Extracting 是将各层解压并联合挂载。[builder2/4]WORKDIR /build0.1s[builder3/4]RUNapt-getupdate...15.2s[builder4/4]RUN pip wheel --no-cache-dir...8.3s第三步builder 阶段执行 apt-get 安装编译工具然后用pip wheel将所有 Python 依赖预编译为.whl文件。[stage-14/11]RUNgroupadd-rappuser...0.4s[stage-15/11]WORKDIR /app0.0s[stage-16/11]COPY--frombuilder /wheels /wheels0.2s[stage-17/11]COPY requirements.txt.0.1s[stage-18/11]RUN pipinstall--no-cache-dir...3.1s[stage-19/11]COPY--chownappuser:appuser..0.1s[stage-110/11]USERappuser0.0s[stage-111/11]HEALTHCHECK...0.0s第四步运行阶段的构建逐层执行——创建用户、设置工作目录、从 builder 阶段复制预编译的 wheel 包、安装依赖这一步因为使用--no-index --find-links/wheels仅从本地安装无需访问 PyPI速度极快、复制应用代码、切换用户、设置健康检查。exporting to image2.8sexporting layers2.8swriting image sha256:e8f9a0b1c2...0.0snaming to docker.io/library/flask-redis-counter:1.00.0s最后一步将所有层打包导出为一个完整镜像并打上标签flask-redis-counter:1.0。4.3 验证镜像dockerimages flask-redis-counter输出REPOSITORY TAG IMAGE ID CREATED SIZE flask-redis-counter1.0e8f9a0b1c2d310seconds ago 138MB138MB 的镜像包含了 Python 运行时、Flask、Redis 客户端以及我们的应用代码。4.4 运行容器并测试# 启动 Redisdockerrun-d--namemy-redis redis:alpine# 创建自定义网络让两个容器互访第 8 篇会深入讲网络dockernetwork create counter-netdockernetwork connect counter-net my-redis# 启动 Flask 应用加入同一网络让 Redis 主机名可解析dockerrun-d--namemy-flask--networkcounter-net-p5000:5000 flask-redis-counter:1.0# 验证curlhttp://localhost:5000输出Hello World!I have been seen1times.再执行几次Hello World!I have been seen2times. Hello World!I have been seen3times.每刷新一次计数器就加 1——Flask 通过容器网络连接 Redis 并成功读写数据。验证健康检查输出中会包含健康检查状态CONTAINER ID IMAGE STATUS PORTS a1b2c3d4e5f6 flask-redis-counter:1.0 Up2minutes(healthy)0.0.0.0:5000-5000/tcp(healthy)表示 Docker 已经执行了HEALTHCHECK中定义的命令并验证通过。验证非 root 运行dockerexecmy-flaskwhoami输出容器内的进程以appuser用户身份运行而不是 root。这降低了安全风险——如果容器内的应用被攻破攻击者只能以非特权用户身份操作无法直接获得宿主机的 root 访问权限。dockerinspect my-flask--format{{.Config.User}}输出docker inspect确认容器级别的用户设置为appuser。4.5 查看镜像分层dockerhistoryflask-redis-counter:1.0输出部分IMAGE CREATED CREATED BY SIZE e8f9a0b1c2d32minutes ago HEALTHCHECK... CMDcurl-f... 0Bmissing2minutes agoUSERappuser 0Bmissing2minutes ago COPY--chownappuser:appuser..3.5kBmissing2minutes ago RUN pipinstall...rm-rf/wheels 12MBmissing2minutes ago COPY requirements.txt.28Bmissing2minutes ago RUNgroupadd-rappuser...1.2kBmissing3weeks ago CMD[python3]0Bmissing3weeks ago ENVPYTHON_VERSION3.12.8 0Bmissing3weeks ago /bin/sh-cset-eux;...35.5MBmissing3weeks ago ADD file:...in/ 120MB第一行是我们自己构建的层越往下越接近基础镜像。最底层是 Debian 基础文件系统 Python 运行时约 120MB顶层是我们的应用代码仅 3.5kB。与早期示例中庞大臃肿的镜像形成鲜明对比——通过多阶段构建和 pip wheel 预编译构建依赖gcc、python3-dev被隔离在 builder 阶段最终镜像体积约 138MB而如果将所有构建工具打入最终镜像体积可能膨胀到 500MB 以上。五、镜像安全扫描在将镜像推送到仓库或部署到生产环境之前扫描已知漏洞是一个基本安全实践。5.1 使用 Trivy 扫描镜像Trivy 是一个开源的容器镜像漏洞扫描工具轻量且无需 Docker Desktop 即可运行# 安装 Trivy以 Ubuntu 为例sudoapt-getinstall-ywgetapt-transport-httpswget-qO- https://aquasecurity.github.io/trivy-repo/deb/public.key|sudoapt-keyadd-echodeb https://aquasecurity.github.io/trivy-repo/deb$(lsb_release-sc)main|\sudotee/etc/apt/sources.list.d/trivy.listsudoapt-getupdatesudoapt-getinstall-ytrivy# 扫描镜像trivy image flask-redis-counter:1.0输出示例2026-05-27T10:00:00.0000800 INFO Vulnerability scanning is enabled2026-05-27T10:00:01.5000800 INFO Detected OS: debian2026-05-27T10:00:02.2000800 INFO Number of language-specific files:12026-05-27T10:00:02.2000800 INFO Detecting pip vulnerabilities... flask-redis-counter:1.0(debian12.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 builderFROM ...COPY --frombuilder构建依赖与运行依赖分离镜像体积可缩小 50-70%。pip wheel 预编译构建阶段提前将所有依赖编译为.whl二进制包最终阶段仅需pip install --no-index --find-links/wheels本地安装无需访问 PyPI也无需在最终镜像中保留 gcc 等编译工具。非 root 用户运行通过RUN groupadd/useraddUSER降低安全风险。镜像安全扫描使用 Trivy 扫描已知 CVE 漏洞集成到 CI/CD 流水线中形成质量门禁。6.2 命令速查表6.3 本篇完成后的项目结构flask-redis-counter/ ├── app.py# Flask 应用主程序├── requirements.txt# Python 依赖清单flask, redis├── Dockerfile# 多阶段构建的 Docker 镜像构建文件├── .dockerignore# 构建上下文排除规则└── docker-compose.yml# 待第 11 篇编写多容器编排下一篇文章——第 5 篇Dockerfile 最佳实践与多阶段构建我们将进一步深入多阶段构建的理论与实践对比单阶段与多阶段的镜像体积差异并学习更多生产级优化技巧.dockerignore进阶、构建缓存调优、镜像标签管理等。准备好让你的镜像“瘦身”了吗想了解更多还可以去各个平台搜索「IT策士」一起升级 IT 思维
Docker 从 0 到 1 再到 Kubernetes 实战:第4篇 编写你的第一个 Dockerfile
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 篇我们正式把它容器化。项目目录结构如下flask-redis-counter/ ├── app.py# Flask 应用主程序├── requirements.txt# Python 依赖清单├── Dockerfile# 我们将要编写的镜像构建文件└── .dockerignore# 排除不需要打包到镜像中的文件1.2 准备 app.py创建app.py内容如下importtimeimportredis from flaskimportFlask appFlask(__name__)# 连接 Redishostredis 后续在 Compose/K8s 中通过服务名解析cacheredis.Redis(hostredis,port6379,decode_responsesTrue)def get_hit_count():带重试机制的 Redis 计数器获取容器启动时可容忍 Redis 暂未就绪 retries5whileTrue: try:returncache.incr(hits)except redis.exceptions.ConnectionError as exc:ifretries0: raise exc retries -1time.sleep(0.5)app.route(/)def hello(): countget_hit_count()returnfHello World! I have been seen {count} times.\napp.route(/health)def health():健康检查端点K8s 探针将调用此接口return{status:ok}if__name____main__:app.run(host0.0.0.0,port5000)代码解读redis.Redis(hostredis, port6379)连接 Redis 时主机名写的是redis这本质上是一个环境约定。在 Docker Compose 中redis是 Compose 文件里定义的服务名在 Kubernetes 中则对应一个名为redis的 Service 对象。下一行decode_responsesTrue确保从 Redis 读取的数据自动解码为 Python 字符串省去手动 decode。get_hit_count()中包含重试逻辑当 Redis 尚未就绪时应用不会直接崩溃而是等待并重试最多 5 次。这在实际环境中非常实用。我们还预留了一个/health端点后面配置探针时会用到。1.3 准备 requirements.txtflask3.1.1redis6.4.0两个依赖足以让这个计数器应用运行起来。Pinned 版本避免后续构建时的版本漂移。1.4 准备 .dockerignore在项目根目录创建.dockerignore避免将无关文件发送到 Docker 构建上下文__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 降到几十 KBSending build context这一步几乎瞬间完成。二、动手写第一个 Dockerfile2.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无扩展名内容如下# syntaxdocker/dockerfile:1# # Flask Redis 计数器应用 —— Dockerfile# 本系列贯穿案例从 Docker 容器化到 Kubernetes 生产部署# # ---- 阶段 1构建阶段pip wheel 预编译依赖----FROM python:3.12-slim AS builder# 设置工作目录WORKDIR /build# 安装构建依赖gcc 等用于编译含 C 扩展的 Python 包RUNapt-getupdate\apt-getinstall-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-rrequirements.txt# ---- 阶段 2运行阶段最终镜像----FROM python:3.12-slim# 容器元数据 LABELmaintainerIT策士\descriptionFlask Redis 访问计数器本系列贯穿案例\version1.0# 环境变量 # PYTHONUNBUFFERED1禁用 Python 标准输出缓冲确保容器日志实时输出# PYTHONDONTWRITEBYTECODE1禁止 Python 生成 .pyc 文件ENVPYTHONUNBUFFERED1\PYTHONDONTWRITEBYTECODE1# 创建非 root 用户 # -m创建家目录 /home/appuser# -u 1000指定 UID 为 1000Linux 惯例普通用户从 1000 开始RUNgroupadd-rappuser\useradd-r-m-u1000-gappuser appuser WORKDIR /app# 安装运行时依赖 # 从构建阶段复制预编译的 .whl 文件并安装COPY--frombuilder /wheels /wheels COPY requirements.txt.# --no-index 表示不从 PyPI 下载只从本地 /wheels 目录安装# --find-links 指定本地 wheel 查找路径RUN pipinstall--no-cache-dir --no-index --find-links/wheels-rrequirements.txt\rm-rf/wheels requirements.txt# 复制应用代码 # --chown 将文件所有权设置为 appuser避免 root 拥有的文件被容器内进程访问时产生权限问题COPY--chownappuser:appuser..# 切换到非 root 用户 # 在此之后的 RUN/CMD/ENTRYPOINT 都以 appuser 身份运行USERappuser# 声明监听端口 # EXPOSE 仅是文档性质的声明实际端口映射依赖 docker run -p 或 K8s ServiceEXPOSE5000# 健康检查 # 每 30 秒使用 curl 访问 /health 端点超时 3 秒启动后等 5 秒再开始检查失败 3 次标记为不健康HEALTHCHECK--interval30s--timeout3s --start-period5s--retries3\CMDcurl-fhttp://localhost:5000/health||exit1# 容器启动命令 # CMD 指定镜像默认的启动命令可被 docker run 后面的参数覆盖CMD[python,app.py]三、逐行拆解 Dockerfile 核心指令现在我们逐行拆解每个指令的含义、用法和最佳实践。3.1 FROM —— 指定基础镜像FROM python:3.12-slim AS builderFROM是每个 Dockerfile 必须有的第一条指令注释和ARG除外。它指定了构建的基础镜像后续所有操作都在这个镜像之上进行。python:3.12-slim基于 Debian 的精简版 Python 镜像约 120MB。相比完整版约 1GBslim 去掉了编译工具和文档但仍保留了标准 glibc兼容绝大多数 Python 包。Slim 镜像是最稳妥的选择Alpine 虽小但因使用 musl libc部分 Python 包尤其是含 C 扩展的可能遇到兼容性问题。AS builder给这个阶段起一个别名builder便于后续阶段通过COPY --frombuilder引用。3.2 WORKDIR —— 设置工作目录WORKDIR设置后续所有RUN、CMD、ENTRYPOINT、COPY、ADD指令的默认工作目录。如果目录不存在Docker 会自动创建。推荐用WORKDIR而不是RUN cd /app后者在每条RUN指令之间不保留状态每条RUN都从一个新的 shell 开始而WORKDIR是持久化的。3.3 COPY —— 复制文件到镜像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 的可读性与可预测性。--chownappuser:appuser设定复制后文件的所有者为appuser用户和appuser用户组确保文件权限正确。3.4 RUN —— 执行构建命令RUNapt-getupdate\apt-getinstall-y--no-install-recommends gcc\rm-rf/var/lib/apt/lists/*RUN在镜像构建过程中执行命令通常用于安装软件、下载依赖、编译代码等。注意这里用了将多个操作串联为一条RUNapt-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。例如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 —— 设置环境变量ENVPYTHONUNBUFFERED1\PYTHONDONTWRITEBYTECODE1ENV在整个容器生命周期内有效包括后续的RUN、CMD以及容器运行时的进程。核心补充PYTHONUNBUFFERED 与容器日志的深层关系在标准 Linux 系统中当 stdout 指向终端时C 标准库glibc默认使用行缓冲line buffered——每输出一行就立即刷新但当 stdout 被重定向到管道或文件时C 标准库会自动切换为全缓冲fully buffered——必须攒够数 KB 的数据才做一次 write() 系统调用。容器环境恰好命中后一种情况Docker 通过管道捕获容器内进程的 stdout/stderrPython 进程的 stdout 在 Python 层面也是行缓冲的但如果 Python 底层调用的 C 扩展如某些数据库驱动直接向 C stdout 写入就会受到 glibc 全缓冲的影响导致日志卡住、延迟数分钟才输出。这就是不少开发者在容器中遇到docker logs迟迟看不到输出的深层原因。PYTHONUNBUFFERED1将 Python 层的 stdout 和 stderr 设为无缓冲模式——每产生一个字节就立即输出彻底根治上述缓冲延迟问题。因此业界普遍将ENV PYTHONUNBUFFERED1视为 Python 容器镜像的必备设置之一。PYTHONDONTWRITEBYTECODE1则是禁止 Python 生成.pyc字节码缓存文件。.pyc文件在本地开发中有加速第二次启动的作用但在容器场景下毫无意义——容器本身就是不可变的docker build 只执行一次运行时不会重新编译 Python 源码。禁用.pyc可减小镜像体积也让镜像层更加干净。3.8 EXPOSE —— 声明端口EXPOSE声明容器运行时监听的端口。重要提示EXPOSE本身不会自动将端口映射到宿主机端口映射需要通过docker run -p或 Compose/K8s 配置来实现。EXPOSE的真正作用更多是文档性质——告诉使用者“这个容器内的应用监听 5000 端口”。在 Kubernetes 中EXPOSE的值可以被某些工具用于自动发现端口。3.9 HEALTHCHECK —— 健康检查HEALTHCHECK--interval30s--timeout3s --start-period5s--retries3\CMDcurl-fhttp://localhost:5000/health||exit1HEALTHCHECK告诉 Docker 如何判断容器是否正常工作。Docker 会在容器内定期执行CMD中定义的命令根据退出状态码判断健康状态。参数详解--interval30s每 30 秒检查一次--timeout3s单次检查超过 3 秒视为失败--start-period5s容器启动后等待 5 秒再开始检查给应用启动时间--retries3连续失败 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把它“编译”成可运行的镜像。基本语法dockerbuild-t镜像名:标签构建上下文路径-t给镜像打标签名称 版本。构建上下文路径通常是.当前目录Docker 会将该目录下的所有文件发送给 Docker Daemon 用作构建上下文。注意构建由 Docker Daemon 执行而非 CLI因此COPY指令只能引用构建上下文内的文件不能引用上下文路径之外的文件。常用参数4.2 执行构建确认目录结构flask-redis-counter/ ├── app.py ├── requirements.txt ├── Dockerfile └── .dockerignore在项目根目录下执行dockerbuild-tflask-redis-counter:1.0.让我们逐段解读完整输出[]Building45.2s(17/17)FINISHED[internal]load build definition from Dockerfile0.0stransferring dockerfile:1.8kB0.0s[internal]load .dockerignore0.0stransferring context: 412B0.0s[internal]load metadatafordocker.io/library/2.1s python:3.12-slim第一步Docker 加载 Dockerfile 和.dockerignore然后拉取基础镜像python:3.12-slim的元数据。注意.dockerignore让上下文只有 412B——如果没写它venv/和__pycache__/全打包进去上下文可能膨胀到几百 MB。[builder1/4]FROM docker.io/library/python:3.12-12.3s slimsha256:a04c2f8c...resolve docker.io/library/python:3.12-slimsha2560.0ssha256:2d35ec5109b2...7.38MB /7.38MB2.1ssha256:8b8e7c31c414...35.5MB /35.5MB8.7sextracting3.5s第二步拉取基础镜像层。这里每一行都是一层——底层 Debian 文件系统、Python 运行时、系统库等。Extracting 是将各层解压并联合挂载。[builder2/4]WORKDIR /build0.1s[builder3/4]RUNapt-getupdate...15.2s[builder4/4]RUN pip wheel --no-cache-dir...8.3s第三步builder 阶段执行 apt-get 安装编译工具然后用pip wheel将所有 Python 依赖预编译为.whl文件。[stage-14/11]RUNgroupadd-rappuser...0.4s[stage-15/11]WORKDIR /app0.0s[stage-16/11]COPY--frombuilder /wheels /wheels0.2s[stage-17/11]COPY requirements.txt.0.1s[stage-18/11]RUN pipinstall--no-cache-dir...3.1s[stage-19/11]COPY--chownappuser:appuser..0.1s[stage-110/11]USERappuser0.0s[stage-111/11]HEALTHCHECK...0.0s第四步运行阶段的构建逐层执行——创建用户、设置工作目录、从 builder 阶段复制预编译的 wheel 包、安装依赖这一步因为使用--no-index --find-links/wheels仅从本地安装无需访问 PyPI速度极快、复制应用代码、切换用户、设置健康检查。exporting to image2.8sexporting layers2.8swriting image sha256:e8f9a0b1c2...0.0snaming to docker.io/library/flask-redis-counter:1.00.0s最后一步将所有层打包导出为一个完整镜像并打上标签flask-redis-counter:1.0。4.3 验证镜像dockerimages flask-redis-counter输出REPOSITORY TAG IMAGE ID CREATED SIZE flask-redis-counter1.0e8f9a0b1c2d310seconds ago 138MB138MB 的镜像包含了 Python 运行时、Flask、Redis 客户端以及我们的应用代码。4.4 运行容器并测试# 启动 Redisdockerrun-d--namemy-redis redis:alpine# 创建自定义网络让两个容器互访第 8 篇会深入讲网络dockernetwork create counter-netdockernetwork connect counter-net my-redis# 启动 Flask 应用加入同一网络让 Redis 主机名可解析dockerrun-d--namemy-flask--networkcounter-net-p5000:5000 flask-redis-counter:1.0# 验证curlhttp://localhost:5000输出Hello World!I have been seen1times.再执行几次Hello World!I have been seen2times. Hello World!I have been seen3times.每刷新一次计数器就加 1——Flask 通过容器网络连接 Redis 并成功读写数据。验证健康检查输出中会包含健康检查状态CONTAINER ID IMAGE STATUS PORTS a1b2c3d4e5f6 flask-redis-counter:1.0 Up2minutes(healthy)0.0.0.0:5000-5000/tcp(healthy)表示 Docker 已经执行了HEALTHCHECK中定义的命令并验证通过。验证非 root 运行dockerexecmy-flaskwhoami输出容器内的进程以appuser用户身份运行而不是 root。这降低了安全风险——如果容器内的应用被攻破攻击者只能以非特权用户身份操作无法直接获得宿主机的 root 访问权限。dockerinspect my-flask--format{{.Config.User}}输出docker inspect确认容器级别的用户设置为appuser。4.5 查看镜像分层dockerhistoryflask-redis-counter:1.0输出部分IMAGE CREATED CREATED BY SIZE e8f9a0b1c2d32minutes ago HEALTHCHECK... CMDcurl-f... 0Bmissing2minutes agoUSERappuser 0Bmissing2minutes ago COPY--chownappuser:appuser..3.5kBmissing2minutes ago RUN pipinstall...rm-rf/wheels 12MBmissing2minutes ago COPY requirements.txt.28Bmissing2minutes ago RUNgroupadd-rappuser...1.2kBmissing3weeks ago CMD[python3]0Bmissing3weeks ago ENVPYTHON_VERSION3.12.8 0Bmissing3weeks ago /bin/sh-cset-eux;...35.5MBmissing3weeks ago ADD file:...in/ 120MB第一行是我们自己构建的层越往下越接近基础镜像。最底层是 Debian 基础文件系统 Python 运行时约 120MB顶层是我们的应用代码仅 3.5kB。与早期示例中庞大臃肿的镜像形成鲜明对比——通过多阶段构建和 pip wheel 预编译构建依赖gcc、python3-dev被隔离在 builder 阶段最终镜像体积约 138MB而如果将所有构建工具打入最终镜像体积可能膨胀到 500MB 以上。五、镜像安全扫描在将镜像推送到仓库或部署到生产环境之前扫描已知漏洞是一个基本安全实践。5.1 使用 Trivy 扫描镜像Trivy 是一个开源的容器镜像漏洞扫描工具轻量且无需 Docker Desktop 即可运行# 安装 Trivy以 Ubuntu 为例sudoapt-getinstall-ywgetapt-transport-httpswget-qO- https://aquasecurity.github.io/trivy-repo/deb/public.key|sudoapt-keyadd-echodeb https://aquasecurity.github.io/trivy-repo/deb$(lsb_release-sc)main|\sudotee/etc/apt/sources.list.d/trivy.listsudoapt-getupdatesudoapt-getinstall-ytrivy# 扫描镜像trivy image flask-redis-counter:1.0输出示例2026-05-27T10:00:00.0000800 INFO Vulnerability scanning is enabled2026-05-27T10:00:01.5000800 INFO Detected OS: debian2026-05-27T10:00:02.2000800 INFO Number of language-specific files:12026-05-27T10:00:02.2000800 INFO Detecting pip vulnerabilities... flask-redis-counter:1.0(debian12.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 builderFROM ...COPY --frombuilder构建依赖与运行依赖分离镜像体积可缩小 50-70%。pip wheel 预编译构建阶段提前将所有依赖编译为.whl二进制包最终阶段仅需pip install --no-index --find-links/wheels本地安装无需访问 PyPI也无需在最终镜像中保留 gcc 等编译工具。非 root 用户运行通过RUN groupadd/useraddUSER降低安全风险。镜像安全扫描使用 Trivy 扫描已知 CVE 漏洞集成到 CI/CD 流水线中形成质量门禁。6.2 命令速查表6.3 本篇完成后的项目结构flask-redis-counter/ ├── app.py# Flask 应用主程序├── requirements.txt# Python 依赖清单flask, redis├── Dockerfile# 多阶段构建的 Docker 镜像构建文件├── .dockerignore# 构建上下文排除规则└── docker-compose.yml# 待第 11 篇编写多容器编排下一篇文章——第 5 篇Dockerfile 最佳实践与多阶段构建我们将进一步深入多阶段构建的理论与实践对比单阶段与多阶段的镜像体积差异并学习更多生产级优化技巧.dockerignore进阶、构建缓存调优、镜像标签管理等。准备好让你的镜像“瘦身”了吗想了解更多还可以去各个平台搜索「IT策士」一起升级 IT 思维