Docker
Docker 镜像保存程序、文件和默认启动方式。容器基于镜像创建,在隔离的进程、网络和挂载环境中运行;需要保留的数据通过 volume 或 bind mount 放到容器可写层之外。Linux 容器共享宿主内核,跨 CPU 架构和内核能力仍有兼容要求。
Docker CLI 通过 socket 或远程 API 请求 Engine 创建和管理这些对象。Compose 则把多个服务的镜像、端口、网络、挂载和启动依赖写进 YAML。开发机、CI 与服务器可以使用相同模型,但操作前要确认 context 指向哪台机器。
| 当前任务 | 进入位置 | 主要内容 |
|---|---|---|
| Linux 安装、daemon、权限、代理、日志和升级 | Engine 控制链 | Client/Server、systemd、配置、存储和回退记录 |
| 多服务依赖、变量、网络、卷和健康检查 | Compose 项目 | 配置解析、服务发现、就绪和重建 |
| 磁盘告警、旧数据、孤儿对象与构建缓存 | 资源生命周期与安全清理 | 引用、owner、备份恢复、删除范围与重建结果 |
| Windows/macOS 桌面虚拟化与许可 | Docker Desktop | 独立产品的 VM、context、设置与许可核验 |
CLI、Engine 与运行组件
CLI 可以连接本机或远程 Engine。下面的 systemctl 命令在运行 rootful Engine 的 Linux 主机执行;桌面客户端连接 VM 时应查看对应 VM/Engine 状态:
docker version
docker info
systemctl is-enabled docker
systemctl is-active docker
sudo journalctl -u docker.service -n 100 --no-pagerdocker version 应同时包含 Client 与 Server;systemctl is-active 预期输出 active;docker info 应记录当前存储后端、数据根目录、日志驱动、cgroup 驱动和安全选项。若 CLI 版本正常而 Server 连接失败,问题位于 socket、daemon 或 systemd,不要先重装客户端。
安装 Linux Engine
包管理器安装能保留仓库签名、软件包版本和升级路径。便利脚本适合一次性实验机,不适合作为团队长期基线,因为脚本内容会变化,也很难在变更前审查实际安装版本。Linux 发行版自带的 docker.io 与 Docker 官方仓库的 docker-ce 由不同维护方构建,团队必须选定一条来源,不能混装后再把版本冲突归因于 Docker。
Ubuntu 与 Debian
先读取发行版身份并盘点冲突包:
. /etc/os-release
printf 'id=%s version=%s codename=%s arch=%s\n' "$ID" "$VERSION_ID" "${VERSION_CODENAME:-unknown}" "$(dpkg --print-architecture)"
dpkg -l | grep -E 'docker|containerd|runc' || true下面的脚本只接受原生 Ubuntu 或 Debian,并根据发行版选择官方仓库。衍生发行版要先确认它映射到哪个受支持的上游版本,再按上游官方页面单独生成配置,不能直接把自己的 codename 填进去。
安装者需 sudo 权限。先确认专用主机没有需要保留的其他容器运行时,按 Ubuntu 或 Debian 官方支持范围选择发行版。
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
case "$ID" in
ubuntu) repo_os=ubuntu; repo_suite="${UBUNTU_CODENAME:-$VERSION_CODENAME}" ;;
debian) repo_os=debian; repo_suite="$VERSION_CODENAME" ;;
*) printf 'unsupported distribution: %s\n' "$ID" >&2; exit 23 ;;
esac
sudo curl -fsSL "https://download.docker.com/linux/$repo_os/gpg" \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/$repo_os
Suites: $repo_suite
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
apt list --all-versions docker-ce团队机器应从列表中选择已经过兼容验证的完整版本字符串,而不是每台机器各自安装当日最新版本:
VERSION_STRING='<reviewed-version-from-apt-list>'
case "$VERSION_STRING" in
''|*'<'*|*'>'*) printf 'set an exact reviewed version\n' >&2; exit 23 ;;
esac
sudo apt install -y \
docker-ce="$VERSION_STRING" \
docker-ce-cli="$VERSION_STRING" \
containerd.io docker-buildx-plugin docker-compose-plugincontainerd.io 是 Docker 仓库提供的依赖组合。宿主机原先独立维护 containerd 或 runc 时,先评估其他工作负载是否依赖它们,再按目标发行版说明处理冲突,不能直接卸载共享运行时。
上面的命令只固定了 Engine 与 CLI;containerd.io、Buildx 和 Compose 仍由当时仓库解析。团队镜像或离线基线还要记录 apt-cache policy containerd.io docker-buildx-plugin docker-compose-plugin 与安装事务,或者给全部包使用经过验证的精确版本,避免同一 Engine 版本在不同日期解析出不同插件组合。
Fedora、CentOS 与兼容 RPM 系统
先确认发行版和仓库:
. /etc/os-release
printf 'id=%s version=%s arch=%s\n' "$ID" "$VERSION_ID" "$(uname -m)"
sudo dnf repolistCentOS 安装路径示例:
sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
dnf list docker-ce --showduplicates | sort -r上述是使用 DNF4 的 CentOS 路径。Fedora 的 DNF5 添加仓库语法有差异,按 Fedora 安装说明执行,不能只替换 URL;CentOS 的支持版本与包选择见 CentOS 安装说明。安装时同样选择列表中的完整版本:
VERSION_STRING='<reviewed-version-from-dnf-list>'
case "$VERSION_STRING" in
''|*'<'*|*'>'*) printf 'set an exact reviewed version\n' >&2; exit 23 ;;
esac
sudo dnf install -y \
"docker-ce-$VERSION_STRING" \
"docker-ce-cli-$VERSION_STRING" \
containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker首次接受仓库 GPG key 时要由自动化或人工核对当前官方指纹,不能看到“Docker”名称就直接确认。镜像站、代理缓存和离线包也要保存上游 URL、包摘要、签名验证结果和同步时间。
确认服务可用
sudo systemctl status docker --no-pager
sudo docker version
sudo docker info --format 'server={{.ServerVersion}} root={{.DockerRootDir}} driver={{.Driver}} logging={{.LoggingDriver}} cgroup={{.CgroupDriver}}'
sudo docker run --rm hello-world欢迎输出确认该镜像完成了启动与退出。一个持续运行的 HTTP 服务还需要监听、端口发布和可读文件,下面直接建立这样的服务。
构建镜像并访问容器
文件如何进入镜像
在已安装 Docker 的 Linux 开发机,以具备 Engine 权限的账号创建专用目录。Windows/macOS 可通过 Docker Desktop提供 Linux Engine;挂载路径与宿主权限不能照搬 Linux。
mkdir docker-web-lab
cd docker-web-lab
printf '<h1>docker-web-one</h1>\n' > index.html
cat > Dockerfile <<'EOF'
FROM alpine:3.22
RUN apk add --no-cache busybox-extras
WORKDIR /site
COPY index.html /site/index.html
USER 10001:10001
EXPOSE 8080
CMD ["httpd", "-f", "-p", "8080", "-h", "/site"]
EOF
printf '*\n!Dockerfile\n!index.html\n' > .dockerignore
docker build -t local/docker-web-lab:1 .
docker image inspect local/docker-web-lab:1 --format 'user={{.Config.User}} cmd={{json .Config.Cmd}}'预期镜像用户为 10001:10001,CMD 为前台 httpd。构建时用 root 安装提供 httpd 的 busybox-extras,运行时切换为普通 UID。FROM 选择基础文件系统,COPY 从构建上下文取文件,WORKDIR 设置工作目录。最后的 . 指定构建上下文,不只是 Dockerfile 所在位置。Dockerfile 参考
.dockerignore 在发送上下文前排除无关内容。把密码 COPY 进去再在后续 RUN 删除,旧层仍可能保留内容;构建机密应通过 BuildKit secret mount 提供,且命令不能把它写回产物或日志。构建机密
镜像由可共享的只读层及配置组成,容器另有自己的可写层。采用 OverlayFS 等写时复制后端时,修改底层文件可能触发 copy-up,删除则通过相应元数据隐藏下层文件。大文件频繁改写适合专用数据挂载,不宜依赖容器可写层。
监听、发布与首次请求
确认本机 18088 端口及 docker-web-lab 名称未被使用后启动:
docker run -d --name docker-web-lab \
--read-only --cap-drop ALL --security-opt no-new-privileges \
--memory 64m --cpus 0.5 --pids-limit 64 \
--log-driver local --log-opt max-size=10m --log-opt max-file=3 \
-p 127.0.0.1:18088:8080 local/docker-web-lab:1
curl -q --noproxy '*' --fail-with-body --silent --show-error --max-time 5 \
http://127.0.0.1:18088/
docker exec docker-web-lab id响应为 docker-web-one 的 HTML,容器内 UID 为 10001。-p 把宿主回环端口转到容器 8080,EXPOSE 不会自行发布端口;容器应用必须监听容器网卡可达的地址,不能只监听容器自己的 127.0.0.1。端口发布
宿主 curl → 127.0.0.1:18088 → 发布规则 → 容器网卡:8080 → httpd
容器自身 curl → 127.0.0.1:8080 ─────────────────────→ httpd容器共享内核,namespace 提供进程、网络和挂载等视图隔离,cgroup 记录并限制资源;capabilities、seccomp 等进一步约束系统能力。以普通 UID 运行缩小了应用权限,但宿主操作者能调用 rootful Docker API 时依然具备很高权限。
docker logs docker-web-lab 只显示程序写向容器 stdout/stderr 的内容,空日志不代表没有请求。观察指定对象:
docker ps -a --filter name=docker-web-lab
docker inspect docker-web-lab --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}'
docker top docker-web-lab
docker stats --no-stream docker-web-lab
docker port docker-web-lab容器秒退时先查看退出状态、CMD 与日志。前台主进程退出就结束容器,在命令后加 & 把服务后台化容易导致这一问题。需要 shell 串联启动逻辑时,最终使用 exec 把信号交给应用;多个子进程还需考虑回收僵尸进程。
restart policy 响应的是容器退出:on-failure 关注非零退出,unless-stopped 等策略还会影响 daemon 重启后的恢复。普通 healthcheck 变成 unhealthy 并不会自动触发它。应用卡死但进程仍活着时,应先观察日志和探针,再由明确的运行管理机制摘流或重建,避免无限重启放大依赖故障。
端口不通时沿连接路径检查:容器是否存活、内部是否监听、宿主是否发布正确地址,最后才查外部路由与防火墙。Docker 发布流量可能绕过某些 ufw 规则,不能把主机防火墙默认策略当作容器端口访问控制;按目标 Engine 的防火墙后端核对,并从独立客户端验证实际暴露范围。
重建与挂载的差异
修改宿主的 index.html 不会改变已经 COPY 进镜像的文件。重新 build 产生新的镜像配置或层,旧容器继续引用原镜像;要使用新内容必须创建新容器。docker restart 也不会切换镜像。
开发时可以将当前目录的静态文件只读挂入另一容器:
docker run -d --name docker-web-bind \
--read-only --cap-drop ALL \
--mount "type=bind,src=$PWD/index.html,dst=/site/index.html,readonly" \
-p 127.0.0.1:18089:8080 local/docker-web-lab:1
printf '<h1>bind-two</h1>\n' > index.html
curl -q --noproxy '*' --fail-with-body --silent --show-error --max-time 5 \
http://127.0.0.1:18089/新容器响应为 bind-two,原 18088 端口仍为 docker-web-one。挂载遮住镜像对应路径,文件来自宿主;读权限、SELinux 标签和路径存在性仍需满足。生产镜像通常将静态文件固化,只把确需变化的配置或数据挂出。
docker stop docker-web-lab docker-web-bind
docker rm docker-web-lab docker-web-bind
printf '<h1>docker-web-one</h1>\n' > index.html只移除刚创建的两个容器,镜像与工程文件保留,后面的 Compose 继续使用它们。
Dockerfile 缓存与多阶段构建
COPY 输入变化会使依赖它的后续步骤重建。把依赖描述文件先复制、下载依赖,再复制经常变化的源码,可以减少不必要下载;但锁文件、插件、平台和构建参数必须真实参与缓存判断。需要检查回源行为时用 --no-cache 重建,更新基础镜像则另考虑 --pull。构建缓存
多阶段构建用一个阶段编译,再用 COPY --from=build 只复制运行产物。编译器、测试依赖和缓存留在构建阶段;这不会自动消除产物内的机密,也不会保证构建阶段非 root。完整 Java Dockerfile、JVM 配额与健康探针见 Spring Boot 部署运维。
tag 是可移动名称,本地 image ID 是镜像配置摘要,registry digest 可能指向单平台 manifest 或多平台 index。跨机器交付要区分这些对象;固定 digest 后还需要匹配目标 CPU 架构,不能拿本机 image ID 冒充远程可拉取引用。
用 systemd 管理生命周期与启动失败
Docker Engine 的安装包通常提供 docker.service 与 docker.socket。开发机是否开机自启要由用途决定:个人实验机可按需启动;共享开发机与本地 CI 节点需要显式启用,并监控启动失败。
sudo systemctl enable --now docker
systemctl is-enabled docker
systemctl is-active docker
sudo systemctl show docker -p FragmentPath -p DropInPaths -p ExecStart -p ActiveState不要直接编辑 /usr/lib/systemd/system/docker.service 或 /lib/systemd/system/docker.service,包升级会覆盖它。代理、资源限制或受控参数使用 drop-in:
sudo systemctl edit docker配置变化后的顺序是:验证配置、daemon-reload、重启、检查状态和日志。不要把 restart 与后面的验证用 ; 串联,否则重启失败后脚本仍可能继续:
sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl is-active --quiet docker
sudo journalctl -u docker.service -n 100 --no-pager
docker versionStart request repeated too quickly 只是 systemd 停止重试的结果,根因通常在更早的 daemon 日志,例如 JSON 无效、同一选项同时出现在 unit 参数和 daemon.json、数据目录不可访问、存储后端不兼容或防火墙规则失败。
daemon.json 每个字段都改变宿主机行为
Linux rootful daemon 默认读取 /etc/docker/daemon.json。先创建备份目录并只复制当前文件,不直接覆盖未知配置:
sudo install -d -m 0750 /etc/docker/backup
if sudo test -f /etc/docker/daemon.json; then
sudo cp -a /etc/docker/daemon.json "/etc/docker/backup/daemon.json.$(date +%Y%m%d%H%M%S)"
fi一份适合共享开发机讨论的基线如下,实际地址与并发数要由容量测试决定:
{
"data-root": "/var/lib/docker",
"log-driver": "local",
"log-opts": {
"max-size": "20m",
"max-file": "5"
},
"live-restore": true,
"max-concurrent-downloads": 4,
"max-concurrent-uploads": 2,
"default-address-pools": [
{ "base": "172.30.0.0/16", "size": 24 }
]
}字段影响不能停留在“建议值”:
data-root 决定镜像、容器元数据和部分 daemon 状态落在哪个文件系统。迁移必须停 daemon、保持所有权与扩展属性、验证挂载先于 Docker 启动,并保留旧目录回退。log-driver 是新建容器的默认日志实现。local 自带轮转且磁盘效率更高;json-file 默认不轮转,长期高日志量可能写满磁盘。log-opts 的值必须写成字符串。改变 daemon 默认值不会改造已经存在的容器,旧容器要重建后才采用新策略。
live-restore 可在满足版本和配置条件时让 daemon 暂时不可用期间的容器继续运行,已有网络通信也可能继续;管理 API 不可用,日志管道满后还可能阻塞容器输出。它不提供宿主故障恢复。Live restore
下载上传并发会改变代理、registry 与磁盘压力。低带宽或高延迟网络不一定适合更高并发。default-address-pools 用来减少新建 bridge 网络与企业网段冲突;改变后不会自动重编已有网络。
在重启前验证 JSON 与 daemon 选项。目标版本支持时使用 daemon 自带验证;至少还要做严格 JSON 解析:
python3 -m json.tool /etc/docker/daemon.json >/dev/null
sudo dockerd --validate --config-file=/etc/docker/daemon.json反向实验应在临时文件中完成,不污染真实配置:
tmp="$(mktemp)"
trap 'rm -f -- "$tmp"' EXIT
printf '{"log-driver":"local",}\n' > "$tmp"
set +e
sudo dockerd --validate --config-file="$tmp" >"$tmp.out" 2>&1
rc=$?
set -e
test "$rc" -ne 0
grep -Ei 'invalid|error|character' "$tmp.out"
printf 'EXPECTED_FAILURE rc=%s\n' "$rc"
rm -f -- "$tmp.out"预期为非零退出码和 JSON 解析证据。若目标版本没有 --validate,使用与生产相同版本的隔离虚拟机做启动验证,不能在共享 daemon 上试错。
权限模型:docker 组不是普通开发组
rootful daemon 的 Unix socket 可以创建特权容器、挂载宿主机目录、访问设备和修改网络。能调用它的用户基本拥有 root 级能力。因此有三种常见模型:
受控 sudo
个人或少量管理员通过 sudo docker ... 操作,权限最清晰,适合共享宿主机。不要为“命令短一点”给整个研发组开放任意 Docker 命令的免密 sudo;即便只允许 docker run,用户也能挂载宿主机根目录获得高权限。
docker 组
sudo usermod -aG docker "$USER"重新登录后可免 sudo 调用 daemon,但这是一项主机特权授权。授权应进入资产与权限审计,限定专用开发机,离职或角色变化时移除。不要把公开 PR 的 CI Runner、多人教学账号和生产运维账号混在同一组。
如果此前用 sudo 运行 CLI 导致个人配置归 root:
sudo chown -R "$USER":"$USER" "$HOME/.docker"
sudo chmod -R g+rwx "$HOME/.docker"修改前先确认 $HOME/.docker 是当前用户目录且没有符号链接指向其他位置。
Rootless mode
Rootless 让 daemon 与容器都运行在用户 namespace 内,降低 rootful daemon 被利用时的宿主机风险。它适合个人开发环境和不需要特权设备的构建任务;低端口、cgroup、UID/GID 子区间、网络与存储要逐项验证。Rootless mode
先确认当前用户拥有至少 65536 个连续 subordinate UID/GID,并判断系统级 daemon 是否仍在运行:
id -u
grep -E "^${USER}:" /etc/subuid /etc/subgid
command -v newuidmap
command -v newgidmap
systemctl is-active docker 2>/dev/null || true缺少 newuidmap/newgidmap 时安装发行版的 uidmap 包;缺少子区间时由主机管理员分配,不能让多个账户重叠。dockerd-rootless-setuptool.sh install 默认会在检测到系统级 Docker daemon 时拒绝安装;个人专用机可先停用 rootful daemon,共享机确需并存时才在风险评估后使用工具提示的 --force,并始终用 context 区分两套 socket。安装 docker-ce-rootless-extras 后,以普通用户执行:
dockerd-rootless-setuptool.sh install
systemctl --user enable --now docker
sudo loginctl enable-linger "$USER"
docker context use rootless
docker infodocker info 的 Security Options 应出现 rootless。部分工具还需要:
export DOCKER_HOST="unix:///run/user/$(id -u)/docker.sock"不要在同一个 Shell 同时设置 DOCKER_HOST 又随意切 context。低于 1024 的端口、设备映射、某些 overlay 网络和资源控制可能需要额外系统配置或无法等价实现。团队支持 Rootless 前,应使用真实 Compose、bind mount、调试器、数据库 volume 与 CI 构建做兼容矩阵。
用正反实验验证隔离、资源与持久化
下面的自包含脚本只创建唯一标签对象。无论正向断言、反向断言还是镜像拉取在哪一步失败,退出钩子都只清理标签与随机名称同时匹配的容器和 volume:
set -eu
lab="engine-lab-$(date +%s)"
volume="${lab}-data"
err="$(mktemp)"
cleanup() {
container_label="$(docker inspect "$lab" --format '{{ index .Config.Labels "tool-efficiency.lab" }}' 2>/dev/null || true)"
if [ "$container_label" = "$lab" ]; then
docker rm -f "$lab" >/dev/null
fi
volume_label="$(docker volume inspect "$volume" --format '{{ index .Labels "tool-efficiency.lab" }}' 2>/dev/null || true)"
if [ "$volume_label" = "$lab" ]; then
docker volume rm "$volume" >/dev/null
fi
rm -f -- "$err"
}
trap cleanup EXIT
docker volume create --label tool-efficiency.lab="$lab" "$volume" >/dev/null
docker run --name "$lab" \
--label tool-efficiency.lab="$lab" \
--memory 64m --cpus 0.25 \
--mount "type=volume,src=$volume,dst=/data" \
alpine sh -c 'printf ENGINE_OK >/data/result && cat /data/result'
test "$(docker inspect "$lab" --format '{{.State.ExitCode}}')" = 0
test "$(docker run --rm --mount "type=volume,src=$volume,dst=/data,readonly" alpine cat /data/result)" = ENGINE_OK
limits="$(docker inspect "$lab" --format 'memory={{.HostConfig.Memory}} nano_cpus={{.HostConfig.NanoCpus}}')"
test "$limits" = 'memory=67108864 nano_cpus=250000000'
set +e
docker run --rm --mount "type=volume,src=$volume,dst=/data,readonly" \
alpine sh -c 'printf SHOULD_FAIL >/data/result' 2>"$err"
rc=$?
set -e
test "$rc" -ne 0
grep -Ei 'read-only|permission denied' "$err"
printf 'VERIFY_OK lab=%s %s negative_rc=%s\n' "$lab" "$limits" "$rc"预期输出 VERIFY_OK,内存为 67108864,CPU 配额为 250000000,第二个容器能从同一 volume 读到 ENGINE_OK,只读写入返回非零。这证明容器可写层与 volume 持久状态是两套边界,也证明只读挂载门禁实际生效。
共享主机上的清理脚本应按项目 label、明确对象名和保留期执行。docker system prune 作用于整个 daemon,不应成为单个项目的 reset 命令。
日志必须有容量上限,也要知道丢日志的代价
daemon 日志与容器日志是两条链:
sudo journalctl -u docker.service --since '-30 min' --no-pager
: "${CONTAINER_NAME:?set CONTAINER_NAME to a real container name}"
docker logs --tail 100 "$CONTAINER_NAME"
docker inspect "$CONTAINER_NAME" --format '{{json .HostConfig.LogConfig}}'
docker info --format '{{.LoggingDriver}}'daemon 默认 json-file 时可能没有轮转。共享开发机可以选择 local,或为 json-file 配置 max-size 与 max-file。修改只影响新建容器,必须通过重建探针验证:
docker run --rm alpine echo LOG_PROBE
docker info --format 'default={{.LoggingDriver}}'阻塞日志模式能保留背压语义,但日志端异常可能拖慢应用;非阻塞模式保护应用线程,却会在缓冲区满时丢新日志。故障诊断与审计要求高的任务不能在没有丢弃指标和外部采集的情况下盲目改成非阻塞。无论使用哪种驱动,都要监控数据根文件系统的字节和 inode。
存储后端不是一个永远固定的 overlay2
镜像只读层与容器可写层由 Engine 的镜像存储后端管理,数据库等持久数据应放 volume 或经过设计的 bind mount。把数据库数据写进容器可写层会承担 copy-on-write 开销,删容器时也会丢失。
检查真实状态:
docker info --format 'driver={{.Driver}} status={{json .DriverStatus}} root={{.DockerRootDir}}'
findmnt -T "$(docker info --format '{{.DockerRootDir}}')"
df -hT "$(docker info --format '{{.DockerRootDir}}')"
df -ih "$(docker info --format '{{.DockerRootDir}}')"Docker Engine 29.0 及更高版本的全新安装默认使用 containerd image store 与 overlayfs snapshotter;从旧版本升级的机器继续使用经典 overlay2,除非启用新后端。切换两类后端时,另一后端创建的镜像与容器会暂时不可见,但数据仍占磁盘。containerd image store 还会同时保存压缩层和解压层,容量通常高于经典存储;它目前也不能与 userns-remap 同时使用。迁移前先核对容量、兼容性和恢复方式。containerd image store
经典 overlay2 还依赖宿主文件系统能力,例如 XFS 需要合适的 d_type。更换 storage driver 或 backing filesystem 前应:停止写入、导出自建镜像或推送到 registry、对 volume 做应用一致性备份、记录现有 docker info、在同版本测试机验证,再安排维护窗口。绝不能手工修改 /var/lib/docker 内的层目录。
迁移 data-root 前先识别两套数据根
使用经典 overlay2 时,镜像、容器与 volume 主要位于 Docker data-root。使用 containerd image store 时,镜像内容与容器 snapshot 默认位于 /var/lib/containerd,volume、配置等其他状态仍在 /var/lib/docker;daemon.json 的 data-root 不会移动前者。先识别后端和两个文件系统:
docker info --format 'root={{.DockerRootDir}} driver={{.Driver}} status={{json .DriverStatus}}'
sudo du -sh /var/lib/docker /var/lib/containerd 2>/dev/null || true
findmnt -T /var/lib/docker
findmnt -T /var/lib/containerdcontainerd 路径要通过 /etc/containerd/config.toml 的 root 单独规划,并与 Docker 数据目录使用不同的专用目录。它涉及 containerd 服务和 snapshot 元数据,不应把下面只针对 Docker data-root 的复制步骤直接套上去;先在同版本隔离节点验证服务停止顺序、配置和恢复。
经典数据根的回退链
迁移前确认新路径是独立挂载且容量、inode、权限、SELinux/AppArmor 策略满足要求。先摘除入口流量、暂停消费者与定时任务,再按应用流程停止所有相关容器并确认没有写入;启用 live-restore 时仅停止 daemon 可能保留容器进程。记录容器原有重启策略,迁移验证期间禁止它们自动恢复业务写入。下面只在已停业务的经典存储专用主机执行,任一检查失败都应停止,不继续复制或切换:
(
set -eu
old=/var/lib/docker
new=/srv/docker-data
test "$old" = /var/lib/docker
test "$new" = /srv/docker-data
sudo install -d -m 0710 "$new"
test "$(findmnt -n -o TARGET -T "$new")" = "$new"
sudo systemctl stop docker docker.socket
test "$(systemctl is-active docker 2>/dev/null || true)" != active
sudo rsync -aHAXx --numeric-ids "$old"/ "$new"/
sudo du -sb "$old" "$new"
)findmnt 断言要求新目录本身就是挂载点,避免数据意外写回根分区。du 只能发现数量级异常,不能证明扩展属性、稀疏文件和运行状态一致。然后设置 data-root,验证 JSON,启动 daemon,逐项检查镜像、容器、volume、网络和只读项目探针。入口、消费者和后台任务保持停写;daemon 启动时可能按重启策略拉起容器,必须再次核对,不能把“尚未开放 HTTP 入口”当作没有写入。
回退方法取决于新目录是否已经接收业务写入:
尚未产生新增写入:确认全部写入方仍停止,停止相关容器与 daemon,恢复原配置并指向旧目录;启动后核验原数据、权限与应用读取结果,再恢复原重启策略和业务入口。
已经恢复写入,或无法证明未写入:旧目录停留在迁移时刻,直接切回会遗漏之后的订单、任务和数据库变化。先重新停写,保留新旧目录及日志,以新目录上的当前业务数据为恢复来源。按数据库或中间件自身的备份恢复流程,把包含新增记录的一致备份恢复到独立目标;核对业务记录、增量位置和应用版本兼容后,再安排切换。不要把运行中的 Docker 目录反向 rsync,也不要用旧目录覆盖新目录。
验证新目录后才能恢复业务写入;一旦开放,就进入第二条恢复路径。恢复演练应在迁移后写入一条可唯一识别的测试记录,并在恢复目标中查回它,同时检查原有数据。新旧目录都保留到恢复验收与保留窗口结束,不能因服务启动成功立即删除。若复制期间任何相关容器仍在写入,应重新取得一致副本。
中国大陆与内网使用方式
安装源与镜像仓库是不同入口。软件源不可达时使用组织批准且保留签名校验的包镜像或离线包;公共镜像仓库不可达时,可从企业 Harbor、云厂商私有仓库或经过审核的缓存代理获取。不能假定任意加速地址会代理所有 registry、所有命名空间和认证请求。
有镜像推送权限时,为本次镜像增加目标仓库标签,再通过凭据助手或受保护的标准输入登录。下面变量须由实际仓库配置提供;操作会上传镜像,先确认仓库与 namespace 已获授权:
: "${REGISTRY:?set the approved registry host}"
: "${REGISTRY_USER:?set the authorized registry user}"
docker login "$REGISTRY" --username "$REGISTRY_USER"
docker tag local/docker-web-lab:1 "$REGISTRY/your-project/web:1"
docker push "$REGISTRY/your-project/web:1"在接收主机使用同一受控来源 pull,检查目标平台并运行业务请求。登录并不授予额外仓库权限,401/403 仍需核对账号及推送路径;不要在命令行直接填写密码。Registry 登录
完全离线时,在有镜像的机器保存归档和摘要:
mkdir docker-image-delivery
docker image save -o docker-image-delivery/web.tar local/docker-web-lab:1
(cd docker-image-delivery && sha256sum web.tar > SHA256SUMS)把整个交付目录传到目标机,进入该目录后执行:
sha256sum -c SHA256SUMS
docker image load -i web.tar
docker image inspect local/docker-web-lab:1 --format 'os={{.Os}} arch={{.Architecture}} user={{.Config.User}}'校验应为 OK,导入后仍需核对平台与首次请求。save/load 保存镜像,不包含业务 volume、宿主配置或容器运行参数;export/import 也不能等价替代完整镜像交付。镜像保存
daemon、构建与容器代理
daemon 拉镜像的代理可写进 daemon.json,也可以通过 systemd drop-in 提供:
[Service]
Environment="HTTP_PROXY=http://proxy.example.invalid:3128"
Environment="HTTPS_PROXY=http://proxy.example.invalid:3128"
Environment="NO_PROXY=localhost,127.0.0.1,.internal.example.invalid"这里使用保留域名,实际值由企业配置系统注入。修改后:
sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl show --property=Environment docker
docker pull alpinesystemctl show 可能暴露代理 URL 中的凭证,因此生产代理优先使用不含明文用户密码的认证设计,日志与工单也要脱敏。daemon 代理只解决拉取和访问 registry;Dockerfile 构建阶段与运行中容器访问外网,还要分别验证自己的代理环境。
私有 registry 使用企业 CA 时,把只含公钥的 CA 放进与 registry 端点完全一致的目录,CA 扩展名必须是 .crt,不能误写成代表客户端证书的 .cert:
registry='registry.example.invalid:5000'
ca_file='./enterprise-registry-ca.crt'
test -f "$ca_file"
openssl x509 -in "$ca_file" -noout -subject -issuer -fingerprint -sha256
sudo install -d -m 0755 "/etc/docker/certs.d/$registry"
sudo install -m 0444 "$ca_file" "/etc/docker/certs.d/$registry/ca.crt"
sudo systemctl restart docker
http_code="$(curl -q --noproxy '*' --cacert "$ca_file" --silent --show-error --max-time 10 -o /dev/null -w '%{http_code}' "https://$registry/v2/")" || exit 23
case "$http_code" in
200|401) printf 'TLS_OK http=%s\n' "$http_code" ;;
*) printf 'registry probe failed: http=%s\n' "$http_code" >&2; exit 23 ;;
esac示例使用保留域名,必须先替换成获准直连的真实端点,执行前核对证书指纹。curl 显式使用同一 CA,安装到 Docker 目录的 CA 不会自动进入 curl 信任库。受保护 Registry 可以返回 401;还需检查其认证挑战及真实 pull,不能仅凭状态码认定已连接到正确仓库。Docker Registry 证书
必须经过代理时按批准路径另设对照,不沿用直连结论。失败时保留 daemon 日志和 TLS 错误,不用 insecure-registries 绕过校验。业务容器访问企业 HTTPS 还要由容器内的系统或语言运行时信任 CA。
需要比较网络层时,按各层实际使用的代理、CA 和目标执行。下面是允许直连公共目标的测试机示例:
code=$(curl -q --noproxy '*' --silent --show-error --max-time 10 -o /dev/null -w '%{http_code}' https://registry-1.docker.io/v2/) || exit 23
test "$code" = 401
docker pull alpine:3.22
docker run --rm alpine:3.22 wget -qO- https://example.com >/dev/null
printf 'FROM alpine:3.22\nRUN wget -qO- https://example.com >/dev/null\n' | docker build --no-cache -t engine-net-probe -
docker image rm engine-net-probe宿主失败先查 DNS、路由、代理和 CA;宿主成功但 pull 失败查 daemon;pull 成功而容器失败查镜像信任库与容器代理;只有 build 失败则查 BuildKit 与构建参数。
远程 API、凭证与 socket 必须按 root 权限治理
不要把 dockerd 监听到 0.0.0.0:2375。未认证 Docker API 相当于远程 root。确需远程管理时,使用双向 TLS、主机防火墙、独立 CA、短生命周期客户端证书和访问审计,并让 context 指向受控端点。更常见的研发方案是通过 SSH context 访问专用主机,仍需限制远端账号权限。
把 /var/run/docker.sock 挂进 CI 容器或开发容器,同样把宿主机控制权交给该容器。所谓 Docker-outside-of-Docker 不是隔离,只是把客户端装进容器。运行不可信仓库代码时使用临时虚拟机、Rootless daemon 或具备明确隔离边界的构建服务,不要让公开 PR 接触共享 socket。
Registry 凭证位于用户 Docker 配置或凭证助手中。不要以 root 登录 registry 后把 /root/.docker/config.json 复制给团队,也不要把长期 token 烘焙进镜像。使用最小权限机器人账号、短期 token 和操作系统凭证存储;脚本退出与设备退役时撤销服务端凭证。
升级与回退
升级前保存:
docker version
docker info
sudo systemctl cat docker
if sudo test -f /etc/docker/daemon.json; then
sudo cp -a /etc/docker/daemon.json "./daemon.json.before-upgrade"
fi
docker ps --format '{{.Names}}|{{.Image}}|{{.Status}}'
docker volume ls再阅读目标版本发行说明与弃用项,检查 API 兼容、cgroup、iptables/nftables、containerd image store、Compose/Buildx 插件和存储变化。代表性回归至少包含:拉取带摘要镜像、构建项目、创建 bridge 网络、端口访问、volume 重启持久化、日志轮转和 Rootless 路径。
共享开发机采用 canary:先升级一台具有相同内核、文件系统、代理和安全策略的机器,观察一个完整工作日或团队约定窗口,再滚动升级。包管理器要保留上一版软件包、完整依赖版本和可用仓库快照。APT 可先用 apt list --all-versions 确认旧版仍存在,再对 Engine、CLI 及经过验证的依赖执行精确版本安装;DNF 则先读取 dnf history info 和仓库版本列表。不要在故障主机上直接尝试 history undo:先停写、导出运行证据并在 canary 复现。
回退时不能只降 docker-ce,还要同步检查 CLI、containerd、Buildx、Compose、配置格式、API、网络规则和存储元数据是否发生不可逆变化。若升级同时启用了 containerd image store,先按后端切换方案恢复可见性,再判断是否降包;把“旧镜像重新出现”与“旧版 daemon 可以安全读取新元数据”当成两项不同验证。
出现升级后旧对象不可见时,先比较 docker info 的 Docker Root Dir、Driver 与 DriverStatus,再判断是否切换了 image store。不要立即清空数据目录。若新版本已迁移元数据或宿主防火墙规则,回退必须按对应发行说明执行,不能承诺“装回旧包就恢复”。
卸载与清理必须把软件包和数据分开
卸载 Engine 软件包通常不会自动删除 /var/lib/docker、/var/lib/containerd、volume 与自定义配置。这是防止误删数据的设计,不是卸载不完整。退役流程先列清:
docker context ls
docker system df -v
docker volume ls
sudo du -sh /var/lib/docker /var/lib/containerd 2>/dev/null || true
sudo find /etc/docker -maxdepth 3 -type f -print确认备份恢复、停止业务、撤销 registry 与远程 API 凭证,再由主机 owner 删除软件包。数据销毁必须使用资产系统确认的绝对路径,检查挂载点和设备,不能把官方卸载示例中的 rm -rf 原样放进自动脚本。对云盘或含密钥的设备,还要按组织数据销毁标准处理快照与备份。
把 Engine 管成团队共享能力
团队基线应记录发行版与内核、软件包来源、精确版本、daemon 配置、systemd drop-in、数据盘、存储后端、日志策略、代理与 CA、权限模型、升级 owner 和回退证据。配置进入受控仓库,但代理密码、registry token、客户端私钥和真实内网地址不进入公开文档。
容量判断至少观察数据根的字节与 inode、镜像和 Build Cache 增长、volume 增长、日志速率、pull 延迟、daemon 重启次数与容器启动失败。磁盘还有 30% 空闲却 inode 耗尽,同样会让容器创建失败;日志轮转减少磁盘风险,却会缩短本地排障窗口,需要外部日志系统承接长期证据。
共享开发主机还要明确租户边界。Docker Engine 默认不是强多租户沙箱,docker 组成员互相可见容器、镜像、网络和 volume 元数据,并可能获得宿主机控制权。不同信任域、客户数据或公开代码不能仅靠容器名隔离,应拆到不同 VM、不同 Rootless 用户或专用构建池。
Compose 项目、服务与网络
在前面的 docker-web-lab 目录创建 compose.yaml。web 使用已经构建的镜像,probe 是一次性客户端,检查同网络服务名访问:
name: docker-compose-lab
services:
web:
image: local/docker-web-lab:1
build: .
ports:
- "127.0.0.1:18088:8080"
read_only: true
cap_drop: [ALL]
healthcheck:
test: [CMD, wget, -q, -O, /dev/null, "http://127.0.0.1:8080/"]
interval: 2s
timeout: 2s
retries: 10
probe:
image: alpine:3.22
profiles: [check]
depends_on:
web:
condition: service_healthy
command: [wget, -qO-, "http://web:8080/"]这里无需单独声明默认网络,Compose 会创建 docker-compose-lab_default。服务名 web 由该网络内的 DNS 解析为容器地址;probe 使用容器端口 8080,宿主使用发布端口 18088。Compose 网络
docker context show
docker compose version
docker compose config --quiet
docker compose up -d --wait --wait-timeout 60 web
docker compose ps
docker compose run --rm probe
curl -q --noproxy '*' --fail-with-body --silent --show-error --max-time 5 \
http://127.0.0.1:18088/两条请求都应得到 docker-web-one。显式运行 probe 会激活该服务,无需另外启用 check profile。普通 up 不会无故启动可选探针。--wait 在存在健康检查时等待 healthy;检查命令必须在镜像中真实存在,不能使用未提供的脚本路径。
短形式 depends_on 只建立创建与启动顺序;service_healthy 等待依赖健康,service_completed_successfully 可用于一次性迁移任务。服务随后故障不会自动重新执行依赖排序。应用仍需要连接超时、有限重试和失败恢复。启动依赖
同一份 YAML 为什么会产生不同结果
Compose 先完成变量插值和多文件合并,再创建运行对象。Shell 环境、.env 和 --env-file 提供插值输入;service 的 environment/env_file 决定容器环境,两层不是同一件事。必要变量用 ${VAR:?message} 阻止空值,默认值用 ${VAR:-default} 明确表达。变量插值
docker compose config --services
docker compose config --volumes
docker compose config > compose.resolved.yaml解析文件可能含环境变量展开后的凭据,只存放在受控目录。现代 Compose 不需要顶层 version 字段;多个 -f 文件按顺序合并,挂载、端口等列表规则要查实际模型,不靠简单“后者覆盖前者”判断。
更改 YAML 后用 up 应用变化。restart 只重启已创建容器,不更新镜像或环境。若修改 COPY 输入,需要先 build,再 up;只有挂载文件内容变化时,才可能由应用自身重新读取,是否热加载取决于应用。
project 名称与持久数据
默认 project 名称可以来自目录,顶层 name 或 -p 可以固定它。更换目录或 -p 后,会创建另一组容器、网络和项目内 named volume。数据库看起来像空库时先比较 project 和挂载,不要立即重新初始化。
docker compose ls --all
docker ps -a --filter label=com.docker.compose.project --format '{{.Names}} {{.Label "com.docker.compose.project"}}'
docker volume ls --filter label=com.docker.compose.project
docker network inspect docker-compose-lab_default服务之间使用同一用户网络即可通信,不必逐一发布宿主端口。原生 Linux 的 host 网络共享宿主网络空间,-p 不再承担端口转发;rootless、Desktop 的网络实现另有差异。跨主机服务发现与调度需要其他网络/编排方案,单机 bridge 无法自行完成。
named volume 由 Engine 管理,适合持久数据;bind mount 暴露宿主明确路径,适合配置或开发文件;config/secret 表达配置与敏感输入,具体存储方式由平台决定。普通 Compose secret 不应被理解为自动提供完整的集中密钥系统。
external: true 的 volume 在项目外创建并维护,Compose 不负责创建或销毁它。挂载能让数据跨容器保存,备份仍需复制到独立位置并验证恢复;旧 volume 中已存在的数据也可能让镜像初始化参数不再生效。
停止、重建与退出
docker compose stop web
docker compose start --wait web
docker compose run --rm probe
docker compose downstop 保留容器,start 恢复它;down 删除项目容器和默认网络,通常保留 named volume。--volumes 还会删除本项目声明的非 external named volume 及附属匿名卷,不能在普通更新中随手添加。--remove-orphans 也要先确认孤儿容器是否属于需要删除的同一项目。
实验没有业务数据卷,退出后镜像和当前目录保留。单机 Compose 不提供自动跨主机调度、滚动发布或失败回滚;需要这些能力时使用专门编排机制,并保留应用级恢复验证。
清理从对象引用图开始,不从 prune 开始
Docker 对象分布在当前 daemon:容器引用镜像、network 和挂载,镜像层可被多个 tag 与容器共享,BuildKit 缓存属于具体 builder,volume 中的数据可能没有任何运行容器却仍是唯一副本。磁盘告警时先证明自己正在查看哪一个 daemon,再判断占用来自字节、inode、日志、镜像、缓存还是 volume。
docker context show
docker version
docker system df -v
docker ps -a --size
docker buildx ls
docker volume ls
docker network lsdocker system df -v 的 reclaimable 是引用视角,不是业务所有权。一个 stopped container 可能是故障证据,一个 unused image 可能是离线环境唯一可用基线,一个未挂载 volume 可能等待下次项目启动。清理决策必须关联 owner、来源、保留期和恢复办法。
按对象范围回收
第一层只停止对象并保存日志、inspect、镜像摘要和挂载信息。第二层删除可从配置重建的项目容器与 network。第三层按明确 builder 和时间窗口处理构建缓存。第四层处理可重新拉取的镜像。volume 数据始终最后删除,而且要先做恢复演练。
: "${CONTAINER_NAME:?set the reviewed container name}"
docker container inspect "$CONTAINER_NAME" > container.inspect.json
docker logs "$CONTAINER_NAME" > container.log 2>&1inspect 可能含凭据,输出放受控目录。确认容器可以移除后,用精确名称 stop、rm;镜像、卷与缓存再分别判断。共享 daemon 不使用全局 prune 代替项目清理,时间过滤也不能确认业务是否还需要旧对象。
volume 删除必须先证明可以恢复
备份不只是一个 tar 文件。恢复演练需要在新 volume 中解包,使用目标镜像版本启动,校验数据库或应用不变量,并记录备份时点、镜像摘要、校验和和恢复耗时。对数据库优先使用数据库原生备份;直接打包在线数据目录可能捕获不一致状态。
: "${SOURCE_VOLUME:?set the reviewed stopped-data volume}"
docker volume inspect "$SOURCE_VOLUME"
mkdir volume-backup
docker run --rm \
--mount "type=volume,src=$SOURCE_VOLUME,dst=/source,readonly" \
--mount "type=bind,src=$PWD/volume-backup,dst=/backup" \
alpine:3.22 \
sh -lc 'cd /source && tar -cf /backup/data.tar .'
(cd volume-backup && sha256sum data.tar > SHA256SUMS)示例只适用于能够接受文件级复制的一致状态,执行前停止全部写入并确认卷存在。备份容器默认以 root 读取文件元数据,这与应用运行身份不同。普通 tar 未必保留应用依赖的 ACL、扩展属性等,数据库优先按原生备份方式操作。
恢复到新卷后先验证文件,再启动匹配版本的应用。下面的新卷名应未被使用:
(cd volume-backup && sha256sum -c SHA256SUMS)
docker volume create docker-lab-restored
docker run --rm \
--mount type=volume,src=docker-lab-restored,dst=/restore \
--mount "type=bind,src=$PWD/volume-backup,dst=/backup,readonly" \
alpine:3.22 sh -c 'cd /restore && tar -xf /backup/data.tar && ls -la'原卷保留到恢复验证和保留期结束。文件能解包后还需验证应用可读、版本兼容与业务数据;Compose 只能重新创建运行对象,不能重新生成已删的数据内容。
镜像、network 与 Build Cache 各有停止条件
删除 tag 可能只移除一个引用,共享 layer 不一定释放同等空间;多架构 manifest 与本地平台镜像也要区分。清理前保存仍需复现的 digest,确认 registry、代理、CA 和凭证可用。离线环境若没有可信镜像归档,所谓可重新拉取并不成立。
network 本身通常不是磁盘大户,但残留 endpoint 会阻止删除并制造错误服务发现。先 docker network inspect 找出连接容器,再按项目生命周期处理。不能为了删 network 强行断开仍在运行的其他项目容器。
Build Cache 的清理按 builder 进行。docker buildx du 能显示缓存占用,docker buildx prune 前要确认 builder 与共享 CI 关系。释放缓存会增加下一次构建时间和回源流量;团队要把磁盘收益与 registry 可用性、供应链速率限制和开发等待一起衡量。
运行环境的选择
Linux 服务器可直接安装 Engine;Windows/macOS 的 Linux 容器通常由 Docker Desktop 的 VM 承载,需要另配文件共享、网络转发和资源。桌面产品的许可与更新按 Docker Desktop确认。
需要 rootless、不同容器引擎或桌面 Kubernetes 时,可比较 Podman 与 Rancher Desktop。迁移前用实际项目核对 API、Compose、网络、文件权限和持久数据。API 命令相似不意味着存储能够直接互换。
权威资料与规范地址
安装、身份与存储
| 查阅内容 | 官方地址 |
|---|---|
| Ubuntu 安装 | https://docs.docker.com/engine/install/ubuntu/ |
| Debian 安装 | https://docs.docker.com/engine/install/debian/ |
| Fedora 安装 | https://docs.docker.com/engine/install/fedora/ |
| CentOS 安装 | https://docs.docker.com/engine/install/centos/ |
| Live restore | https://docs.docker.com/engine/daemon/live-restore/ |
| Rootless | https://docs.docker.com/engine/security/rootless/ |
| containerd image store | https://docs.docker.com/engine/storage/containerd/ |
