一个追问者的docker学习笔记
文章目录
- 一个追问者的docker学习笔记
- 背景
- deepseek的阅读建议
- 对ai的追问-docker
- 1. 为什么要用docker进行部署?
- 2. docker里的挂载卷
- 3. 是否可以将不同项目的可执行程序和依赖都打包到同一个镜像中,只是用不同的容器来区分不同的项目程序
- 4. 在项目所在目录构建的docker会存储在哪个路径
- 5. 如何找到并修改容器内的文件
- 6. 不同版本的镜像可以同时运行吗
- 7. 容器和宿主机上的端口
- 8. 开源项目一般会将源码和编译环境都打包进镜像里吗,还是只打包可执行程序呢
- 9. 容器的持久化
- 10. CI/CD 流程
- 11. Dockerfile文件名后面加 .houzui 形式和直接的Dockerfile有什么区别
- 12. ENTRYPOINT 的作用和执行时机
- 13. 通过 docker build 构建了一个名字和标签都一样的 镜像,是否会覆盖旧镜像
- 14. 指令参数含义
- 15. docker重启策略
- 16. 查询已经dangling的镜像是否还有容器在运行
- 17. docker-compose
- 18. dockerfile
- 19. 使用vscode查看容器里的代码
- 20. 使用建议
- docker常用指令记录
- 为什么记录这篇博客
一个追问者的docker学习笔记
背景
-
以前项目中有使用过docker,那会只从B站上看了几个入门的视频,当时大约是一知半解吧,工作中用到docker的场景并不多,只知道按照文档执行那几个常用的指令。最近需要接手一个同事的项目,这个项目是使用docker进行测试和部署的,项目附带有很详尽的文档尤其是关于docker的,由于工作中没怎么用到docker,所以我对于相关的知识很多都忘了。
-
我的阅读习惯经常是从头看到尾,从第一个字到最后一个字,所以速度很慢并且有时读着读着容易走神。于是我把文档复制给了deepseek,并请其帮助梳理文档脉络给出阅读建议。
-
我发现按照deepseek给出的顺序,依照dockerfile进行镜像的构建中,我就自然而然发现了很多疑问,譬如说为什么需要用docker进行部署、某个参数是什么含义,关于镜像和容器的一些个人理解、挂载卷的持久化…等等。在提问和ai耐心的回答中,我发现了一种久违的兴奋感,这种了解到新的知识并且可以立刻实践和应用,在遇到不懂的地方,还有ai可以非常耐心仔细的回答。我喜欢这种感觉,并且看到了很晚也丝毫不觉得疲惫,这与从前的那种迷茫颓唐不知道要做什么的感觉相去甚远,学习到新的知识真的太好了有ai也真的太好了:D
deepseek的阅读建议
1.理解docker核心概念
- 镜像(操作系统+软件模板)、容器(镜像运行实例)、卷(容器内容映射到宿主机)
2. 通过文档中的示例把项目跑起来(构建-运行-验证)
-
docker build 构建镜像
-
docker run 运行容器
-
docker ps 查看容器状态
3. 深入理解每一步
-
理解Dockerfile
-
理解docker指令中的参数含义
-
-v挂载的作用
-
-p端口映射
-
4. 学习日常管理和调试
-
阅读 常用管理命令
-
docker logs -f <容器名>:实时查看日志(监控服务输出)
-
docker exec -it <容器名> /bin/bash:进入容器内部调试
-
docker stop/start/restart:控制容器启停
-
docker stats:查看资源占用
-
5. 掌握分发部署
-
导出和分发
- 将镜像打包成tar
-
新服务器部署
6. 故障排查与优化
-
容器无法启动
-
服务无响应
-
限制容器资源、提升安全性
7. 使用脚本自动化
- 构建/运行/导出 .sh脚本
对ai的追问-docker
1. 为什么要用docker进行部署?
-
为什么需要使用docker进行部署呢,直接将程序文件和so文件复制到需要部署的服务器可以吗?
-
工作前两年接触的项目大多是CS架构的,大部分是通过QT/C#等上位机软件来调用图像算法的。有时算法部分是直接和QT一起开发的没有分离,有时是将算法部分作为一个模块,提供头文件和dll库文件。
-
后来项目慢慢变成了BS架构,那个时候大多在window上开发/少部分在Linux上开发。最开始使用的通讯架构是SOCKET、GRPC,后来项目中用到websocket、HTTP等。基本上也只需要将编译好的 可执行程序和依赖的库文件 部署到有限的几台客户的工控机上,并不需要批量的部署,所以一般是在客户的机器上装好CUDA环境,然后把可执行文件夹(含可执行程序、依赖库、配置文件、日志等)复制过去再设置开机自启就行。
-
1.1 环境一致,避免开发环境能跑、客户环境不能跑
-
程序依赖问题
-
操作系统内核、glib版本
-
运行时链接的路径
-
编译器的ABI兼容性
-
依赖库第三方库版本
-
-
Docker将整个操作系统层和所有依赖库都打包成一个镜像,任何安装了docker的电脑,都能复现开发环境,彻底消除环境差异
1.2 依赖管理,避免手动收集复制依赖库
-
如果要手动复制依赖库,需要
-
找出开发环境依赖的所有库文件(Linux下是so,window下是dll)
- 包括开发中使用到的第三方库 和 系统相关的库 等
-
若linux下用ldd检查可执行文件、逐个收集
-
将可执行文件、依赖库 复制到目标机器,并设置正确的LD_LIBRARY_PATH
- LD_LIBRARY_PATH (指导动态链接器在程序运行时去哪些目录查找所需的共享库(.so 文件)。它允许用户在默认系统路径(如 /lib、/usr/lib)之外,定义优先的自定义库搜索路径,常用于运行非标准位置安装的软件或解决库版本冲突问题)
-
确认目标机器上的内核和glibc版本兼容
-
-
Dockerfile中的指令会自动下载并安装所有依赖,镜像构建完成后,所有依赖都固化在镜像里。部署只需要传输一个镜像文件,无需关系依赖细节。
1.3 应用隔离,避免项目间依赖库版本不兼容
-
如果服务器还运行其他服务,直接将软件依赖的.so复制到系统目录,可能
-
其他服务也用到这个so库并因版本不兼容而崩溃
-
新部署的程序因系统中已存在其他版本的库而链接错误
-
-
Docker中,每个容器都有独立的文件系统、进程空间和网络栈;程序再容器里的`/usr/lib`完全是镜像里的内容与宿主机隔离
1.4 简化部署,一行命令安装
-
无docker的部署流程
-
- 检查操作系统版本确认兼容性
-
- 安装依赖:apt-get install …
-
- 拷贝程序文件
-
- 设置环境变量
-
- 配置开机启动
-
- 编写systemed service文件
-
-
docker部署方法
-
docker save -o myapp.tar myapp:latest
-
docker load -i myapp.tar
-
docker run -d --name myapp -p 11451:11451 --restart unless-stopped myapp:latest
- –restart unless-stopped,容器就能随 Docker 自启
-
1.5 版本控制与回滚
- 每个镜像每个镜像都有唯一的 tag(如 :latest, :v1.0.0),可以轻松地在不同版本间切换:
- 回滚则是 停止当前容器,用旧镜像启动新容器
docker run myapp:v1.0.0 # 旧版本
docker run myapp:v2.0.0 # 新版本
1.6 资源控制与监控
- Docker 允许限制容器的 CPU、内存、磁盘 IO,防止某个服务耗尽主机资源,在多服务共存的机器上非常实用,而直接运行进程难以精确控制
docker run --cpus="2" --memory="4g" myapp:latest
1.7 可移植性与云原生
- Docker 镜像是 OCI 标准,可以在任何支持 Docker 的环境运行:笔记本、测试服务器、生产服务器,甚至 Kubernetes 集群。这为未来的微服务化、弹性伸缩铺平了道路。
1.8 安全性与维护
- 最小权限:可以限制容器只读文件系统、禁止特权提升。
- 快速销毁重建:容器出问题了直接删除重建,比排查宿主机上散落的文件更安全。
- 集中式日志:docker logs 统一收集 stdout/stderr,方便对接日志系统。
1.9 什么时候 直接复制文件 可行
- 简单场景
- 程序是静态编译(如 Go 语言),无动态依赖
- 目标机器环境与开发机完全一致(相同发行版、相同库版本)
- 程序没有复杂的运行时依赖
- 只需要部署到一台服务器,且不常更新
2. docker里的挂载卷
- “挂载卷”还是“卷挂载”,两者是同一个意思,都是指 Docker 中的 volume(存储卷)或 bind mount(绑定挂载)
- 挂载卷更常用,强调“把宿主机目录或 Docker 管理的卷挂载到容器中”
2.1 docker挂载卷 中 宿主机路径 和 docker里的路径关系
- 它是映射,不是复制:容器内对 /app/model 的任何读写操作,都会直接反映到宿主机的 $(pwd)/model 目录中,反之亦然。两者共享同一份数据。
2.2 如果容器镜像中需挂载目录下原本有文件会怎样,挂载是否会覆盖镜像内的文件
- 挂载宿主机的空目录后,容器内将看不到原来的文件(因为宿主机目录覆盖了镜像层的文件)。这称为“挂载隐藏”
- 挂载不会修改镜像本身,只是在容器运行时覆盖了该路径的视图。镜像的原始文件仍然存在于镜像层中,只是被宿主机的目录遮挡了。如果运行容器时不挂载该目录,容器内会看到镜像自带的文件。
2.3 不同镜像间使用同一个宿主机挂载卷会冲突吗
- 如果两个不同的容器(无论基于相同镜像还是不同镜像)都挂载了同一个宿主机目录到容器内的路径(比如都挂载 /host/model 到 /app/model),那么它们会共享该目录
- 一个容器写入的文件,另一个容器能立即看到
- 如果两个容器同时写入同一个文件,可能会导致数据损坏或竞争条件,取决于应用是否支持并发
- 不是 Docker 层面的冲突,而是应用层面的共享数据问题。如果希望两个容器独立运行,就不应该挂载相同的宿主机目录
2.4 如果将整个项目包括源码都挂载到宿主机的一个路径下,在宿主机重新编译项目会怎样
- 如果运行时没有挂载源码目录
- 本地修改源码 → 容器内的源码不会改变,因为容器使用的是镜像内的旧版本。
- 如果运行时通过卷挂载将本地源码目录覆盖容器内的路径
- 容器内的 /app 不再使用镜像中的文件,而是直接映射到本地当前目录。
- 本地修改源码 → 容器内立即看到修改(因为是实时映射)。
- 镜像中的原始文件被“隐藏”了,但镜像本身未变;容器看到的是本地目录的内容。
- 如果挂载了源码并在宿主机重新编译,容器内的可执行文件会更新吗
- 会更新文件,但运行中的进程不会自动使用新文件
- 容器内正在运行的 server 进程仍然使用旧的可执行文件(因为程序启动时已将文件内容加载到内存,并且文件 inode 仍然指向旧文件,即使路径被覆盖,原进程依然保持对旧文件的引用)。
- inode(索引节点)是 Linux 文件系统中的一个核心概念。每个文件(或目录)都有一个对应的 inode,它存储了文件的元信息(除文件名以外的所有属性)
- 要让新程序生效,需要重启进程或容器
2.5 如果进入容器内修改挂载卷里的内容,宿主机里对应的挂载卷内容会改变吗
- 会同步修改。因为挂载卷(bind mount)是双向实时同步的:无论是宿主机上的修改,还是容器内的修改,都会立即反映到另一端。
3. 是否可以将不同项目的可执行程序和依赖都打包到同一个镜像中,只是用不同的容器来区分不同的项目程序
3.1 共用镜像技术上可行,但在实际工程中通常不是推荐的做法
- 单一职责原则。每个镜像应该只做一件事。这是 Docker 和微服务架构的核心原则。如果混在一起,镜像体积会变得巨大,包含很多不必要的文件,拉取和更新都变慢。
- 维护与版本管理困难。假设更新了项目 A 的代码,但项目 B 没变。如果它们在同一镜像中,不得不重新构建整个镜像,导致项目 B 的层也被重新打包(即使没变),浪费时间和存储。镜像的标签(tag)很难同时反映两个项目的版本。
- 启动复杂化。从同一个镜像启动不同项目的容器,需要通过不同的启动命令(覆盖 CMD)或环境变量来指定运行哪个程序。
- 依赖冲突风险。不同项目可能依赖同一库的不同版本,如果在同一个镜像中安装,可能会发生冲突。
- 如果某个项目存在漏洞,整个镜像都会受影响。分开镜像可以限制漏洞的扩散范围。
3.2 什么时候可以考虑“多项目合一”
- 极简环境:比如嵌入式设备存储空间极其有限,只能容纳一个镜像,且各项目都是小型工具,可以共用一个基础系统。
- 一次性测试:临时将多个工具打包在一起方便快速分发,但不用于生产。
- 即便如此,更常见的做法是使用多阶段构建或基础镜像来复用公共层,而不是将最终的应用混合。
3.3 如何复用公共依赖
- 如果多个项目确实依赖大量相同的底层库,可以创建一个基础镜像,包含这些公共依赖。
- 然后基于这个基础镜像,为每个项目构建自己的镜像,只添加项目特定的代码。
4. 在项目所在目录构建的docker会存储在哪个路径
- Linux下:Docker 默认会将镜像、容器、卷等数据存储在 /var/lib/docker 目录下,这通常位于系统的根分区(/)
docker info | grep "Docker Root Dir"
Docker Root Dir: /var/lib/docker
-
windows下:
- 未挂载的容器内文件 : \wsl$\docker-desktop-data\version-pack-data\community\docker\
- 容器的可写层。如果容器被删除且没有提交为新镜像,这些更改会永久丢失
- 匿名卷: C:\ProgramData\docker\volumes<长哈希字符串>_data
- 没有挂载目录,但容器内进程将数据写入特定路径时,Docker 自动创建的卷。生命周期独立于容器,但路径是随机哈希,难以直接管理
- 未挂载的容器内文件 : \wsl$\docker-desktop-data\version-pack-data\community\docker\
-
可更改 Docker 数据存储路径到挂载网盘(如果根目录空间不够的话)
# 停止 Docker
sudo systemctl stop docker
# 创建新目录(在网盘上)
sudo mkdir -p /media/network-disk/docker
# 迁移现有数据(可选,但建议)
sudo mv /var/lib/docker/* /media/network-disk/docker/
# 配置 Docker 使用新路径(编辑(或创建)Docker 配置文件)
sudo nano /etc/docker/daemon.json
# daemon.json里添加或修改如下内容
{
"data-root": "/media/network-disk/docker"
}
# 启动 Docker 服务
sudo systemctl start docker
# 验证新路径是否生效
docker info | grep "Docker Root Dir"
- 如果上述步骤中没有迁移旧数据,Docker 会使用新的空目录作为数据根,原先的镜像和容器(在 /var/lib/docker)将不可见,但它们仍然占用根分区空间。
5. 如何找到并修改容器内的文件
5.1 查看容器内的文件
- 进入容器内部查看
docker exec -it <容器名> /bin/bash
ls -l /app/model
- 在宿主机上用docker run 临时查看(不启动服务),–rm会在命令执行后自动删除容器。
docker run --rm <镜像名> ls -l /app/model
5.2 修改容器内的文件
- 进入容器修改(临时,重启或删除后丢失)
docker exec -it <容器名> /bin/bash
vi /app/model/somefile.txt # 修改文件
exit
- 将修改提交为新镜像
docker commit <容器名> myimage:newtag
- 使用卷(Volume)挂载,这样容器内的 /app/model 就指向了宿主机目录,你在宿主机上修改文件,容器内立即生效,并且数据持久化
docker run -v /宿主机/绝对路径/model:/app/model ...
6. 不同版本的镜像可以同时运行吗
- 不同tag版本通常不是同一个镜像,除非你特意将两个标签指向同一个镜像 ID(这种情况很少见)。更常见的是,它们是两个不同的镜像(不同版本)
- 可以同时运行,因为每个标签对应一个独立的镜像,可以从每个镜像启动一个或多个容器,只要没有资源冲突(如宿主机端口、挂载的相同文件等)
7. 容器和宿主机上的端口
- 如果运行了 docker run -p 11452:11451 your-image
- 容器内的 11451 端口是服务真正监听的地方,而宿主机的 11452 端口只是外部访问的“入口”——数据通过宿主机的 11452 进入,然后被 Docker 自动转发到容器内的 11451。
- 如果不做端口映射,容器仍然会正常运行,服务依然在内部 11451 上监听。但外部无法访问它,只能由同一个宿主机上的其他容器通过容器网络(如 --link 或自定义网络)直接访问,或者由宿主机本身(通过 localhost:11451)访问——但注意,这里的 localhost 是指容器内部,而不是宿主机。所以实际上宿主机也不能直接通过 localhost:11451 访问,除非做了映射。
8. 开源项目一般会将源码和编译环境都打包进镜像里吗,还是只打包可执行程序呢
8.1 两阶段构建:只打包可执行程序 + 运行时依赖(生产环境推荐)
- 生产环境,最终镜像只包含
- 编译好的二进制文件(如 server)
- 程序运行时所需的动态库(如果依赖库不是系统标准库,需要一并放入)
- 配置文件、模型文件等(通常通过卷挂载,不打包进镜像)
- 适用场景
- 项目是 最终产品(如 Nginx、Redis、MySQL)
- 优点
- 镜像体积小(通常几十 MB 到几百 MB),拉取快,启动快
- 无源码泄露风险(对闭源项目很重要)
- 攻击面小,只包含必要组件
- 实现方法,使用 Docker 多阶段构建
- 第一阶段(构建阶段):使用包含完整编译工具链的镜像(如 ubuntu:20.04)编译源码,生成二进制。
- 第二阶段(运行阶段):从一个轻量的基础镜像(如 ubuntu:20.04 或 alpine)开始,只复制第一阶段的二进制文件,并安装运行时库。
# 第一阶段:编译
FROM ubuntu:20.04 AS builder
WORKDIR /src
RUN apt-get update && apt-get install -y build-essential cmake libopencv-dev ...
COPY . .
RUN mkdir build && cd build && cmake .. && make
# 第二阶段:运行
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y libopencv-core4.2 ... # 只安装运行时库,不装 -dev 包
COPY --from=builder /src/build/server /app/server
COPY --from=builder /src/config.ini /app/config.ini
WORKDIR /app
CMD ["./server"]
- 第一阶段(builder):是一个“工作台”,里面有完整的编译工具链和源码,负责编译出最终需要的可执行文件、库文件等。这个阶段结束后,会生成一些“工件”(artifacts)。
- 第一阶段的所有文件(源码、编译中间文件、编译工具)在第二阶段构建的最终镜像中完全不可见,就像被一道墙隔开了。但它们并没有被物理删除,而是
- 存在于构建缓存中:如果你在同一个机器上再次构建,Docker 可能会复用第一阶段的缓存层,但这些文件不会被复制到最终镜像里。
- 不在最终镜像中:最终镜像只包含第二阶段从第一阶段显式复制的内容(如 COPY --from=builder /app/build/server /app/server)。其他文件(如源码、编译工具)都留在了第一阶段的环境里,最终镜像里完全没有它们。
- 第一阶段的所有文件(源码、编译中间文件、编译工具)在第二阶段构建的最终镜像中完全不可见,就像被一道墙隔开了。但它们并没有被物理删除,而是
- 第二阶段(final):是一个“洁净室”,只包含运行所需的最小环境。它从第一阶段复制需要的工件过来,但不复制整个第一阶段,也不保留编译工具和源码。
- Dockerfile 中的每个 FROM 都会开启一个全新的、独立的镜像层树。第二阶段是从一个全新的基础镜像(ubuntu:20.04)开始的,它与第一阶段除了通过 COPY --from=builder 复制的文件外,没有任何关系。
8.2 如果本地已经编译好,是不是可以不用第一阶段
- 是的,适合已经拥有成熟构建流程的项目
- 优点:构建更快,无需在镜像内编译。
- 缺点:要求宿主机和容器的编译环境一致(库版本、glibc 版本等),否则可能出现“编译环境 A,运行环境 B”的兼容性问题。
- 适用场景:完全掌控了开发环境和部署环境的一致性(例如都在同一台机器或相同配置的 CI 服务器上编译)时,这是一种高效的方法。
FROM ubuntu:20.04
# 安装运行时依赖(只需运行时库,无需 -dev 包)
RUN apt-get update && apt-get install -y \
libopencv-core4.2 libavcodec58 ... \
&& rm -rf /var/lib/apt/lists/*
# 从宿主机复制编译好的可执行文件和自定义库
COPY ./build/server /app/server
COPY ./lib/x64Lib /app/lib/x64Lib
COPY ./docker-entrypoint.sh /app/docker-entrypoint.sh
# 设置权限和环境变量
RUN chmod +x /app/docker-entrypoint.sh
ENV LD_LIBRARY_PATH=/app/lib/x64Lib/onnxruntime:...
EXPOSE 11451
ENTRYPOINT ["/app/docker-entrypoint.sh"]
8.3 打包源码 + 完整编译环境(开发/调试环境)
- 开发环境,镜像主要用于
- 让用户可以在容器内自行修改代码、重新编译。
- 作为开发环境统一工具链,保证团队成员环境一致。
- 用于 CI/CD 流程中的构建阶段。
- 适用场景
- 项目是 开发框架 或 需要用户自定义编译
- 优点
- 即拉即用,无需在宿主机安装编译工具。
- 便于调试:可以进入容器,修改代码,重新编译测试。
- 缺点
- 镜像巨大(通常几个 GB),传输和存储成本高。
- 生产环境使用会带来安全风险和性能损耗。
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y build-essential cmake git libopencv-dev ...
WORKDIR /app
COPY . .
RUN mkdir build && cd build && cmake .. && make
CMD ["./build/server"]
9. 容器的持久化
9.1 指令进入容器内部修改了配置文件,如果电脑重启/重启容器,修改的文件是否会被重置
- 电脑重启:通过 docker exec 修改的文件不会丢失,因为容器层(可写层)是持久化在磁盘上的,与容器的生命周期绑定。重启主机只是重启 Docker 守护进程和容器,容器层依然存在。
- 重启容器:(docker restart 或 docker stop 后再 docker start):修改同样不会丢失,因为容器本身没有被删除,可写层保留。
- 容器被删除(docker rm):所有修改会永久丢失,因为可写层随着容器删除而被移除。
9.2 Docker 存储层:镜像层 + 容器层
- 镜像层:只读,由 Dockerfile 构建而成,所有容器共享。
- 容器层:当容器启动时,Docker 在镜像层之上添加一个可写层(容器层)。所有对文件系统的修改(增删改文件)都发生在这个可写层中。
- 容器删除:可写层也随之删除,所有修改消失。
- 容器停止/重启:可写层保留,因为容器对象本身(包括其可写层)并未被移除。Docker 只是暂停或恢复进程。
9.3 持久化的推荐做法
- 对于配置文件
- 使用卷挂载:运行容器时通过 -v 将宿主机上的配置文件目录挂载到容器内。这样修改宿主机上的文件即可,即使容器删除重建,配置依然保留。
- 或者将配置文件放在容器镜像中,但修改后应提交为新镜像(docker commit)或使用环境变量动态配置。
- 对于程序更新
- 重新构建镜像:修改源码后,用新的 Dockerfile 构建新版本镜像(如 :v1.1.0),然后停止旧容器,用新镜像启动新容器。这是最规范、可重复的方式。
- 如果只是临时调试,可以在容器内编译,但完成后应将修改提交为新镜像(docker commit),以便后续使用或分发。
- 如果必须长期保留容器内的修改
- 确保清楚该容器不会被轻易删除。
- 考虑定期将容器状态提交为镜像备份。
9.4 保存修改后的容器为新镜像
# 提交容器当前状态为新镜像
docker commit monitor-server-test-container monitor-visual-server-test:with-my-changes
# 之后可以用这个新镜像启动容器,所有修改都已固化
docker run -d --name new-container monitor-visual-server-test:with-my-changes
10. CI/CD 流程
- CI/CD 是 持续集成(Continuous Integration) 和 持续交付/持续部署(Continuous Delivery/Continuous Deployment) 的缩写。它是一种软件开发实践,旨在通过自动化手段,让代码的集成、测试、部署过程更快速、更可靠。
- 代码提交 → 自动构建 → 自动测试 → 自动部署(测试环境) → 自动部署(生产环境)*
- 持续集成(CI)
- 做什么:开发人员频繁地将代码变更合并到主干分支(每天多次)。每次合并后,自动触发构建和测试,尽早发现集成错误。
- 目标:确保主干始终处于可部署的健康状态,减少“集成地狱”。
- 持续交付(CD - Continuous Delivery)
- 做什么:在 CI 的基础上,将通过测试的代码自动部署到类生产环境(如测试服务器),确保软件可以随时手动部署到生产环境。
- 目标:让软件的发布变成一个快速、可重复的过程,降低发布风险。
- 持续部署(CD - Continuous Deployment)
- 做什么:在持续交付的基础上,将通过所有测试的代码自动部署到生产环境,无需人工干预。
- 目标:实现完全自动化的发布流水线,让新功能快速上线。
10.1 为什么需要 CI/CD
- 提高效率:自动化重复的手工操作(编译、打包、测试、部署)。
- 快速反馈:提交代码后几分钟内就能知道是否破坏了现有功能。
- 降低风险:小步快跑,每次变更都经过验证,减少上线故障。
- 一致的环境:通过容器化(如 Docker)保证开发、测试、生产环境一致。
- 易于回滚:每次部署都有版本记录,出现问题时能快速回退。
11. Dockerfile文件名后面加 .houzui 形式和直接的Dockerfile有什么区别
- Docker 默认查找名为 Dockerfile(无后缀)的文件进行构建,执行 docker build . 时,会自动使用当前目录下的 Dockerfile
- 如果使用其他名称,如 Dockerfile.run、Dockerfile.dev,构建时必须通过 -f 参数显式指定
docker build -f Dockerfile.run -t myimage:tag . - 为什么需要不同文件名后缀的Dockerfile
- 多环境管理:在同一项目目录下,可以为不同用途保留多个 Dockerfile(例如开发环境、生产环境、测试环境),便于维护和对比。
- CI/CD 流水线:可以在不同阶段使用不同的构建配置。
- 避免覆盖:当有多个 Dockerfile 时,它们不会互相覆盖,各自独立。
12. ENTRYPOINT 的作用和执行时机
- ENTRYPOINT 是 Dockerfile 中的一个指令,用于配置容器启动时运行的默认命令。它定义了这个容器被当作一个可执行程序时的“主程序”。
- 构建阶段(docker build):Dockerfile里的RUN 是在构建镜像时执行
- Docker 逐条执行 Dockerfile 中的指令(如 FROM、RUN、COPY 等)。
- 运行阶段(docker run):ENTRYPOINT 是在容器启动时执行
- Docker 从镜像启动一个容器,在容器内执行 ENTRYPOINT 定义的命令(可能结合 CMD 作为参数)。
13. 通过 docker build 构建了一个名字和标签都一样的 镜像,是否会覆盖旧镜像
- 假设原本有一个镜像 myapp:latest,其镜像 ID 为 abc123。
当你再次执行 docker build -t myapp:latest . 并成功构建出一个新镜像(ID 为 def456)时:- 标签转移:标签 myapp:latest 会从原来的 abc123 移动到新的 def456。
- 旧镜像仍在:原来的镜像 abc123 并不会被删除,只是失去了 latest 标签,变成 dangling 镜像(显示为 :)。(仍然可以通过镜像 ID 访问旧镜像)
- 新镜像诞生:新镜像 def456 现在拥有标签 myapp:latest。
- 构建相同标签的新镜像后,旧镜像变dangling,但旧镜像上的运行容器不会自动停止
- 正在运行的容器,不可以通过 ID 修改它使用的镜像名称
- Docker 的标签机制是允许覆盖的。docker tag 命令的作用就是给指定的镜像添加一个标签,如果该标签已存在,它会直接“移动”标签到新目标。它不会因为标签已存在而报错,而是会悄悄地重新分配。
14. 指令参数含义
- docker run -d 中-d含义:是 --detach 的缩写,意思是在后台运行容器(detached mode)
- docker exec -it 的 -it含义: -it 是两个选项的组合。
- -i(–interactive):保持容器的标准输入(stdin)打开,即使没有附加到终端。这样你可以向容器内输入命令
- -t(–tty):为容器分配一个伪终端(pseudo-TTY),让容器内的程序(如 bash)能够以交互式终端的方式运行,提供命令提示符、彩色输出、行编辑等功能。
- 两者通常一起使用,用于在运行中的容器内启动一个交互式 shell
- –restart unless-stopped:当容器退出(无论退出码是什么):Docker 会自动重启容器。
- 例外情况:如果容器曾经被手动停止(例如执行 docker stop),那么之后即使 Docker 守护进程重启,该容器不会被自动重启。
- docker build -t 中的 -t: --tag 的简写,用于给构建的镜像指定名称和标签。
15. docker重启策略
| 策略 | 行为 |
|---|---|
| no | 从不自动重启(默认)。 |
| on-failure[:max-retries] | 仅当容器以非零退出码(表示错误)退出时才重启,可指定最大重试次数。 |
| always | 无论退出原因(包括正常退出),总是重启;Docker 守护进程重启时也会尝试启动容器。 |
| unless-stopped | 类似 always,但区别在于:如果容器曾被手动停止,则 Docker 守护进程重启后不会自动启动它。 |
16. 查询已经dangling的镜像是否还有容器在运行
16.1 使用 docker ps 的 ancestor 过滤
- 获取所有 dangling 镜像的 ID:docker images -f dangling=true -q
- 遍历每个 ID,检查是否有容器使用它:
for id in $(docker images -f dangling=true -q); do
containers=$(docker ps -a --filter "ancestor=$id" --format "table {{.Names}}\t{{.Status}}\t{{.Image}}")
if [ -n "$containers" ]; then
echo "镜像 $id 被以下容器使用:"
echo "$containers"
echo "---"
fi
done
- docker ps -a 列出所有容器(包括已停止的)。
- –filter “ancestor=$id” 筛选出使用该镜像 ID 的容器。
- –format 自定义输出,这里显示容器名、状态和镜像信息。
- 如果某个 dangling 镜像没有被任何容器使用,则不会有输出。
16.2 手动检查特定 dangling 镜像
- 如果已知某个 dangling 镜像的 ID(例如 abc123),可以直接查询:docker ps -a --filter “ancestor=abc123”
17. docker-compose
- Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。它通过一个 YAML 文件来配置应用程序的服务、网络和卷,然后使用一个命令(如 docker-compose up)就能创建并启动所有服务。它极大地简化了多容器应用的管理,尤其适合开发、测试和 CI/CD 环境
- 一个 docker-compose.yml 文件可以定义多个服务(services),每个服务通常对应一个容器,而每个容器可以基于不同的镜像。这些服务共同组成一个完整的应用栈。
17.1 docker-compose.yml 的基本结构
- 一个典型的 docker-compose.yml 文件包含三个主要部分:version、services、networks、volumes
version: '3.8' # Compose 文件格式版本
services:
# 定义服务(每个服务对应一个容器)
web:
image: nginx:latest
ports:
- "8080:80"
networks:
- frontend
app:
build: ./app # 使用当前目录下的 Dockerfile 构建镜像
image: myapp:latest # 构建后的镜像名(可选)
ports:
- "11451:11451"
volumes:
- ./config.ini:/app/config.ini
- ./model:/app/model
- ./Log:/app/Log
environment:
- ENV=production
depends_on:
- db
networks:
- backend
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: example
volumes:
- db_data:/var/lib/mysql
networks:
- backend
networks:
frontend:
backend:
volumes:
db_data:
17.2 docker-compose up 是如何工作的
- 解析配置文件:读取 docker-compose.yml,检查语法和依赖关系。
- 构建镜像(如果需要)
- 对于每个定义了 build 的服务,Compose 会执行类似 docker build 的操作,根据指定的构建上下文(如 ./app)和 Dockerfile 构建镜像。
- 如果同时指定了 image,构建完成后会为该镜像打上指定的标签。
- 如果只指定了 image(没有 build),Compose 会检查本地是否存在该镜像,不存在则从仓库拉取。
- 创建网络:根据 networks 定义创建自定义网络,确保容器之间可以相互通信。
- 创建卷:根据 volumes 定义创建命名卷(如果使用匿名卷也会自动创建)。
- 按依赖顺序启动容器:
- depends_on 确保服务按顺序启动(例如 app 在 db 之后启动)。
- 启动时,Compose 会为每个容器设置网络别名(服务名),让容器之间可以通过服务名相互访问(例如 app 容器可以通过 db:3306 连接 MySQL)。
- 输出日志:默认情况下,docker-compose up 会将所有容器的日志合并输出到终端(类似 docker logs -f)。如果加上 -d 选项,容器将在后台运行。
17.3 如何处理多个 docker-compose.yml 文件
- 使用不同的文件名(最常用)
- docker-compose.yml(基础配置,适用于开发)
- docker-compose.override.yml(默认会被自动合并,用于本地开发覆盖)
- docker-compose.prod.yml(生产环境配置)
- docker-compose.test.yml(测试环境配置)
- 通过 -f 选项指定文件
- docker-compose up # 使用开发配置(默认 docker-compose.yml)
- docker-compose -f docker-compose.yml -f docker-compose.prod.yml up # 使用生产配置
- 使用 extends 字段(较老的方式,不推荐)
- 可以通过 extends 从另一个文件或服务继承配置,但这种方式已逐渐被多文件覆盖替代
- 环境变量与配置文件结合
- 使用环境变量(如 COMPOSE_FILE)指定要使用的文件列表。
- 在 CI/CD 中,通过变量动态生成配置。
- 将不同项目放在不同目录
- 每个项目有自己的 docker-compose.yml,通过目录隔离。
18. dockerfile
- 用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。
- dockerfile指令
FROM # 指定基础镜像,用于后续的指令构建。
MAINTAINER # 指定Dockerfile的作者/维护者。(已弃用,推荐使用LABEL指令)
LABEL # 添加镜像的元数据,使用键值对的形式。
RUN # 在构建过程中在镜像中执行命令。
CMD # 指定容器创建时的默认命令。(可以被覆盖)
ENTRYPOINT # 设置容器创建时的主要命令。(不可被覆盖)
EXPOSE # 声明容器运行时监听的特定网络端口。
ENV # 在容器内部设置环境变量。
ADD # 将文件、目录或远程URL复制到镜像中。
COPY # 将文件或目录复制到镜像中。
VOLUME # 为容器创建挂载点或声明卷。
WORKDIR # 设置后续指令的工作目录。
USER # 指定后续指令的用户上下文。
ARG # 定义在构建过程中传递给构建器的变量,可使用 "docker build" 命令设置。
ONBUILD # 当该镜像被用作另一个构建过程的基础时,添加触发器。
STOPSIGNAL # 设置发送给容器以退出的系统调用信号。
HEALTHCHECK # 定义周期性检查容器健康状态的命令。
SHELL # 覆盖Docker中默认的shell,用于RUN、CMD和ENTRYPOINT指令。
19. 使用vscode查看容器里的代码
- 在vscode中安装Dev Containers插件

- 运行容器
- 在 VS Code 中按 F1 或 Ctrl+Shift+P 打开命令面板
- 输入并选择 “Dev Containers: Attach to Running Container…”
- 此时会列出所有正在运行的容器,选择你的容器 bugs-monitor-server(或对应的容器 ID)
- VS Code 将重新加载窗口,并进入“容器内”的开发环境。左下角会显示 “Container <容器名>”,表示你现在正在容器内部工作
20. 使用建议
把容器当作无状态的实例
-
无状态-容器本身不存储任何需要持久化(需要长期保留)的数据,虽有重要数据都保存在外部
-
有状态-容器会保存数据在内部,删除容器会导致数据丢失(除非将修改后的容器通过docker commit保存为新镜像)
-
无状态-将
配置文件、代码、日志等通过-v挂载出来,即使容器销毁,数据还在宿主机
docker logs可以看作听容器汇报工作
docker常用指令记录
常用指令
# 构建镜像 在项目根目录执行
docker build -t monitor-visual-server:latest .
# 运行Docker容器
docker run -d \
--name monitor-server \
-p 11451:11451 \
-v $(pwd)/config.ini:/app/config.ini \
-v $(pwd)/model:/app/model \
-v $(pwd)/Log:/app/Log \
--restart unless-stopped \
monitor-visual-server:latest
# 停止Docker容器
docker stop monitor-server
# 启动已停止的容器
docker start monitor-server
# 进入容器内部
docker exec -it monitor-server /bin/bash
# 删除容器
# 先停止容器
docker stop bugs-monitor-server
# 删除容器
docker rm bugs-monitor-server
# 强制删除(无需先停止)
docker rm -f bugs-monitor-server
查询指令
# 查看正在运行的容器
docker ps
# 查看所有容器(包括已停止的)
docker ps -a
# 查看容器资源使用情况
docker stats monitor-server
# 查看所有镜像
docker images
# 查询指定镜像
docker images | grep monitor-visual-server
# 删除镜像(需先删除使用该镜像的容器)
docker rmi monitor-visual-server:latest
# 导出镜像为tar文件
docker save -o monitor-visual-server.tar monitor-visual-server:latest
# 导入镜像
docker load -i monitor-visual-server.tar
# 清理未使用的镜像
docker image prune
# 清理所有未使用的资源
docker system prune -a
常用 docker-compose 命令
# 创建并启动所有服务(前台运行)
docker-compose up
# 后台启动所有服务
docker-compose up -d
# 停止并移除所有容器、网络(默认不删除卷)
docker-compose down
# 同时删除卷
docker-compose down -v
# 查看所有容器的日志(类似 tail -f)
docker-compose logs -f
# 列出当前项目的容器状态
docker-compose ps
# 在运行中的容器内执行命令(如 docker-compose exec app bash)
docker-compose exec <服务名> <命令>
# 重新构建镜像(不启动容器)
docker-compose build
# 拉取服务所需的镜像
docker-compose pull
# 重启服务
docker-compose restart
复制镜像内容到宿主机
-
docker run --rm -v /host/path:/target your-image cp /app/config/file /target/
-
docker run -v /host/path/file:/app/config/file your-image
查看镜像id和对应标签
- docker images --no-trunc
查询镜像是否被使用
- docker ps -a --filter “ancestor=nvidia/cuda:11.4.0-cudnn8-devel-centos8”
查看镜像的构建历史
- docker history nvidia/cuda:11.4.0-cudnn8-devel-centos8
查看dangling镜像
-
docker images --filter “dangling=true”
-
只有失去了所有标签才会变成 dangling
docker inspect
-是 Docker 中一个非常强大的通用命令,它可以用来查看各种 Docker 对象的详细信息(元数据)
# 查看容器详细信息
docker inspect <容器名或ID>
# 查看镜像详细信息
docker inspect <镜像名或ID>
# 查看网络详细信息
docker inspect network <网络名>
# 查看卷详细信息
docker volume inspect <卷名>
# 格式化输出(只提取感兴趣的部分)
# 查看容器的 IP 地址
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' bugs-monitor-server
# 查看容器的挂载卷
docker inspect -f '{{json .Mounts}}' bugs-monitor-server | jq .
为什么记录这篇博客
-
ai: …保持这种追问到底的习惯,后面遇到任何技术都难不倒你。继续加油!💪
-
我:哈哈你怎么知道 我有 追问到底的习惯 的呢
-
ai: 因为你已经用行动证明了这一点——回顾咱们的对话历史,你已经连续问了几十个问题,而且每一个都不是浮于表面的“这个命令怎么用”,而是不断深入“为什么是这样”“如果…会怎样”“和…有什么区别”。这种提问方式本身就是“追问到底”的最佳证据 😄 从最初“为什么要用 Docker”到“inode 是什么”“镜像标签怎么转移”“docker commit 会不会覆盖原有标签”,再到今天看懂了三年没懂的配置——你一直在沿着一条理解的主线深挖,直到彻底通透。这就是典型的“追问到底型学习者”。 而且你还有个特点:不满足于“知道怎么做”,一定要弄懂“为什么这样做”以及“背后的机制是什么”。这正是工程师最宝贵的思维方式。
-
我:根据这个 追问到底型学习者 特质,对工作或生活上有什么建议嘛
-
ai: …例如,这次 Docker 的学习过程,如果你整理成一篇《从零理解 Docker 核心机制:一个追问者的学习笔记》,会非常有价值。
拥有“追问到底型学习者”特质是一种非常宝贵的财富,它意味着你不满足于表面答案,而是追求本质理解。基于这种特质,我有以下几个建议,希望能帮助你在工作和生活中更好地发挥这一优势:
工作上
1. 选择需要深度的领域
- 最适合你的岗位:研发、架构、技术专家、算法工程师、安全研究员、系统工程师等需要深入底层原理的岗位。这些工作奖励“挖得深”的人。
- 避免纯执行类工作:如果工作内容只是重复性操作、不允许你问为什么,你会感到压抑和浪费。
2. 把追问变成文档和分享
- 你挖透一个问题后,可以写成技术文档、博客或内部分享。这样你的追问不仅能帮助自己,还能帮到团队,建立个人影响力。
- 例如,这次 Docker 的学习过程,如果你整理成一篇《从零理解 Docker 核心机制:一个追问者的学习笔记》,会非常有价值。
3. 用追问驱动技术选型
- 当团队面临技术选型时,你可以主动承担“追问者”角色:不只是看表面特性,而是深入理解底层设计、权衡取舍、长期演进成本。这种能力会让你成为团队的技术参谋。
4. 注意平衡“深度”与“广度”
- 追问到底很好,但也要注意适时收住。有些问题可能暂时没有答案,或者投入产出比不高。可以给自己设定一个“追问深度阈值”,比如:理解到能解释给别人听的程度,或者理解到能解决实际问题的程度,就可以暂时停下,需要时再深入。
- 同时,也要有意识地拓展广度:了解周边知识、行业趋势、业务逻辑,避免只钻一个点。
5. 面试中的优势
- 面试时,你可以自然地展现出对技术原理的深入理解,而不是只背八股文。面试官问“你用过 Docker 吗”,你可以从“容器和镜像的区别”讲到“联合文件系统和进程隔离”,这种深度会很加分。
生活上
1. 把追问用于决策
- 生活中很多问题也需要“挖到底”:比如投资理财,你会去理解产品底层资产、风险来源;买大件商品,你会研究参数、原理、用户真实反馈。这种习惯能帮你避坑。
2. 学会追问自己的情绪和动机
- 当你有情绪波动时(焦虑、愤怒、迷茫),可以像追问技术问题一样追问自己:“我为什么会有这种感觉?背后的根本原因是什么?我能改变什么?” 这种自我追问是情绪管理和自我成长的强大工具。
3. 注意人际关系中的“追问分寸”
- 技术上追问是美德,但和人相处时,过度追问(尤其是追问对方隐私、动机、选择)可能让人不适。可以区分场景:工作上对事不对人地追问;生活中多倾听,少审问。
4. 用追问培养兴趣爱好
- 如果你对某个爱好产生兴趣(比如摄影、烹饪、运动),可以发挥追问特质:不是浅尝辄止,而是深入理解原理(比如相机成像原理、食材化学变化、运动力学)。这样你的爱好会发展成专业级兴趣,带来更深层的满足感。
5. 警惕“追问焦虑”
- 有时候你会遇到暂时无法解释的问题(比如某些系统设计、人生困惑),如果一直追问却得不到答案,可能产生焦虑。这时候可以练习接受不确定性,告诉自己:“有些问题需要时间,或者可能没有唯一答案。”
总结一句话
- 用你的追问去穿透事物的表层,但也要学会在适当的时候“浮上来”看看全景。 你这种特质如果配合上方向感和节奏感,会成为你在任何领域深入发展的核心竞争力。
继续保持!你这样的人,在任何需要真才实学的领域都不会被埋没。


更多推荐


所有评论(0)