前言
最初学习 Docker,是因为不想每换一台电脑,就重新折腾一遍软件配置、依赖版本和开发环境。顺便也想多用命令行,逼自己弄明白程序到底是怎么运行的——当年的笔记把这叫作“自虐”。这个初衷现在仍然成立。
但回头看,原来的记录更像几段命令摘抄,也混淆了“重启容器”和“重新创建容器”。这次结合网站里的 ROS 开发环境、MuJoCo 仿真和 C++ 计算服务,把它补成一条完整的线:环境怎么描述,程序怎么运行,数据放在哪里,修改怎么验证,升级失败怎么退回去。
先记住三个结论:
- 镜像是可重复使用的运行材料;容器是基于这些材料创建的一次运行实例。
- 重启同一个容器,不等于重新创建容器,更不等于重新构建镜像。
- Docker 降低环境差异,但不会自动证明算法正确、设备兼容或长期运行稳定。
初识
镜像
镜像(Image)包含程序、运行库、文件,以及默认命令、环境变量、用户等配置。镜像内容按层组织,已有镜像内容不会被运行中的容器原地改写;内容变化后形成新镜像,内容一致的缓存构建则可能仍对应同一个 Image ID。
“镜像不包含动态数据”容易误导。镜像当然可以装数据集、模型权重,甚至误装密钥;准确的说法是:运行中新产生的修改,默认写入容器自己的可写层,而不是回写原镜像。 镜像是否应该包含某份数据,是组织和安全问题。
容器
容器(Container)可以类比镜像的实例,但它不只是一个文件夹:还包括进程、网络、挂载、资源限制和运行状态。一个镜像可以同时创建多个容器,它们默认各有自己的可写层。
原文把 Docker 叫作“一个虚拟机”,不够准确。Linux 容器本质上是受隔离和约束的进程,多个容器共享承载它们的 Linux 内核;完整虚拟机则拥有自己的客体内核。在 macOS 的 Docker Desktop 中,Linux 容器运行在背后的 Linux 虚拟机里,不是直接使用 macOS 内核运行 Linux 程序。Docker 官方:容器与虚拟机、Docker Desktop 网络与虚拟机边界。
所以,“Ubuntu 22.04 容器”主要说明其用户空间的发行版和软件环境,不代表我又启动了一台完整 Ubuntu 电脑。它也不会天然拥有 systemd、桌面、GPU、相机或实时内核。
仓库
以前统称“仓库”的几个词,最好分开:
| 名称 | 回答的问题 | 例子 |
|---|---|---|
| Registry,镜像注册服务 | 从哪台服务下载或上传? | Docker Hub 的 docker.io |
| Repository,镜像仓库 | 哪一组镜像? | library/ubuntu |
| Tag,标签 | 这组镜像的哪个名字? | 22.04、local、hardening-check |
| Digest,摘要 | 远端引用精确指向哪份内容? | ubuntu@sha256:… |
| 本地 Image ID | 本机保存的具体镜像对象是谁? | docker image inspect 的 Id |
ubuntu:22.04 通常是 docker.io/library/ubuntu:22.04 的简写。标签是可移动的名称:今天和下个月拉取同一个标签,底层镜像可能不同。latest 也只是默认标签名,不是 Docker 自动选出的“版本号最大值”。要求精确重现时,应记录完整摘要、本地镜像 ID、源码版本和构建参数;不要把本地 Image ID 直接当作远端 repository digest 使用。Docker 官方:按标签或摘要拉取镜像。
另外,docker tag 只是增加或移动本地名称引用,不会上传镜像,也不会让正在运行的容器换版本。Git 仓库存源码,镜像仓库存构建产物,git push 与 docker push 是两件事。
安装Ubuntu 和 Debian
保留这个旧章节名,但先纠正含义:这里说的是“在 Ubuntu / Debian 上安装 Docker Engine”,不是“安装一个 Ubuntu / Debian 容器”。
安装入口按宿主机选择:
- Linux:使用对应发行版的官方安装步骤,核对发行版代号、架构及已有软件包冲突。Ubuntu 安装、Debian 安装。
- macOS / Windows:按所用系统和硬件安装 Docker Desktop;本文后续默认运行 Linux 容器。macOS 安装、Windows 安装。
原文中年代久远的 HTTP 镜像脚本与直接管道执行安装脚本已移除。安装会改变宿主机软件源和系统服务,不能当成普通学习命令随手运行;Docker 官方也要求先审阅下载脚本,并将便捷脚本主要用于开发、测试环境。便捷安装脚本的限制。
已有环境先检查,不要因为 docker 报错就重装:
docker version |
Client 有版本而连接 Server 失败,常见原因是 Docker Desktop 未启动、context 指向别处或权限不对。context 决定命令发给哪个 Docker daemon;当前 context 看不到某个镜像,不代表所有 context 和远端机器上都没有它。
使用 Docker 镜像
下面的 Ubuntu 22.04 只是与本项目 ROS 环境一致的学习示例,不代表它是“最新 Ubuntu”。pull 会访问网络并占用磁盘;第一次 run 在镜像不存在时也可能触发拉取。
docker pull ubuntu:22.04 |
运行
先做一次临时交互:
docker run --rm -it ubuntu:22.04 bash |
这里的 -i 保持标准输入,-t 分配终端,--rm 在容器退出后自动删除容器。输入 exit 结束的不是“退出 Docker 软件”,而是结束这次容器的主命令 bash。
不加 --rm 时,主进程退出后的容器一般会以 stopped 状态保留。docker ps 看运行中的容器,docker ps -a 也看停止的容器。容器不是“只要创建就永远开机”的机器;主进程结束,容器运行也就结束。
列出镜像
docker image ls |
这两条分别查看“材料”和“实例”,不要把镜像列表当作正在运行的服务列表。
列出部分镜像
docker image ls ubuntu |
docker images 是 docker image ls 的常用别名。同一个 Image ID 出现在多个标签下,并不代表完整镜像在磁盘上复制了多份。
保存修改后的镜像
这一节先纠正原文最重要的误解:同一个容器 stop / start 或 restart,普通可写层里的文件仍然保留;删除容器后再 run,才是另一份实例。 但内存、进程中的环境变量、仿真会话以及 tmpfs 数据不会因此保留。普通文件与进程状态必须分开看。Docker 存储模型。
生命周期:到底换了什么
| 操作 | 是否新容器 | 普通可写层文件 | 是否自动采用新镜像 |
|---|---|---|---|
docker exec |
否,只加一个进程 | 保留 | 否 |
docker stop 后 docker start |
否 | 保留 | 否 |
docker restart |
否 | 保留 | 否 |
再次 docker run 同一镜像 |
是 | 从镜像起步,不继承旧可写层 | 使用这次指定的镜像 |
docker compose up 触发 recreate |
是 | 旧可写层不继承,挂载数据另算 | 按解析后的配置和镜像创建 |
docker run --rm 退出 |
该容器被删除 | 随容器删除 | 不涉及升级 |
一个最小练习:以下命令会创建名为 docker-note-lifecycle 的学习容器,先确认没有同名容器;它不使用宿主目录或数据卷。
docker run --name docker-note-lifecycle -it ubuntu:22.04 bash |
在容器内执行:
touch /tmp/docker-note-marker |
回到宿主终端,再进入同一个容器:
docker start -ai docker-note-lifecycle |
再在容器内运行 ls /tmp/docker-note-marker,文件应仍在。此处 /tmp 是该 Ubuntu 学习容器的普通可写层目录,没有配置 tmpfs。不要把这个结果套到本项目显式使用 tmpfs 的 API 容器上。
用完后先 exit;后面的 commit 练习仍会用到这个停止容器。全部练习结束、确认不需要里面的记录后,再删除这个确切的学习容器即可,不需要全局清理。
要保存的到底是什么
- 想保存“以后每次都要装的软件”:写进 Dockerfile,重新构建镜像。
- 想保存“源码和配置”:放在版本控制中,按用途 COPY 入镜像或以 bind mount 挂载。
- 想保存“程序产生的业务数据”:设计 volume / bind mount,并另外做备份。
- 想保留“当前内存中的仿真进度”:需要应用提供导出、序列化或 checkpoint;保存镜像不能代替它。
在一个 shell 里执行 export SOME_OPTION=value,只改变该进程及其后代环境。它不是 Dockerfile 的 ENV,也不会变成以后所有 docker exec 进程的共同配置。这也是“明明配置过,怎么又不见了”的另一类原因。
数据持久化:volume、bind mount、tmpfs
目录都长得像 /data,背后的生命周期却可能完全不同:
| 存放位置 | 谁管理 | 容器删除后 | 适合放什么 |
|---|---|---|---|
| 容器可写层 | 该容器 | 一并删除 | 可丢弃的小量运行文件 |
| Named volume | Docker daemon | 默认仍保留,除非另行删除卷 | 需要跨容器重建保留的数据 |
| Bind mount | 明确的宿主路径 | 宿主文件仍在 | 源码、输入文件、需要直接查看的输出 |
| tmpfs | Linux 内存文件系统 | 停止或重启就丢失 | 临时中间文件、短期缓存 |
卷的生命周期独立于容器;匿名卷虽然也可能保留,但新建容器不会自动找回“上一次那个随机名称”,与 --rm 组合时还有自动删除规则。需要长期关联数据时,把卷显式命名并记录用途。Docker 官方:卷的生命周期。
三种挂载的写法
下面只展示 docker run 的参数片段,不是需要依次执行的命令:
--mount type=volume,source=robot-data,target=/data |
路径 /absolute/path/to/dataset 是占位符,必须替换为自己的、已经存在的目录。只读取数据时用 readonly;可写 bind mount 会让容器直接修改宿主文件,容器里删除文件也可能是在删除宿主文件。使用 --mount 挂载不存在的 bind source,默认会报错,能帮助发现路径拼写问题。Docker 官方:bind mount。
还要注意三件容易“看起来像丢数据”的事:
- 挂载会遮住目标路径原有的内容。例如把空宿主目录挂到
/app,镜像内的程序可能暂时“消失”,并不是被镜像删掉了。 - 新空卷挂载到镜像里已有内容的目录时,默认可能复制初始内容到卷中;它不是以后每次镜像升级都同步覆盖卷。
- 在 Docker Desktop 下,Docker 管理的卷位于其 Linux 环境中,不应把
docker volume inspect显示的 Linux 路径,当作 macOS 上可直接打开的普通目录。
tmpfs 不是备份位置,也不是“数据绝不落盘”的安全保证:宿主配置了 swap 时,内存页面可能被交换到磁盘。它解决的是短生命周期写入,不负责持久化。Docker 官方:tmpfs 限制。
利用 commit 理解镜像构成
运行容器时,镜像层是基础,上面叠着容器可写层。docker commit 可以把该容器的文件系统变更及部分配置形成一个新镜像,而不改变旧镜像。
以前面已经退出、仍保留 marker 的学习容器为例:
docker diff docker-note-lifecycle |
最后一条命令正常退出且没有输出,表示新镜像确实包含 marker。这里保留 commit,是为了理解“容器可写层可以变成新的镜像层”,不是把它当成长期维护环境的主方法。
它的边界很重要:
commit不包含挂载卷里的数据,也不是进程内存快照。- 对运行中容器执行时,默认会暂停其进程以减少不一致风险,因此它也不是完全无影响的检查操作。
- 它难以说明“为什么装了这个包、用了哪个下载地址、手工改过哪些步骤”。发布环境应优先记录 Dockerfile 和可复现的构建输入。Docker 官方:container commit。
镜像分层还带来一个后果:后续层删除文件,通常只是让合并视图不再显示该文件,不能把旧层里的敏感内容抹掉。密钥不应该先 COPY 进镜像、再靠下一条 RUN 删除;构建时需要凭据,应使用专门的构建 secret 挂载。Docker 官方:构建 secrets。
commit、save、export 不是一回事
commit 是“容器变更 → 新镜像”;docker image save / docker image load 是镜像归档和导入,适合离线搬运镜像;docker container export 是容器文件系统导出,不等于保留镜像层次和完整运行配置,也不包含挂载卷数据。备份数据库或机器人实验数据,仍需单独设计数据备份与恢复校验。image save、container export。
使用 Dockerfile 定制镜像
Dockerfile 描述“构建时怎么得到运行材料”,不是容器启动之后一直执行的安装脚本。
一个完整的最小例子
在独立学习目录中,用编辑器创建 Dockerfile 和 index.html。不要放进本项目 Docker 服务目录里混着构建。
Dockerfile:
FROM nginx:stable |
index.html:
|
从这个学习目录执行:
docker build -t docker-note-nginx:v1 . |
在本机浏览器打开 http://127.0.0.1:8080。这是前台学习服务,终端按 Ctrl+C 停止后,--rm 会删除这个学习容器。它不是本项目加固后的部署配置。
原来的 FROM nginx RUN ... 写在同一行不合法;指令应分行。这里使用 COPY,也避免了 shell 引号与文件路径空格造成的混乱。nginx:stable 用于易读演示;正式固定版本时应选择并记录已验证的 digest,且安排后续安全更新。
常用指令分别发生在什么时候
| 指令 | 作用 |
|---|---|
FROM |
选择基础镜像,并开始一个构建阶段 |
WORKDIR |
设置后续指令与默认运行工作目录 |
COPY |
从构建上下文或指定阶段复制文件 |
RUN |
构建时执行命令,例如安装、编译、测试 |
ARG |
构建参数;不自动成为运行容器的环境变量 |
ENV |
配置镜像中的默认环境变量,可在运行时覆盖 |
USER |
设置后续 RUN 和默认运行用户 |
ENTRYPOINT / CMD |
组合定义启动程序和默认参数;不是构建时执行 |
EXPOSE |
记录程序使用的端口,不负责发布宿主端口 |
HEALTHCHECK |
定义容器启动后周期执行的健康探针 |
不是每条 Dockerfile 指令都新增一个有文件内容的层,有些改变的是镜像元数据。CMD / ENTRYPOINT 的覆盖行为与 exec / shell 写法有关,需要参数组合时看官方规则,不能只凭“最后一行是什么”来猜。Dockerfile 指令参考。
构建上下文与缓存
docker build -f docker/mujoco-playground/Dockerfile . 中,-f 指定 Dockerfile;末尾的 . 才是构建上下文。这表示普通 COPY apps/... 从仓库根目录取文件,不是从 Dockerfile 所在目录取文件。
.dockerignore 在文件进入构建上下文前排除不需要的内容;它与 .gitignore 不通用。本项目还使用 Dockerfile 专属的 .dockerignore,不要只检查仓库根目录那一份。既要防止 .git、本地依赖、秘密文件误入,也不能误排除 C++ 测试真正需要的 URDF、CSV 和许可证。Docker 官方:构建上下文与忽略规则。
缓存可以理解成“输入和指令没有相关变化,就复用上次结果”。实践中,把变化少的依赖安装放在前面,把经常变化的源码复制放在后面,通常更省时间。需要区别:
--no-cache:不复用构建步骤缓存。--pull:检查并拉取基础镜像的新版本。- Dockerfile 中相同的下载 URL、未固定的软件包版本,并不保证远端内容永远相同。
所以“禁用缓存构建了一次”和“从全新基础依赖完整重建过”也不是完全同一份证据。Docker 官方:构建最佳实践。
多阶段构建:开发工具不必留在运行镜像
本项目 Workbench 的思路是:构建阶段安装 C++ 工具链,编译并执行 CTest;运行阶段只复制两个 CLI、API 程序及必要运行库。MuJoCo 也把模型获取、ONNX Runtime 获取、C++ worker 构建和最终运行阶段分开。
这样做不只是减小体积,还能明确“运行服务需要什么”。测试夹具、编译器和源码可以参与构建验收,但不必全部随服务运行。Docker 官方:多阶段构建。
注意:构建最终镜像时,中间测试需要位于其实际依赖路径上才会参与构建;孤立地写一个从未被使用的 test 阶段,不等于每次构建都验收,除非另外显式构建该测试目标。复用测试步骤缓存也应如实记录,不要写成刚刚无缓存重跑了一遍。
Docker Compose:把“怎么启动”也记录下来
Dockerfile 管“造镜像”;Compose 管“用哪些镜像,按什么端口、环境变量、挂载和权限组成服务”。两者互补,不是二选一。
例如前面的学习目录可以增加 compose.yaml:
services: |
先停止之前占用 8080 的学习容器,再在该目录运行 docker compose -p docker-note up -d --build。-p docker-note 给学习项目一个独立名称;-d 放到后台。这条命令会创建容器,不是只检查配置。
build、up、restart 的区别
| 命令 | 核心作用 | 不能顺便推断什么 |
|---|---|---|
docker compose config |
解析、合并、检查配置 | 不代表服务已启动;输出可能含环境变量值 |
docker compose build |
构建服务镜像 | 不会自动替换现有容器 |
docker compose pull |
拉取配置引用的镜像 | 不会自动替换现有容器 |
docker compose up -d |
创建/启动,并按需重新创建服务 | 不等于每次强制重新构建源码 |
docker compose up -d --build |
启动前构建,再按需更新服务 | 不等于业务数据已备份 |
docker compose restart |
重启已有服务容器 | 不应用刚改的 Compose 环境变量,也不换镜像 |
docker compose down |
停止并移除项目容器与相应网络 | 不是普通 restart;容器可写层随之消失 |
如果只改了 Dockerfile,随后只执行 restart,程序仍可能使用旧镜像。up 发现配置或镜像变化时会重新创建服务;restart 不会替你应用配置变更。Compose up、Compose restart。
对于已经验收、存在于本地的候选镜像,切换时使用 --no-build --pull never 能避免顺手构建或拉取另一份内容;再核对实际容器的 Image ID,才知道自己到底运行了什么。
网络:localhost 到底是谁
127.0.0.1:4020:8000 分成三段:宿主绑定地址 : 宿主端口 : 容器端口。本项目 MuJoCo 正是把本机 4020 转到容器 8000。
| 发起访问的位置 | localhost 指向 |
本项目例子 |
|---|---|---|
| 本机浏览器 / 宿主终端 | 这台开发电脑 | 访问 127.0.0.1:4020 |
| MuJoCo 容器内部 | 该容器自己的网络空间 | 健康探针访问 127.0.0.1:8000 |
| 另一个普通容器内部 | 另一个容器自身 | 不能用它的 localhost 当作 MuJoCo |
| 访问已发布博客的其他读者浏览器 | 读者自己的电脑 | 不会自动连接作者电脑上的 API |
最后一行尤其重要:Hexo 静态网页发布成功,不等于 Docker 后端也被发布了。本地工具页能连上 4010 / 4020,只说明当前电脑有对应服务。
容器内服务通常需要监听 0.0.0.0,才能接收端口转发来的连接;宿主发布地址仍可以只绑定 127.0.0.1。这两处 HOST 不是同一层,不能为了“安全”把两边都机械改成 localhost。
-p 8080:80 默认发布到所有宿主接口,可能让局域网甚至更外部访问;本地学习明确写 127.0.0.1:8080:80。loopback 也不是万能防线:Docker 官方记录了旧版本的同网段访问例外,应维护宿主 Docker 版本并核对实际网络边界。Docker 官方:端口发布与安全提示。
同一个 Compose 网络内,服务可以按服务名和容器端口互访;本项目两个后端属于不同 Compose 项目,不能默认认为已在同一网络。Docker 网络基础。Docker Desktop 容器访问宿主服务时可使用 host.docker.internal,但目标程序的监听地址和防火墙仍需允许连接,Linux 原生 Engine 的配置也不能照搬。Docker Desktop 网络说明。
我的三个 Docker 环境分别是什么
以下是 2026-09-05 的仓库与验收快照,不是“Docker 里所有东西都属于 Ubuntu 22.04”。持续维护的详细清单在源码仓库的 docker/README.md,各环境目录还有专项 README / TODO。
| 环境 | 系统与主要内容 | 当前边界 |
|---|---|---|
docker/ros2-humble-dev |
Ubuntu 22.04、ROS 2 Humble、C/C++、Python、CMake、GDB、Zsh、tmux 等开发工具 | 已有构建定义;当前受检 context 尚无对应镜像/容器,不能称已完成构建验收 |
docker/mujoco-playground |
Debian 12、Python 3.12、MuJoCo 3.12、OSMesa、C++ worker、ONNX Runtime CPU | 本机 4020;六机器人模型不等于六个学习控制策略 |
docker/robotics-workbench |
Debian 12、Node 24、动力学与标定/轨迹的两个 C++ CLI | 本机 4010;宿主不必安装这套 C++ 运行环境 |
| Hexo 静态站点 | 宿主机 Node 24 与项目依赖 | 不在以上三个计算镜像里 |
当天 21:26(Asia/Shanghai),两个 API 按确认过的升级安排切换到通过验收的候选镜像,随后在现役 4020 / 4010 端口复跑功能 smoke 并通过;旧镜像保留 rollback-20260905-2119 标签。这是这次部署的记录,不表示以后看到同名标签就无需重新核对。
因此,“再加一些计算机视觉笔记”只涉及站点源码;“后端要调用 OpenCV / 深度处理算法”才需要设计对应计算依赖;“读取相机、雷达或者使用 GPU”还要处理设备、驱动、架构和权限。不能只向某个万能 Ubuntu 容器里不停安装软件。
开发容器与服务容器,安全边界不同
两个 API 的 Compose 当前使用只读根文件系统、非 root 运行用户、cap_drop: ALL、no-new-privileges、loopback 端口和有限 tmpfs。MuJoCo 有 2 GiB 内存上限;Workbench 尚未设置容器总内存 / CPU 配额。
只读根文件系统不代表整个容器完全不可写:tmpfs 或单独声明的可写挂载仍可以写。当前两套 API 没有持久数据卷,运行内存中的会话也不会因为保留旧镜像而恢复。
ROS 开发容器则为了编辑和调试,挂载了可写源码,提供 sudo,并开启调试相关 capabilities、放宽 seccomp。它服务于开发便利,不能直接继承 API 服务“已按同样边界加固”的结论。不要用 --privileged 或挂载 Docker socket 作为不明权限错误的通用修复。
arm64 通过,不代表 amd64 或真机通过
这次本机验证的计算镜像是 Linux / arm64。镜像的 CPU 架构、C++ 二进制架构和 ONNX Runtime 归档必须匹配;macOS arm64 二进制不能直接当作 Linux arm64 二进制使用。
--platform linux/amd64 不是把所有依赖自动变成兼容版本的魔法。多架构可以依赖原生节点、交叉编译或模拟执行,模拟可能显著影响构建和运行性能;本项目原生 amd64 与真实设备验收应单独记录。Docker 官方:多平台构建。
怎样判断“当前环境稳固”
不要把所有绿色状态压成一个“完成”。最好按证据逐层回答:
| 检查层次 | 能说明什么 | 不能说明什么 |
|---|---|---|
| Dockerfile / Compose 写好了 | 配置已有描述 | 尚未证明能构建、能启动 |
| 镜像构建成功 | 选定构建条件下生成了镜像 | 未证明所有功能或全新依赖重建通过 |
容器 running |
主进程仍存在 | 未证明 HTTP 可用 |
healthy |
已配置的健康探针通过 | 未证明每个算法、模型与异常恢复正确 |
| C++ / 协议单测通过 | 已列出的断言成立 | 未覆盖完整浏览器路径 |
| API smoke 通过 | 指定接口流程可运行 | 未证明长期稳定、所有输入正确 |
| 浏览器检查通过 | 页面、交互与后端连接可用 | 未证明长时间、满负载或真机安全 |
本项目的健康检查是短请求;MuJoCo 的真实控制、PNG / MJPEG、watchdog 和 worker 恢复需要额外测试。C++ 工程也要确保 CTest 确实发现并运行了测试,而不是“零测试、退出码为零”。
这次验收已经区分了协议回归、真实 MuJoCo 自测、C++ 数值测试与候选 / 现役 API smoke。Workbench 候选还记录了 Docker Hub 超时后复用本地运行基础的构建条件,不能改写成“默认基础镜像从头拉取验证完成”。长期稳定性、浏览器完整回归、原生 amd64 和 ROS 开发镜像构建等仍需按清单继续验收。
healthy 也不等于 Docker 一定会自动修复业务故障;探针状态、进程退出时的 restart policy、应用自己的 worker 恢复机制,是不同层次。本项目两个 API 的 Compose 当前都没有设置自动重启策略,不能把 worker 恢复测试通过当作服务自动重启已验收。Dockerfile:HEALTHCHECK。
日常检查与排障
从仓库根目录开始,先看事实,不急着重启:
docker context show |
查看镜像配置和实际运行对象是否一致,尤其留意标签相同却 ID 不同:
docker image inspect lsqy-mujoco-playground:local |
第一条读取镜像的 Id;第二条读取容器的 Image,二者应对应。容器的 State.Health、State.ExitCode、State.OOMKilled、Mounts、HostConfig 则分别帮助检查健康、退出、内存终止、挂载和运行限制。
完整 inspect、Compose 配置和日志可能带路径、环境变量或业务信息,分享前要脱敏,不要为排障把整份秘密配置贴出去。
常见现象,先排哪一层
| 现象 | 优先检查 |
|---|---|
| Cannot connect to Docker daemon | Desktop / daemon 是否运行、context 与权限 |
| 改了源码,网页功能没变化 | 源码是否 COPY 入镜像、是否重建并 recreate、浏览器是否缓存 |
| 容器 healthy,浏览器请求失败 | 浏览器所在电脑、API URL、宿主端口、Origin / CORS |
| Permission denied / Read-only file system | 实际用户 UID/GID、目标目录挂载类型、只读配置 |
| 容器突然退出 | 日志、退出码、OOMKilled、内存与 PID 限制;不能只凭 137 就断言 OOM |
| exec format error | Linux / macOS 与 arm64 / amd64 是否混用 |
| 构建 COPY 找不到测试文件 | 构建上下文、对应 Dockerfile 的 ignore 规则 |
| 拉取镜像超时 | Registry 连接、代理、认证服务;与源码编译错误分开处理 |
docker exec 是进入已有运行容器启动一个额外进程。它适合读取配置和诊断,不应把现场 apt install 当作修复完成;已经确认的依赖修改要回写 Dockerfile,才能在下次重新创建时保留。
升级与回滚:先把退路准备好
本项目的 local 是现役引用,hardening-check 是候选引用,二者只是约定的标签名,不自带“稳定”或“已验收”的属性。即使两个标签现在指向同一个 Image ID,以后也可以再次分开。
安全更新按这个顺序做:
- 记录源码版本、构建参数、实际容器 Image ID、端口、挂载与资源限制;明确会中断哪些会话。
- 为旧的确切 Image ID 留一个不覆盖的回退标签;必要时额外保存镜像归档。标签不是异机备份。
- 用独立候选标签构建,在临时 loopback 端口上验收完整镜像;源码挂载测试与无挂载镜像测试分开记。
- 候选通过后再安排切换,把现役引用指向已验收的精确镜像,然后只重新创建目标服务。
- 切换后核对容器实际 ID、健康状态、接口功能、安全边界;测试失败则恢复旧引用并重新创建服务。
- 数据迁移另做备份和兼容性设计。退回程序镜像,不能自动撤销数据库格式变化,也不能找回已结束的仿真会话。
不要只移动标签、执行 restart,然后宣布升级完成。 旧容器仍然关联创建它时的镜像。也不要在候选验收阶段不经确认就对现役服务执行 compose up。
本文不固化一条带过期镜像 ID 的“万能回滚命令”。具体候选 ID、旧镜像回退标签和本次切换结果,以仓库 docker/README.md 的部署记录为准;执行前还要与本机实际 inspect 对照。
清理之前,先知道会删掉什么
空间不足先看 docker system df、docker image ls、docker ps -a 和 docker volume ls。旧镜像、停止的学习容器、构建缓存和持久卷不是同一类东西。
- 删除容器会删除它的普通可写层,重要文件先导出。
docker compose down会移除项目容器;加--volumes/-v还可能删除项目声明的命名卷及附着的匿名卷,external 卷另有规则。Compose down。- 带
-a的镜像清理可能删掉没有被容器引用、却专门留着回退的带标签镜像。“暂未运行”不等于“没用”。 - prune 会按对应对象和选项扩大删除范围。先列出、确认归属、核对备份,再删除确切对象;不要把全局 prune 当作日常故障修复按钮。Docker 官方:清理未使用对象。
运行(windows 下)
原文中 laster 是拼写错误,不能代替 latest;也不需要普遍把 /bin/bash 写成 //bin/bash。
在 Docker Desktop 已切换到 Linux 容器且正常运行的前提下,PowerShell / Windows Terminal 可使用:
docker run --rm -it ubuntu:22.04 bash |
早年的 Git Bash / mintty 可能出现 the input device is not a TTY,winpty 是那类终端适配问题的历史处理方式,不是所有 Windows Docker 命令都要加的前缀。遇到这个问题,先用 PowerShell 或 Windows Terminal 验证,再区分终端交互与镜像本身的问题。
参考
正文中的官方链接用于核对现在的 Docker 行为;下面保留最初入门时读过的材料,旧文中的安装命令和默认行为需要结合官方文档再确认:
- Docker 入门教程——阮一峰(2018)。
- 《Docker 从入门到实践》,杨宝华等。
下次给环境增加内容时,先问自己四句:它属于哪个服务?构建输入写在哪里?状态保存在哪里?怎样证明新版本能用且可以退回去? 能答清楚这四句,比记住更多 docker run 参数更有用。
延伸阅读:容器之下的板卡系统怎样构建
高通板卡 Yocto 元数据与部署查询手册 整理 layer、recipe、machine、distro 和镜像的关系,并区分应用容器、目标 BSP 与交叉 SDK;迁移到高通板时,容器不能代替驱动、设备树和固件的匹配检查。