4、镜像构建入门:docker commit创建镜像、Dockerfile基础指令(FROM、RUN、CMD)、构建上下文概念

镜像构建,说白了就是把你应用运行所需的一切——代码、依赖、配置、环境——打包成一个不可变的「快照」。我见过不少新手直接拿基础镜像跑容器,然后在里面手动装软件、改配置,最后用 docker commit 生成镜像。嗯,这确实是一条路,但走多了容易踩坑。

这一章,咱们从最原始的 docker commit 开始,再过渡到真正的工业级方案——Dockerfile。我会把我在生产环境里踩过的坑、总结的经验都抖出来。

4.1 用 docker commit 创建镜像

docker commit 的用法很简单:把一个正在运行的容器「拍个快照」,生成一个新镜像。我早期做原型验证时经常用这招,因为它快,不需要写任何配置文件。

基本命令格式:

docker commit [选项] <容器ID或名称> [仓库名:标签]

举个例子。假设你启动了一个 Ubuntu 容器,进去装了 nginx 和 vim:

# 启动容器
docker run -it --name my-ubuntu ubuntu:22.04 bash

# 在容器内安装软件
apt update && apt install -y nginx vim

# 退出容器后提交
docker commit my-ubuntu my-nginx:1.0

跑完上面三步,你就有了一个叫 my-nginx:1.0 的镜像。用 docker images 能看到它,大小大概 200MB 左右。

我曾经踩过的坑:用 docker commit 创建的镜像会包含容器运行时的所有「脏数据」——比如 apt 缓存、临时文件、历史命令记录。我有个同事提交了一个 800MB 的镜像,里面光 apt 缓存就占了 300MB。所以如果你非要用 commit,记得先清理:

