← 全部笔记

Docker · 2 镜像与 Dockerfile

写好文件与配置,再启动 Web 应用

Dockerfile 是构建镜像的文本说明:RUN 在构建时执行;应用在容器启动后运行;EXPOSE 只声明端口。[1]

约 15 分钟 · 截至 2026-11-03,Docker Engine 29.x · Docker 实验未实测

构建镜像和运行应用是两个时刻 Dockerfile 和源码通过 docker build 变成镜像,镜像包含文件与配置。docker run 再从镜像启动 Web 应用;发布宿主机端口需要额外的 -p 参数。 Dockerfile + 源码构建输入 build 镜像文件 + 配置 run 容器:Web 应用监听 8000 本机 :8080 用 -p 发布
图 1 构建得到文件与配置,运行才启动应用;声明端口不会自动建立本机访问入口。[4]

前置复习:镜像、容器与仓库 · 第一次 docker run。本页不要求你会 Python,应用代码可以直接复制。

先看完整配方,再逐行构建

六条指令,各有职责

构建(build)把源码和指令变成镜像。下面用 Python 自带的 HTTP 库提供一个网页响应,不需要安装额外依赖。镜像名中的 3.13-slim 是标签,表示选用 Python 3.13 的精简变体;标签内容仍可能更新。[7]

FROM python:3.13-slimWORKDIR /appRUN printf 'built during build\n' > build.txtCOPY app.py ./ENV GREETING="Hello from Docker"EXPOSE 8000CMD ["python", "app.py"]
指令逐步增加文件和配置 底部是 FROM 继承的基础镜像,向上依次是 WORKDIR 创建的目录、RUN 写入的文件、COPY 复制的源码;顶部独立展示工作目录、环境变量、端口声明与启动命令。 镜像配置(元数据) 工作目录 /app GREETING = Hello from Docker 声明端口 8000/tcp 启动命令 python app.py 文件系统:新变化叠在上面 COPY → /app/app.py RUN → /app/build.txt WORKDIR → 缺失时创建 /app/ FROM → 继承 Python 基础镜像
  1. FROM 选择起点。复用基础镜像的文件层和配置,其中已有 Python。它开始一个构建阶段,即从这份基础镜像继续加工的过程。先猜:下一步如何让相对路径都从 /app 开始?
  2. WORKDIR 设置工作目录。后续 RUN 和相对目标路径以 /app 为起点;目录缺失时会创建。运行容器时也沿用这个工作目录。
  3. RUN 执行构建命令。此处用 shell 的重定向写出 /app/build.txt。保存的是文件变化,写文件的进程不会留在镜像里持续运行。
  4. COPY 加入源码。把构建输入里的 app.py 复制到镜像的 /app/app.py。源码到位,但 Web 服务还没启动。先猜:设置默认问候语,需要再写一个文件吗?
  5. ENV 保存默认环境变量。GREETING 是进程可以读取的键值配置;后续构建步骤与容器中的进程都能使用。此行本身不写应用文件。
  6. EXPOSE 声明应用端口。记录预期使用 8000/tcp,省略协议时默认 TCP。它不会启动服务,也不会发布宿主机端口。
  7. CMD 补上启动命令。这里只把它当作“容器启动时默认执行 python app.py”的配置。应用要到 docker run 时才启动;与 ENTRYPOINT 的区别留到下一篇。
图 2 Dockerfile 同时加工文件和配置;此图按本例的文件变化示意,不表示层的字节大小或精确数量。[1] [3]

把最后的 CMD 换成 RUN python app.py,会得到“启动后自动提供网页”的镜像吗?写下构建时和运行时分别会发生什么。

不会。RUN 会在构建阶段启动这个长期运行的服务,构建通常一直等它退出;即使服务后来退出,也不会把活着的进程存进镜像。CMD 保存启动意图,在创建并启动容器时执行。

分清源码位置与镜像内位置

COPY 的两端不同

COPY app.py ./ 的左边相对于构建上下文(build context),即这次允许构建器读取的输入文件集合。右边是镜像内的位置;./ 相对于当前 WORKDIR。[2] [1]

