C++ 镜像制作实战
实战目的
- 编写 Dockerfile 完成一个简单 C++ 镜像
- 了解 Dockerfile 构建流程及优化思路
实战步骤
1. 创建工作目录
mkdir -p /data/myworkdir/dockerfile/cpp
cd /data/myworkdir/dockerfile/cpp
2. 创建 C++ 源代码文件 demo.c
#include <stdio.h>
int main() {
printf("hello docker!\n");
return 0;
}
3. 创建 Dockerfile
指定基础镜像
FROM centos:7
设置版本号
ENV VERSION 1.0
替换为国内源(提高下载速度)
RUN sed -e 's|^mirrorlist=|#mirrorlist=|g' \
-e 's|^#baseurl=http://mirror.centos.org/centos\|baseurl=https://mirrors.ustc.edu.cn/centos\|g' \
-i.bak /etc/yum.repos.d/CentOS-Base.repo
设置工作目录
WORKDIR /src
拷贝源文件
COPY demo.c .
安装 gcc 编译器
RUN yum makecache && yum install gcc -y
编译源文件,并清理不必要文件
RUN gcc demo.c -o demo && \
rm -f demo.c && \
yum remove -y gcc
镜像启动时运行可执行文件
CMD ["/src/demo"]
优化点说明:
- 使用
WORKDIR避免每次写完整路径 - 编译后删除源文件和 gcc,减小镜像体积
- 替换国内源,加快构建速度
4. 构建镜像
docker build -t cpp:v0.1 .
-t cpp:v0.1为镜像命名和打标签.指当前目录为上下文
5. 运行镜像验证
docker run --name cpp1 --rm cpp:v0.1
输出:
hello docker!
--rm:容器停止后自动删除--name cpp1:指定容器名称
CMD 与 ENTRYPOINT 深度实战笔记(完整版)
一、核心本质(先理解底层)
Docker 容器启动本质执行的是:
最终执行命令 = ENTRYPOINT + CMD
👉 但前提是:
- ENTRYPOINT 存在 → CMD 作为参数
- ENTRYPOINT 不存在 → CMD 作为完整命令
二、四种组合情况(必须搞清楚 ⭐)
情况1:只有 CMD
CMD ["echo", "hello"]
👉 执行:
docker run image
👉 实际:
echo hello
👉 覆盖:
docker run image echo world
👉 实际:
echo world ✅ CMD 被完全替换
情况2:只有 ENTRYPOINT
ENTRYPOINT ["echo", "hello"]
👉 执行:
docker run image
👉 实际:
echo hello
👉 覆盖:
docker run image echo world
👉 实际:
echo hello ❌ 不会变
👉 强制覆盖:
docker run --entrypoint echo image world
👉 实际:
echo world
情况3:ENTRYPOINT + CMD(最重要 ⭐⭐⭐)
ENTRYPOINT ["echo"]
CMD ["hello"]
👉 执行:
docker run image
👉 实际:
echo hello
👉 覆盖 CMD:
docker run image world
👉 实际:
echo world ✅ 只替换参数
👉 本质:
ENTRYPOINT = 程序
CMD = 参数
情况4:Shell 模式(隐藏坑)
CMD echo hello
👉 实际执行:
/bin/sh -c "echo hello"
👉 等价于:
CMD ["/bin/sh", "-c", "echo hello"]
三、Shell vs Exec 深入理解(重点⭐)
1️⃣ Shell 模式
CMD ping localhost
👉 实际运行:
/bin/sh -c ping localhost
👉 进程结构:
PID 1 -> /bin/sh
PID 2 -> ping
❌ 问题(关键点)
1. 信号丢失
docker stop container
👉 发送 SIGTERM 给 PID 1(/bin/sh)
👉 shell 不转发给 ping
👉 结果:
- ping 不退出 ❌
- 容器被强制 kill ❌
2. 优雅退出失败
- 无法触发应用的 cleanup
- 容器管理不规范
2️⃣ Exec 模式(推荐)
CMD ["ping", "localhost"]
👉 进程结构:
PID 1 -> ping
👉 优点:
- 信号直接给应用
- 可以优雅退出
- Docker 官方推荐
四、ENTRYPOINT vs CMD 深层区别
1️⃣ 设计哲学
| 指令 | 设计目的 |
|---|---|
| CMD | 默认行为 |
| ENTRYPOINT | 固定入口 |
2️⃣ 行为差异(核心)
| 行为 | CMD | ENTRYPOINT |
|---|---|---|
| docker run 覆盖 | ✅ 完全覆盖 | ❌ 仅追加参数 |
| 适合场景 | 默认参数 | 固定程序 |
| 灵活性 | 高 | 低 |
| 控制性 | 弱 | 强 |
五、组合模式(企业级用法 ⭐⭐⭐)
标准写法
ENTRYPOINT ["python"]
CMD ["app.py"]
👉 执行:
python app.py
覆盖参数:
docker run image test.py
👉 实际:
python test.py
再举一个真实例子(🔥常考)
ENTRYPOINT ["nginx", "-g"]
CMD ["daemon off;"]
👉 实际:
nginx -g "daemon off;"
六、你实验中的关键现象解释(重点理解)
1️⃣ 为什么 CMD 会被覆盖?
docker run cmd1:v0.1 echo hello bit2
👉 Docker认为:
CMD = 默认命令 → 被用户命令替换
2️⃣ 为什么 ENTRYPOINT 不会被覆盖?
docker run cmd1:v0.2 echo hello bit2
👉 Docker认为:
ENTRYPOINT = 必须执行的程序
echo hello bit2 = 参数(被忽略或追加)
3️⃣ 为什么可以用 --entrypoint 覆盖?
docker run --entrypoint /bin/sh image
👉 这是 Docker 提供的强制替换入口机制
七、最容易踩的坑(非常重要 ⚠️)
❌ 坑1:用 shell 模式
CMD python app.py
👉 问题:
- 信号丢失
- PID 不是 1
❌ 坑2:ENTRYPOINT 写死参数
ENTRYPOINT ["ping", "localhost"]
👉 问题:
- 用户无法改 host
❌ 坑3:CMD 写成完整程序(配 ENTRYPOINT 时)
ENTRYPOINT ["ping"]
CMD ["ping", "localhost"] ❌
👉 会变成:
ping ping localhost
八、最佳实践(必须记住 ⭐⭐⭐)
✅ 标准模板
ENTRYPOINT ["可执行程序"]
CMD ["默认参数"]
✅ 一定使用 Exec 格式
CMD ["node", "app.js"]
❌ 不要:
CMD node app.js
✅ 适用场景总结
| 场景 | 用法 |
|---|---|
| 可变参数程序 | ENTRYPOINT + CMD |
| 固定命令 | ENTRYPOINT |
| 简单默认命令 | CMD |
九、终极理解(面试一句话)
👉 你可以这样说(很加分):
ENTRYPOINT 用于定义容器的主程序,
CMD 用于提供默认参数。
当两者同时存在时,
CMD 会作为参数传递给 ENTRYPOINT。
为了保证信号正确传递和容器优雅退出,
推荐使用 exec 形式而不是 shell 形式。
🔥 最后一层理解(高手级)
👉 Docker 容器的本质:
容器 ≈ 一个进程(PID 1)
👉 你写 CMD / ENTRYPOINT,本质就是在决定:
谁是 PID 1
📦 Docker .dockerignore 使用笔记(详细实战版)
一、基础知识
1️⃣ 什么是 build context
Docker 是 C-S 架构:
- Client(你执行
docker build的机器) - Server(Docker daemon)
当你执行:
docker build -f Dockerfile -t test .
👉 最后的 . 就是 build context(构建上下文)
📌 本质
build context = 会被发送给 Docker Server 的所有文件
2️⃣ 存在的问题
如果当前目录很大,比如:
.
├── node_modules/
├── .git/
├── logs/
├── tmp/
├── src/
❗ 默认行为:
👉 全部发送到 Docker Server
带来问题:
- 构建慢(文件太多)
- 镜像变大
- 泄露敏感文件(.env / 密钥)
- 无用文件被 COPY 进镜像
3️⃣ .dockerignore 的作用
👉 类似 .gitignore
核心作用:
过滤 build context,不让某些文件发送给 Docker Server
✔ 优点:
- 🚀 加快 build 速度
- 📦 减小镜像体积
- 🔒 防止敏感信息泄露
- 🧹 保持镜像干净
二、基本语法
1️⃣ 文件名
必须叫:
.dockerignore
放在 build context 根目录
2️⃣ 常用规则
忽略某类文件
*.txt
👉 忽略所有 .txt
忽略目录
node_modules/
忽略多个
*.log
*.tmp
.cache/
取反(白名单)
*.txt
!important.txt
👉 忽略 txt,但保留 important.txt
忽略隐藏文件
.git
.env
三、实战演示(完整步骤)
🧪 实验目标
👉 验证 .dockerignore 会影响 COPY 结果
1️⃣ 创建 Dockerfile
使用基础镜像
FROM centos:7
拷贝当前目录所有文件到容器根目录
COPY ./* /
2️⃣ 创建 .dockerignore
*.txt
👉 表示忽略所有 .txt 文件
3️⃣ 准备测试文件
tree ./
目录结构:
.
├── 1.txt
├── 2.txt
├── 3.doc
└── dockerfile
4️⃣ 构建镜像
docker build -f dockerfile -t test_ignore:1.0 .
关键日志:
Sending build context to Docker daemon 4.096kB
👉 注意:
- 这里的 context 已经 过滤过了
.txt文件不会发送
5️⃣ 运行容器验证
docker run -it test_ignore:1.0
进入容器后:
ls /
输出:
3.doc dockerfile ...
✅ 结论
| 文件 | 是否存在 |
|---|---|
| 1.txt | ❌ 被忽略 |
| 2.txt | ❌ 被忽略 |
| 3.doc | ✅ 存在 |
👉 .dockerignore 生效 ✔
四、核心理解(重点)
❗ 关键点 1
.dockerignore 影响的是:
❌ 发送到 Docker Server 的文件
不是 COPY 本身
❗ 关键点 2
流程:
本地目录
↓(.dockerignore 过滤)
build context
↓(发送)
Docker Server
↓(COPY)
镜像
❗ 关键点 3
如果文件被 ignore:
👉 即使你写了:
COPY 1.txt /
👉 也会报错:
COPY failed: file not found
五、生产环境最佳实践 ⭐⭐⭐
✅ 1. 必加规则(通用模板)
Git
.git
.gitignore
Node
node_modules/
日志
*.log
临时文件
tmp/
*.tmp
环境变量
.env
编译产物
dist/
build/
✅ 2. Java 项目
target/
*.jar
✅ 3. Python 项目
pycache/
*.pyc
venv/
✅ 4. 前端项目
node_modules/
dist/
.cache/
六、常见坑
❌ 1. 忘写 .dockerignore
👉 导致:
- build 超慢
- 镜像巨大
❌ 2. 忽略错文件
*.conf
👉 结果:
👉 配置文件没进镜像 → 容器启动失败
❌ 3. COPY 不到文件
COPY app.jar /
但 .dockerignore 写了:
*.jar
👉 ❌ 直接构建失败
七、一句话总结
.dockerignore= 控制"哪些文件参与构建"的过滤器👉 本质是优化 build context,而不是 COPY 行为
八、进阶建议(面试加分)
你可以这样说:
👉 Docker 构建性能优化:
- 使用
.dockerignore减少 context - 分层 COPY(先依赖再源码)
- 使用缓存机制(layer cache)
🚀 Docker 多阶段构建(Multi-stage Build)笔记
一、为什么需要多阶段构建
1️⃣ 传统构建方式问题
❌ 方式一:全部写在一个 Dockerfile
编译 + 测试 + 打包 + 运行 全塞一起
👉 问题:
- Dockerfile 很长 ❌
- 镜像层数多 ❌
- 镜像体积巨大 ❌(几百 MB ~ GB)
- 部署慢 ❌
- 包含无用工具(gcc、node、maven 等)❌
❌ 方式二:多个 Dockerfile
构建阶段 Dockerfile
运行阶段 Dockerfile
👉 问题:
- 需要多个文件 ❌
- 需要脚本整合 ❌
- 维护复杂 ❌
✅ 解决方案:多阶段构建
👉 Docker 17.05+ 支持
核心思想:
一个 Dockerfile,多个阶段
👉 "构建环境"和"运行环境"分离
二、核心原理 ⭐⭐⭐
🔥 关键机制
阶段1:编译(有 gcc)
阶段2:运行(只有可执行文件)
👉 最终镜像:
❗ 只保留最后一个阶段的内容
📌 本质一句话
只把"结果"拷贝到最终镜像,而不是整个构建环境
三、实战(完整演示)
🧪 实验目标
👉 用 Docker 编译 C 程序并运行
1️⃣ 准备代码
#include <stdio.h>
int main() {
printf("hello docker!\n");
return 0;
}
保存为:
demo.c
四、❌ 单阶段构建(问题演示)
Dockerfile
FROM centos:7
WORKDIR /src
COPY demo.c .
RUN yum install -y gcc
RUN gcc demo.c -o demo
CMD ["/src/demo"]
构建 + 运行
docker build -t multi:v1.0 .
docker run --rm multi:v1.0
输出:
hello docker!
❗ 问题
docker images
👉 结果:
≈ 900MB ❌
📌 原因
- gcc 在镜像里 ❌
- yum 缓存 ❌
- 编译环境 ❌
👉 都被打包进去了!
五、✅ 多阶段构建(核心实战)
Dockerfile(两阶段)
===== 第一阶段:构建阶段 =====
FROM centos:7 AS builder
WORKDIR /src
COPY demo.c .
RUN yum install -y gcc && \
gcc demo.c -o demo
===== 第二阶段:运行阶段 =====
FROM centos:7
从第一阶段复制结果
COPY --from=builder /src/demo /src/demo
CMD ["/src/demo"]
构建 + 运行
docker build -t multi:v2.0 .
docker run --rm multi:v2.0
输出:
hello docker!
📦 镜像大小
≈ 200MB ✅
👉 已经明显变小
六、🚀 进一步优化(极致瘦身)
使用更小基础镜像(busybox)
Dockerfile(终极版)
===== 第一阶段:构建 =====
FROM centos:7 AS builder
WORKDIR /src
COPY demo.c .
RUN yum install -y gcc && \
gcc demo.c -o demo
===== 第二阶段:运行 =====
FROM busybox
COPY --from=builder /src/demo /src/demo
CMD ["/src/demo"]
构建 + 运行
docker build -t multi:v3.0 .
docker run --rm multi:v3.0
📦 镜像大小
≈ 4.8MB 🔥🔥🔥
七、为什么能变小(核心理解)
❗ 本质原因
👉 最终镜像只包含:
- 可执行文件 ✅
- 运行环境 ✅
❌ 不包含:
- gcc ❌
- yum ❌
- 源码 ❌
- 编译过程 ❌
📊 对比总结
| 内容 | 单阶段 | 多阶段 |
|---|---|---|
| gcc | ✅ | ❌ |
| 源码 | ✅ | ❌ |
| 编译工具 | ✅ | ❌ |
| 最终程序 | ✅ | ✅ |
八、核心语法总结 ⭐⭐⭐
1️⃣ 定义阶段
FROM centos:7 AS builder
👉 给阶段起名字
2️⃣ 跨阶段复制
COPY --from=builder /src/demo /src/demo
👉 从前一个阶段拷贝文件
3️⃣ 多阶段结构
FROM A AS build
...
FROM B
COPY --from=build ...
FROM centos:7 AS builder
这句:
FROM centos:7 AS builder
是多阶段构建里的核心语法,我们拆开讲👇
一、逐个拆解
1️⃣ FROM centos:7
👉 表示:
以
centos:7作为基础镜像
也就是:
- 你当前这个阶段运行在 CentOS 7 上
- 可以用
yum、gcc等工具
2️⃣ AS builder
👉 给这个阶段起一个名字
builder = 当前阶段的"别名"
二、整体含义
FROM centos:7 AS builder
👉 等价理解:
"创建一个基于 centos:7 的构建阶段,并命名为 builder"
三、为什么要取名(重点)
👉 因为后面要用到这个阶段的结果!
📌 核心用法
COPY --from=builder /src/demo /src/demo
👉 含义:
从
builder阶段中,把/src/demo拷贝到当前阶段
四、如果不写 AS 会怎样?
也可以写:
FROM centos:7
但问题:
👉 后面只能用 数字索引
COPY --from=0 /src/demo /src/demo
❌ 不推荐原因
- 可读性差
- 容易写错
- 多阶段时很混乱
✅ 推荐写法
FROM centos:7 AS builder
👉 更清晰、可维护
五、完整流程理解(非常重要)
阶段1:构建
FROM centos:7 AS builder
RUN gcc demo.c -o demo
阶段2:运行
FROM busybox
COPY --from=builder /demo /demo
Docker 镜像缓存优化笔记
基础知识
- Docker 在构建镜像时,会根据 Dockerfile 指令顺序 执行,每条指令都会生成一个 镜像层(layer)。
- 构建过程中,Docker 会检查 缓存 :
- 如果某一层镜像已经存在且未修改,则直接使用缓存。
- 一旦某一层被修改,后续层都会重新构建,无法使用缓存。
- 若不想使用缓存,可以在构建时加
--no-cache=true,但通常建议 合理利用缓存加快构建速度。
实战目的
- 学会编写 Dockerfile 以合理利用缓存,加速镜像构建。
示例:C 语言程序
1. C 源文件 demo.c
#include <stdio.h>
int main() {
printf("hello docker!\n");
return 0;
}
2. Dockerfile 初版
FROM centos:7
WORKDIR /src
COPY demo.c .
设置国内源
RUN sed -e 's|^mirrorlist=|#mirrorlist=|g' \
-e 's|^#baseurl=http://mirror.centos.org/centos\|baseurl=https://mirrors.ustc.edu.cn/centos\|g' \
-i.bak /etc/yum.repos.d/CentOS-Base.repo
RUN yum makecache
安装 gcc
RUN yum install -y gcc
编译并清理
RUN gcc demo.c -o demo && \
rm -f demo.c && \
yum erase -y gcc
CMD ["/src/demo"]
3. 构建镜像
docker image build -t cache:1.0 .
- 第一次构建没有缓存,全程约 80s。
4. 修改源文件 demo.c
#include <stdio.h>
int main() {
printf("hello bit!\n");
return 0;
}
5. 再次构建
docker image build -t cache:2.0 .
- 由于修改了
demo.c,COPY 后面的所有层都无法使用缓存。 - 构建耗时 85s,比第一次还长。
优化缓存策略
调整 Dockerfile 顺序
原则:不常修改的内容放前面,经常修改的内容放后面。
FROM centos:7
设置国内源
RUN sed -e 's|^mirrorlist=|#mirrorlist=|g' \
-e 's|^#baseurl=http://mirror.centos.org/centos\|baseurl=https://mirrors.ustc.edu.cn/centos\|g' \
-i.bak /etc/yum.repos.d/CentOS-Base.repo
RUN yum makecache
安装 gcc
RUN yum install -y gcc
WORKDIR /src
COPY demo.c .
编译并清理
RUN gcc demo.c -o demo && \
rm -f demo.c && \
yum erase -y gcc
CMD ["/src/demo"]
构建优化后的镜像
docker image build -t cache:3.0 .
- 首次构建仍无法使用缓存,但时间缩短到 37.8s。
再次修改 demo.c 并构建
docker image build -t cache:4.0 .
- 此时大部分层已使用缓存,仅重新编译源文件。
- 构建时间缩短到 2.5s,效率显著提高。
总结
- Docker 缓存原则 :
- 每条指令生成一层镜像。
- 一旦某层改变,后续层全部重建。
- 优化策略 :
- 不常改动的指令放前面(系统依赖、工具安装、国内源配置)。
- 常改动的指令放后面(COPY 源代码、编译)。
- 效果 :
- 利用缓存可将镜像构建时间从几十秒缩短到几秒。