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 本章知识体系
下面这张图把镜像构建的两种方式、核心指令和上下文的关系串起来了。你可以把它当作本章的「思维导图」:
4.5 小结
这一章咱们走了两条路:一条是 docker commit 的「野路子」,适合临时救急;另一条是 Dockerfile 的「正规军」,适合生产环境。记住三个核心指令 FROM、RUN、CMD 的用法和区别,再理解构建上下文的概念,你就能写出干净、高效的 Dockerfile 了。