COPY 从上下文复制到镜像内的工作目录 docker build 命令末尾的点指定当前目录为上下文;其中的 app.py 被复制到镜像的 /app/app.py,目标由 WORKDIR /app 与相对路径 ./ 共同决定。 构建上下文:当前目录 Dockerfileapp.py 镜像内:/app/ build.txtapp.py 复制 docker build -t df-web:lab . 末尾的点选上下文;WORKDIR 选目标起点
图 3 同一个文件名,左边属于构建输入,右边属于镜像内的文件系统。

源文件不会因为 WORKDIR 改成 /app 就去宿主机的 /app 查找。目标文件也不会自动同步后续的源码修改;COPY 保存的是构建时复制的内容。

改成 WORKDIR /srv/web 后,再执行 COPY app.py ./,镜像内文件的绝对路径是什么?

相对目标 ./ 从工作目录 /srv/web 开始,所以得到 /srv/web/app.py。构建上下文中的源文件位置不变。

深入一点:RUN cd /app 能代替 WORKDIR 吗?

不能。RUN cd /app 只改变这一次 shell 进程的目录;下一条 RUN 不继承这个进程状态。WORKDIR /app 把工作目录写入配置,后续步骤和启动命令才会沿用。用绝对路径也能避免受基础镜像原有工作目录影响。[1]

源码明明存在,COPY app.py ./ 却提示找不到。哪一步最值得先检查?

端口声明不参与源码查找,无法修复 COPY 的输入。

WORKDIR 设置镜像内路径,不改变 COPY 的输入范围。

COPY 只能读取允许的构建输入。.dockerignore 是上下文根目录中列出排除规则的文件;被排除的文件不会提供给这次构建。先确认你把正确目录作为上下文,再检查排除规则。[2]

不要把历史记录数当作文件层数

文件层与配置分开看

文件层记录文件系统的新增、修改或删除。镜像配置记录工作目录、环境变量等默认值;这里的“元数据”指这些描述信息。两者合起来,才决定容器看到什么、怎样启动。[3]

指令文件系统配置或作用
FROM继承基础镜像的已有层开始新的构建阶段,继承基础配置
RUN本例新增 build.txt构建时执行命令
COPY本例新增 app.py复制构建输入
WORKDIR目录不存在时创建它保存工作目录
ENV此行本身不修改文件保存环境变量默认值
EXPOSE此行本身不修改文件保存端口声明

回看图 2:文件变化从下往上叠,配置另列在上方。ENV 与 EXPOSE 不增加文件系统差异层;WORKDIR 要考虑目录创建。即使 RUN 只执行 pwd,也不能因为有一条指令就断言它增加了有内容的文件层。[1]

按本例指令“自身的直接作用”分类。假设 /app 尚不存在;不把后续程序的行为算进来。

  • RUN printf … > build.txt
  • COPY app.py ./
  • ENV GREETING="Hello from Docker"
  • EXPOSE 8000
  • WORKDIR /app(目录缺失)
改文件
只改配置
两者都改

判断对象是“这条指令做了什么”。ENV 自身只写配置;后续 RUN 若读取这个变量再写文件,那是 RUN 的文件变化。WORKDIR 则同时具有目录设置与按需创建行为。

深入一点:怎么验证,而不猜层数?

docker image history --no-trunc df-web:lab 显示构建历史,可能包含只改配置的记录。docker image inspect 可查看镜像的 RootFS.Layers 文件层列表与 Config 配置;不要把历史行数直接当作文件层数。具体层数和大小以你的构建结果为准,本页未实测,也不给虚构数字。[5] [9]

ENV 也不是保存密码的位置:它会留在镜像配置中。运行时可用 docker run -e GREETING=Hi … 给这一次容器覆盖默认值,而不重新构建镜像。[6]

用自己的话解释:用 -e GREETING=Hi 启动新容器,为什么不用重新 COPY app.py?

源码在文件层里,问候语由程序运行时读取环境变量决定。改变新容器的环境就能改变响应,不必改源码。

  • 区分源码文件和环境配置
  • 提到应用需要主动读取环境变量
  • 覆盖只影响这次新建容器,镜像默认值仍保留

