Docker 完整指南:Engine、Compose、资源生命周期与安全清理
Docker 不是一条 docker run 命令,也不是一份孤立的 compose.yaml。CLI 可能指向错误 context,daemon socket 可能把主机控制权交给普通容器,Compose project name 会改变资源归属,旧 volume 会让初始化实验失真,而一次全局 prune 可能同时删除别人的缓存和无法重建的数据。
可靠的顺序是先辨认客户端正在连接哪个 Engine,再理解镜像、容器、网络、卷和 Build Cache 的引用关系;多服务项目由 Compose 固化成可审查模型;退出时只回收能够证明归属且已经验证可恢复的对象。安装、运行、编排和清理共享一套状态,拆开讲会让最危险的删除动作脱离创建上下文。
| 当前任务 | 进入位置 | 完成证据 |
|---|---|---|
| Linux 安装、daemon、权限、代理、日志和升级 | Engine 控制链 | Client/Server、systemd、配置、存储和回退记录 |
| 多服务依赖、变量、网络、卷和健康检查 | Compose 项目合同 | 解析模型、project 归属、就绪和重建结果 |
| 磁盘告警、旧数据、孤儿对象与构建缓存 | 资源生命周期与安全清理 | 引用、owner、备份恢复、删除范围与重建结果 |
| Windows/macOS 桌面虚拟化与许可 | Docker Desktop | 独立产品的 VM、context、设置与许可核验 |
先识别 Engine 的控制链与状态边界
CLI 成功不代表 Server 健康。安装或排障时保留下面四层证据:
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 发行版自带的 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 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 -rFedora 使用对应的 Fedora 仓库 URL。安装时同样选择列表中的完整版本:
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欢迎输出只证明 daemon 能拉取并启动那个镜像。随后还要验证端口、写时复制、volume、日志和重启行为,才能确认运行底座可用于项目。
用 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 不可用时尽量保持运行容器,但不会让网络、exec、日志采集和容器管理继续可用,也不是高可用方案。下载上传并发会改变代理、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 v2、UID/GID 子区间、网络、存储、端口和设备限制。
先确认当前用户拥有至少 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 同时使用。没有容量、兼容、迁移与回退计划时,不要通过改一个 feature 开关完成“升级”。
经典 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 策略满足要求。操作框架如下:
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、网络和项目探针。旧目录先只读保留到回退窗口结束,不能立即 rm -rf。回退时停止 daemon、恢复原配置并重新指向旧目录。复制期间若 daemon 仍在写入,得到的不是一致状态。
代理与 CA 分三层排查
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 -sS -o /dev/null -w '%{http_code}' "https://$registry/v2/")"
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示例使用保留域名,必须先替换并由配置管理注入真实端点;执行前核对证书指纹。未匿名开放的 Registry 正常返回 401,这证明 TLS 和 HTTP 已建立,只是还缺认证;连接错误或证书错误才进入 CA、SNI 和代理排查。失败时保留 journalctl -u docker.service 和 TLS 错误。不要用 insecure-registries 绕过证书错误作为长期方案,它把身份校验问题变成中间人攻击窗口。业务容器访问企业 HTTPS 还要在镜像内部安装 CA,并由语言运行时信任;宿主机信任不会自动进入容器。
四段探针能快速定位故障层:
curl -fsSI https://registry-1.docker.io/v2/ >/dev/null || true
docker pull alpine
docker run --rm alpine wget -qO- https://example.com >/dev/null
printf 'FROM alpine\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 和操作系统凭证存储;脚本退出与设备退役时撤销服务端凭证。
升级不是 apt upgrade 后看服务绿灯
升级前保存:
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 用户或专用构建池。
可靠的验收不是“服务是 active”,而是团队能持续证明:软件包来自受信仓库且版本可追踪;CLI 连接预期 daemon;普通账号没有意外 root 权限;配置错误会在重启前被阻断;数据盘、日志与 inode 有容量门槛;代理和 CA 的故障层可定位;升级能在 canary 复现项目;退役时软件、数据与凭证都能按各自生命周期清除。
Compose 把多容器运行方式变成项目合同
Compose CLI 读取一个或多个 YAML 文件,先完成变量插值与模型合并,再以 project 为边界创建容器、network、volume、config 和 secret。相同文件在不同 shell 变量、覆盖文件、project name 或 context 下,可能解析成完全不同的运行结果。因此 docker compose up -d 成功只证明创建动作完成,不证明应用已经就绪,也不证明连接的是预期数据。
先保存解析后的模型,而不是盯着原始 YAML 猜测:
docker context show
docker compose version
docker compose config --environment
docker compose config > compose.resolved.yaml
docker compose config --services
docker compose config --volumescompose.resolved.yaml 可能含有插值后的敏感值,不应提交或上传工单。审查时使用合成凭证或脱敏输出。需要在 CI 比较模型时,secret 只检查引用是否存在,不打印实际内容。
一份本地项目的基础模型可以保持很小:
name: orders-dev
services:
api:
build:
context: .
environment:
DATABASE_URL: postgres://app:${DB_PASSWORD:?DB_PASSWORD required}@db:5432/orders
depends_on:
db:
condition: service_healthy
ports:
- "127.0.0.1:8080:8080"
networks: [app]
db:
image: postgres:18.4
environment:
POSTGRES_DB: orders
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD required}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d orders"]
interval: 5s
timeout: 3s
retries: 20
volumes:
- db-data:/var/lib/postgresql
networks: [app]
networks:
app: {}
volumes:
db-data: {}现代 Compose Specification 不需要为了兼容旧版文件格式保留顶层 version。镜像 tag 需要与项目兼容基线一致;追求完全复现的 CI 或恢复演练进一步固定 digest。示例密码由环境注入,真实共享环境应使用受控 secret 入口。
依赖顺序与应用就绪是两件事
短形式 depends_on 只表达创建和启动依赖。数据库进程已经运行,不代表完成恢复、迁移或接受查询。condition: service_healthy 会等待依赖通过 healthcheck,service_completed_successfully 适合一次性迁移任务;应用自身仍要设置连接超时、有限重试和失败退出,不能把健康检查当成持续可用保证。
docker compose up -d --build
docker compose ps
docker compose logs --tail=100 db api
docker compose exec -T api ./scripts/verify-dependencies.shhealthcheck 要验证服务真实就绪,又不能执行高开销业务查询。间隔过短会给依赖增加噪声,超时过长则延迟故障暴露。检查命令还必须真实存在于镜像里;依赖宿主机工具的健康检查在容器中不会自动可用。
project name 决定资源归属
Compose 默认可能从目录名推导 project name。仓库换目录、CI 使用不同工作区或开发者通过 -p 改名后,会创建另一组 network、container 和 named volume。旧数据“丢失”时,第一步不是重建数据库,而是列出带 Compose 标签的对象并比较 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 ls --filter label=com.docker.compose.project项目可以用顶层 name 或团队脚本固定 -p。命名只是归属线索,不是授权机制;共享 daemon 上其他 docker 组成员仍可操作这些对象。清理脚本必须同时验证 context、project label、明确对象名和环境标识,不能只匹配一个常见前缀。
合并、插值与 profile 必须审查最终模型
多文件 Compose 会按指定顺序合并,数组和映射的行为并不都相同。Shell 环境、.env、--env-file 和 YAML 中的 environment 又处在不同解析层。生产问题里最常见的不是 YAML 语法错,而是值来自意外来源。
docker compose \
-f compose.yaml \
-f compose.local.yaml \
config > compose.resolved.yaml
docker compose --profile observability config --servicesoverride 文件如果改变端口、挂载、权限、build context 或镜像来源,就属于需要评审的项目配置。profile 适合把 GUI、观测或一次性工具设为可选,不应隐藏数据库、迁移或核心鉴权等必需依赖。缺少必要变量应通过 ${VAR:?message} 在解析阶段失败,避免空字符串运行到应用内部才报错。
network、volume、bind、config 和 secret 不可互换
同一 Compose network 内的服务通过 service name 和容器端口通信,宿主机则通过发布端口访问。容器内使用 localhost 只会回到当前容器,不会找到另一个 service。默认网络适合普通联调;需要明确隔离时拆分前后端网络,并只让真正需要的服务同时加入。
named volume 由 Engine 管理生命周期,适合数据库和工具状态;bind mount 直接暴露宿主路径,适合源码热更新,但会带来 UID/GID、SELinux、文件监听和宿主路径差异。config 与 secret 表达只读配置和敏感输入的意图,实际实现与目标平台有关。任何一种挂载都不等于备份。
docker compose exec -T api getent hosts db
docker compose exec -T api sh -lc 'test -r /run/secrets/app_token'
docker inspect orders-dev-db-1 --format '{{json .Mounts}}'
docker volume inspect orders-dev_db-dataexternal: true 表示 volume 生命周期由 Compose 项目之外管理;Compose 不创建它,down 也不应承担其销毁。使用外部 volume 必须在资产记录中写 owner、备份、挂载消费者和退役流程,否则只是把删除责任藏到了 YAML 外面。
Compose 退出要区分停止、重建和删数据
docker compose stop 保留容器;down 删除项目容器和默认网络,通常保留 named volume;down -v 还会删除项目声明的 volume。三者不能包装成同一个 reset 命令。退出前先保存项目状态与需要恢复的数据:
docker compose ps --all
docker compose images
docker compose config --volumes
docker volume ls --filter label=com.docker.compose.project=orders-dev普通结束使用 docker compose down --remove-orphans 前也要审查 orphan 是否确属当前 project。只有显式的本地数据重置才进入 down -v,脚本必须打印 context、project、volume 名和环境,并要求二次确认。共享、测试和生产环境不允许复用开发机 reset 脚本。
清理从对象引用图开始,不从 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 数据始终最后删除,而且要先做恢复演练。
docker container inspect orders-dev-api-1 > api.inspect.json
docker logs --timestamps orders-dev-api-1 > api.log 2>&1
docker compose down --remove-orphans
docker builder prune --filter 'until=168h'
docker image prune --filter 'until=168h'过滤时间降低范围,仍不能证明对象无人使用。共享 daemon 上的清理应叠加受控 label,并先用只读命令列出候选。不要在自动化中使用 --force 跳过审查,也不要把 docker system prune -a --volumes 作为日常磁盘维护;该命令跨项目删除 stopped container、unused network、unused image、Build Cache,并在指定选项时处理 volume。
volume 删除必须先证明可以恢复
备份不只是一个 tar 文件。恢复演练需要在新 volume 中解包,使用目标镜像版本启动,校验数据库或应用不变量,并记录备份时点、镜像摘要、校验和和恢复耗时。对数据库优先使用数据库原生备份;直接打包在线数据目录可能捕获不一致状态。
docker volume inspect orders-dev_db-data
docker run --rm \
-v orders-dev_db-data:/source:ro \
-v "$PWD/backup:/backup" \
alpine:3.22 \
sh -lc 'cd /source && tar -cf /backup/orders-dev-db.tar .'
sha256sum backup/orders-dev-db.tar > backup/orders-dev-db.tar.sha256示例只适用于能够接受文件级复制的一致状态,执行前要停止写入并确认挂载目标。恢复到新 volume 后,原 volume 保持只读观察窗口;验证通过且 owner 批准,才执行精确的 docker volume rm <name>。删除结果不可由 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 可用性、供应链速率限制和开发等待一起衡量。
Docker 主文与相关产品的边界
Docker Desktop 是独立发行和许可产品,在 Windows、macOS 与 Linux 桌面环境管理 VM、文件共享、网络转发、设置与更新;这些能力不属于 Linux Docker Engine 的同义词,因此保留独立文章。Compose 虽有独立 CLI 插件与发布周期,却直接承担 Docker 多容器应用模型和项目生命周期,读者从 Engine 进入后需要连续使用,故收回主文章。
Dev Containers 与 GitHub Codespaces 交付的是可复现开发环境合同,权威文章归入 IDE 远程开发家族,不在本目录重复。Podman 与 Rancher Desktop 分别维护自己的产品主文;选型迁移页只比较项目事实、停止条件和回滚。任何页面都不能重复讲一套 Docker 安装和清理前提。