apt clean
rm -rf /var/lib/apt/lists/*
history -c

为什么我不推荐在生产环境用 commit?三个原因:

  • 不可复现——你没法从镜像反推出当初装了什么、改了哪些配置。半年后想升级,只能重来一遍。
  • 体积膨胀——每次 commit 都会叠加一层,层数多了镜像会变得臃肿不堪。
  • 无法版本管理——没有像 Dockerfile 那样的「代码即配置」的追溯能力。

说白了,docker commit 适合临时调试、快速验证,但千万别把它当成正式的构建手段。

4.2 Dockerfile 基础指令:FROM、RUN、CMD

Dockerfile 才是镜像构建的正统方案。它用声明式语法描述每一步操作,可读、可复现、可版本控制。我习惯把 Dockerfile 看作「镜像的配方」,每次构建都按配方来,结果一致。

咱们先看一个最简单的 Dockerfile:

# 指定基础镜像
FROM ubuntu:22.04

# 安装软件
RUN apt update && apt install -y nginx

# 启动时执行的命令
CMD ["nginx", "-g", "daemon off;"]

这个文件只有三行,但包含了 Dockerfile 最核心的三个指令。下面我逐个拆解。

4.2.1 FROM:一切的基础

FROM 指定基础镜像。你可以把它理解成「地基」——所有后续操作都在这上面叠加。我见过有人用 FROM scratch 从零开始构建,那通常是给 Go 或 Rust 这种静态编译语言用的。大部分场景下,我们直接用官方镜像:

  • FROM ubuntu:22.04 —— 通用 Linux 环境
  • FROM node:18-alpine —— Node.js 应用,Alpine 版本更小
  • FROM python:3.11-slim —— Python 应用,slim 版本去掉了编译工具

我的建议:基础镜像选「够用就好」。别一上来就拉 ubuntu:latest(1.2GB),如果你的应用只需要 Python 运行时,python:3.11-slim(约 120MB)就足够了。镜像越小,拉取越快,攻击面也越小。

4.2.2 RUN:执行构建命令

RUN 在构建过程中执行命令,结果会持久化到镜像层里。注意,每个 RUN 都会创建一个新层。我见过有人把安装命令拆成三行:

RUN apt update
RUN apt install -y nginx
RUN apt clean

这样会生成三层,镜像体积白白变大。正确的做法是把相关命令用 && 串起来:

RUN apt update && \
    apt install -y nginx && \
    apt clean && \
    rm -rf /var/lib/apt/lists/*

这样只生成一层,而且清理了缓存,镜像更小更干净。

我曾经踩过的坑:有一次构建镜像时,RUN apt update 成功,但 apt install 因为网络超时失败了。Docker 会缓存第一层,下次构建时直接跳过 apt update,导致安装继续失败。解决方案是加 --no-cache 参数强制重新构建,或者把 update 和 install 写在同一行。

4.2.3 CMD:容器启动的默认命令

CMD 指定容器启动时执行的命令。它和 RUN 最大的区别是:RUN 在构建时执行,CMD 在运行时执行。

CMD 有三种写法:

写法 示例 说明
Shell 格式 CMD nginx -g "daemon off;" 通过 /bin/sh -c 执行,支持变量替换
Exec 格式(推荐) CMD ["nginx", "-g", "daemon off;"] 直接执行,不经过 shell,信号处理更干净
参数格式 CMD ["-g", "daemon off;"] 配合 ENTRYPOINT 使用,作为默认参数

我个人习惯用 Exec 格式。为什么?因为 Shell 格式会启动一个 sh 进程,当你用 docker stop 时,信号先发给 sh,它可能不会正确转发给实际进程,导致容器无法优雅退出。Exec 格式直接启动目标进程,信号传递更可靠。

4.3 构建上下文概念

这是新手最容易懵的地方。构建上下文,就是执行 docker build 时指定的那个目录。Docker 会把整个目录打包发送给 Docker daemon,然后 daemon 才能访问里面的文件。

看个例子:

# 当前目录结构
my-app/
├── Dockerfile
├── app.py
├── requirements.txt
└── config/
    └── settings.yaml

# 构建命令
docker build -t my-app:1.0 .

这里的 . 就是构建上下文。Docker 会把 my-app/ 目录下的所有文件都发送给 daemon。然后 Dockerfile 里的 COPY 指令只能引用上下文内的文件:

FROM python:3.11-slim
WORKDIR /app
COPY app.py .
COPY requirements.txt .
COPY config/ config/
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

关键点:构建上下文不是 Dockerfile 所在的目录,而是你执行 docker build 时指定的路径。如果你写 docker build -t my-app:1.0 /some/other/dir,那上下文就是 /some/other/dir。

为什么要理解这个概念?因为上下文的大小直接影响构建速度。我见过有人把整个家目录作为上下文——里面有几个 GB 的电影、照片、代码仓库。Docker 每次构建都要打包发送这几 GB 的数据,构建一次要等好几分钟。

我的建议:用 .dockerignore 文件排除不需要的文件。它的语法和 .gitignore 一样:

# .dockerignore
.git/
node_modules/
*.log
__pycache__/
*.md
dist/

这样 Docker 在打包上下文时会自动跳过这些文件,构建速度能快不少。

4.4 本章知识体系

下面这张图把镜像构建的两种方式、核心指令和上下文的关系串起来了。你可以把它当作本章的「思维导图」:

镜像构建知识体系 镜像构建知识体系 镜像构建 方式一:docker commit 基于运行中的容器创建镜像 适合快速验证 / 临时调试 不可复现 / 体积膨胀 / 不推荐生产 方式二:Dockerfile 声明式语法 / 可复现 / 可版本控制 FROM:指定基础镜像 RUN:构建时执行命令 CMD:运行时默认命令 构建上下文(docker build 指定目录) .dockerignore 控制上下文大小

4.5 小结

这一章咱们走了两条路:一条是 docker commit 的「野路子」,适合临时救急;另一条是 Dockerfile 的「正规军」,适合生产环境。记住三个核心指令 FROM、RUN、CMD 的用法和区别,再理解构建上下文的概念,你就能写出干净、高效的 Dockerfile 了。