三件独立的事:声明、监听、发布

EXPOSE 只作声明

声明由 EXPOSE 记录;监听由应用进程完成;发布由运行时的 -p 建立宿主机端口入口。任意一步都不能替代另外两步。[4]

仅声明端口,没有宿主机发布入口镜像里写了 EXPOSE 8000。容器内应用监听 8000,但宿主机的 8080 没有发布规则,不会因此转发给它。 未加 -p配置:EXPOSE 8000 容器内应用:监听 8000仍然可以在容器内提供服务 宿主机 :8080 × 没有发布入口
图 4 只声明端口,不会让宿主机的 8080 自动转发给容器。
发布把宿主机 8080 接到容器 8000-p 127.0.0.1:8080:8000 将本机回环地址的 8080 转发到容器的 8000;应用应监听容器内的 0.0.0.0:8000。 加 -p 127.0.0.1:8080:8000本机请求 127.0.0.1:8080 转发到 8000 容器内应用:监听0.0.0.0:8000
图 5 发布入口和应用监听端口对上,宿主机请求才能进入这个服务。

-p 127.0.0.1:8080:8000 的顺序是“宿主机 IP : 宿主机端口 : 容器端口”。即使 Dockerfile 删掉 EXPOSE,这条显式发布仍可使用。它也不会替应用启动监听。[4] [1]

保留 EXPOSE 8000,把应用改为监听 9000,运行时仍用 -p 127.0.0.1:8080:8000。访问本机 8080 会进入这个应用吗?怎样修正?

不会。发布的目标是容器 8000,但服务在 9000。应改为 -p 127.0.0.1:8080:9000,并把 EXPOSE 更新为 9000 以准确描述镜像。真正决定服务入口的是应用的监听和发布目标。

深入一点:没发布,就一定无法从容器外访问吗?

不能这样推断。同一容器网络中的其他容器可以按容器 IP 和端口通信;Linux 宿主机也可能直接访问桥接网络里的容器 IP。“没有 -p”只表示没有这个宿主机发布入口,不是访问控制规则。-P(大写)则会根据声明的端口自动分配宿主机端口;它和小写 -p 的显式映射不同。网络路径留到模块 5。[1] [4]

在你自己的 Docker 机器上验证

构建一次,验证三件事

Docker 未实测

编写环境没有 Docker,执行 docker version 返回 docker: command not found。下面的 Docker 命令未实测;“预期”是根据文档和示例代码推导的检查点,不是本机终端记录。请维护者补实测输出。示例应用已用本机 Python 单独验证两种问候语的 HTTP 响应,这不等于容器实测。

准备:已启动的 Docker(Linux 容器模式)、能连接它的终端、拉取基础镜像所需网络。以下宿主机命令使用 Bash;无需本机安装 Python。Web 示例只供学习,不能直接用于生产。[8]

宿主机影响:会创建一个实验目录,下载并构建镜像、创建容器网络;发布端口时 Docker 会配置转发规则(Linux 上可能涉及防火墙规则)。端口仅绑定本机 127.0.0.1,不需要特权模式或宿主机目录挂载。若 Docker 连接远程机器,下面的“本机访问”需在 Docker 服务端机器上进行。

1. 准备两个文件

在新终端创建一个未占用的目录。把图 2 中的完整七行保存为当前目录下的 Dockerfile(无扩展名),再创建下面的 app.py。[10]

mkdir dockerfile-basics-lab
cd dockerfile-basics-lab
import os
from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        body = (os.environ["GREETING"] + "\n").encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()

应用从环境里读取 GREETING,对 HTTP GET 请求返回这段文本。0.0.0.0 表示监听容器内全部 IPv4 接口;若只监听容器自己的 127.0.0.1,发布进来的流量到不了这个监听。[8]

# 在包含 Dockerfile 和 app.py 的宿主机目录执行
docker version
docker build -t df-web:lab .
docker run --rm df-web:lab sh -c 'pwd; cat build.txt; printenv GREETING; test -f app.py && echo app.py-exists'

-t df-web:lab 为镜像指定名称与标签;末尾 . 选择当前目录为构建上下文。这里在镜像名后指定 sh -c …,临时替换默认 CMD 以检查文件;--rm 在容器退出后自动删除容器,保留镜像。[2] [6]

预期(未实测):检查命令依次打印工作目录 /app、文件内容 built during build、环境值 Hello from Docker,以及文件存在标记 app.py-exists。镜像 ID、构建日志和大小随环境变化,此处不预填输出。

2. 比较声明与发布

docker run --rm --name df-web-lab df-web:lab

这个终端留给前台服务。在同一台宿主机另开终端执行:

docker port df-web-lab

预期(未实测):没有发布映射可列出。不要仅凭访问 8080 失败来判断:那个端口可能已被别的程序占用。回到第一个终端按 Ctrl + C 停止服务;容器自动删除。

docker run --rm --name df-web-lab -p 127.0.0.1:8080:8000 df-web:lab
# 第二个终端执行以下命令;或用浏览器打开这个地址
docker port df-web-lab
curl http://127.0.0.1:8080/

预期(未实测):端口检查应出现容器 8000/tcp 到本机 127.0.0.1:8080 的映射;请求正文应为 Hello from Docker 并换行。若宿主机 8080 已占用,把 -p 的中间数字和访问地址一起改为 8081。[11]

3. 覆盖配置,不重建

停止当前服务后,给新容器加 -e GREETING=Hi,不运行 build。你预计网页文字和 build.txt 分别会怎样变化?

网页变成 Hi;build.txt 仍是构建时写入的内容。环境覆盖只改变这次容器运行的配置,不会重新执行 RUN 或修改镜像。

先在第一个终端按 Ctrl + C 停止旧服务,再启动:

docker run --rm --name df-web-lab -e GREETING=Hi -p 127.0.0.1:8080:8000 df-web:lab
# 第二个终端执行
curl http://127.0.0.1:8080/
docker image inspect df-web:lab --format '{{json .Config}}'
docker image inspect df-web:lab --format '{{json .RootFS.Layers}}'
docker image history --no-trunc df-web:lab

预期(未实测):请求正文变成 Hi 并换行;镜像 Config 中仍保留 GREETING=Hello from Docker、工作目录 /app 和端口声明。层列表与构建历史用于核对结构,不要求匹配固定的层数或大小。

结束后按 Ctrl + C;若没有回到运行终端,可在另一终端执行 docker stop df-web-lab。确认只删除本实验镜像时,运行 docker image rm df-web:lab。实验目录和基础镜像保留,按需自行清理。

合上示例,再检索一次

把三个时刻分清

镜像声明 EXPOSE 8000;应用监听 0.0.0.0:8000。想从本机 8080 访问,并把问候语改成 Hi,哪条命令合适?

环境变量覆盖了,但没有发布宿主机 8080;EXPOSE 不会替你补上。

端口方向反了:最后一个数字必须指向应用监听的容器 8000。

命令 c 把本机 8080 接到容器 8000,同时用 -e 给新容器设置 GREETING。源码与镜像都不需要修改;程序启动后读到新值,再返回 Hi。

来源

核查日期:2026-11-03。Docker 行为以官方文档为准,版本范围为 Docker Engine 29.x;页面图为示意,Docker 实验未实测。

  1. Dockerfile reference:FROM、RUN、COPY、WORKDIR、ENV、EXPOSE、CMD 的语义,尤其是 WORKDIR 的按需创建行为。
  2. Build context:构建输入、命令末尾的路径与 .dockerignore。
  3. Understanding the image layers:层记录文件系统变化,基础镜像层可复用。
  4. Publishing and exposing ports:声明、显式发布、回环地址绑定与 -P。
  5. docker image inspect:检查镜像详情和格式化输出。
  6. docker container run:--rm、-e、-p 与临时指定启动命令。
  7. Python 官方镜像:基础镜像和 3.13-slim 标签。
  8. Python 3.13 http.server、os.environ:示例 HTTP 服务与环境变量读取;该 HTTP 服务不用于生产。
  9. docker image history:查看镜像构建历史。
  10. Dockerfile overview:文件名、构建流程与启动命令。
  11. docker container port:查看容器的实际端口映射。