企业级容器化技术 Docker:扩展实践

企业级容器化技术 Docker:扩展实践 1. Podman 安装与环境 1.1 Podman 的定位 Podman(Pod Manager)是支持 OCI 容器和镜像的开源管理工具,采用无长期驻留中心 daemon 的本地管理架构,通过 crun、runc 等运行时启动容器,并使用 conmon

企业级容器化技术 Docker:扩展实践

1. Podman 安装与环境

1.1 Podman 的定位

Podman(Pod Manager)是支持 OCI 容器和镜像的开源管理工具,采用无长期驻留中心 daemon 的本地管理架构,通过 crun、runc 等运行时启动容器,并使用 conmon 等组件管理运行过程。它提供 rootful 和 rootless 两种运行方式。

Podman 的许多 CLI 命令与 Docker 相近,可以构建 Dockerfile/Containerfile,并提供兼容的 API 服务;这不代表所有 Docker 参数、网络、插件、Compose、API 和存储配置都可以无条件替换。Podman 的 API 服务需要按需启动,不能把“无中心 daemon”理解成没有辅助进程或服务。

Podman 支持 Pod,使一组容器共享选定命名空间。共享网络很常见,但卷并非因为进入同一个 Pod 就自动全部共享。Compose 可通过外部兼容提供程序,例如 podman-compose 或 Docker Compose,配合 Podman socket 使用,仍需核对版本。

rootless Podman 通常使用用户命名空间映射容器身份;rootful 模式不能一概认为默认将容器 root 映射成普通宿主机用户。Docker 同样支持 rootless,加入 docker 组则通常意味着拥有接近主机 root 的管理能力。

特性 Podman Docker Engine
核心架构 本地操作无中心常驻 daemon;可按需提供 API 服务 通过 dockerd 提供服务
用户模式 rootful、rootless rootful、rootless
常见数据目录 rootful:/var/lib/containers/storage;rootless:~/.local/share/containers/storage rootful 常见 /var/lib/docker;rootless 按用户环境配置
编排 内置 Pod 概念,Compose 依赖提供程序 Compose、Swarm 等;Kubernetes 使用 CRI 运行时
镜像 支持 OCI 与兼容 Docker 镜像格式 支持 Docker 与 OCI 兼容格式
安全机制 用户命名空间、能力、seccomp、SELinux/AppArmor 等 也可结合相应内核机制,具体由模式和配置决定

适用场景包括开发机、希望使用 rootless 的环境、容器调试和受限资源系统。Podman 的 Pod 概念有助于理解 Kubernetes,但真实迁移仍要适配配置、存储、网络和编排对象,不能保证无缝迁移。

1.2 发行版软件源

是否预装取决于安装镜像和软件组,不应按发行版名称作绝对判断。Ubuntu 20.04 的 universe 中也提供过 Podman;Rocky 8 通常可从 AppStream/container-tools 获取,不是必须启用 PowerTools 才能安装。

查询包版本:

# Ubuntu
apt-cache madison podman
# Rocky
yum list --showduplicates podman

原课件 Ubuntu 24.04 的仓库记录:

podman | 4.9.3+ds1-1ubuntu0.2 | noble-updates/universe amd64
podman | 4.9.3+ds1-1ubuntu0.1 | noble-security/universe amd64
podman | 4.9.3+ds1-1build2    | noble/universe amd64

Rocky 记录:

已安装的软件包
podman.x86_64   4:4.9.4-3.el9_4    @AppStream
可安装的软件包
podman.x86_64   5:5.4.0-10.el9_6   appstream

1.3 Ubuntu 安装与信息

apt update
apt install -y podman
podman --version
podman info | wc -l
podman info

原稿版本为 podman version 4.9.3,podman info | wc -l 得到 143。行数由版本和格式决定,不能用来比较工具能力。

原稿环境信息,按层级整理如下;这是 root 用户运行的 rootful 实验:

host:
  arch: amd64
  buildahVersion: 1.33.7
  cgroupControllers: [cpuset, cpu, io, memory, hugetlb, pids, rdma, misc]
  cgroupManager: systemd
  cgroupVersion: v2
  conmon:
    package: conmon_2.1.10+ds1-1build2_amd64
    path: /usr/bin/conmon
    version: 'conmon version 2.1.10, commit: unknown'
  cpuUtilization:
    idlePercent: 87.5
    systemPercent: 6.73
    userPercent: 5.77
  cpus: 2
  databaseBackend: sqlite
  distribution:
    codename: noble
    distribution: ubuntu
    version: '24.04'
  eventLogger: journald
  freeLocks: 2048
  hostname: ubuntu24-13
  idMappings:
    gidmap: null
    uidmap: null
  kernel: 6.11.0-26-generic
  linkmode: dynamic
  logDriver: journald
  memFree: 1952002048
  memTotal: 4054519808
  networkBackend: netavark
  networkBackendInfo:
    backend: netavark
    dns:
      package: aardvark-dns_1.4.0-5_amd64
      path: /usr/lib/podman/aardvark-dns
      version: aardvark-dns 1.4.0
    package: netavark_1.4.0-4_amd64
    path: /usr/lib/podman/netavark
    version: netavark 1.4.0
  ociRuntime:
    name: crun
    package: crun_1.14.1-1_amd64
    path: /usr/bin/crun
    version: |-
      crun version 1.14.1
      commit: de537a7965bfbe9992e2cfae0baeb56a08128171
      rundir: /run/user/0/crun
      spec: 1.0.0
      +SYSTEMD +SELINUX +APPARMOR +CAP +SECCOMP +EBPF +WASM:wasmedge +YAJL
  os: linux
  pasta:
    executable: /usr/bin/pasta
    package: passt_0.0~git20240220.1e6f92b-1_amd64
    version: pasta unknown version
  remoteSocket:
    exists: true
    path: /run/podman/podman.sock
  security:
    apparmorEnabled: true
    capabilities: CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_FOWNER,CAP_FSETID,CAP_KILL,CAP_NET_BIND_SERVICE,CAP_SETFCAP,CAP_SETGID,CAP_SETPCAP,CAP_SETUID,CAP_SYS_CHROOT
    rootless: false
    seccompEnabled: true
    seccompProfilePath: /usr/share/containers/seccomp.json
    selinuxEnabled: false
  serviceIsRemote: false
  slirp4netns:
    executable: /usr/bin/slirp4netns
    package: slirp4netns_1.2.1-1build2_amd64
    version: |-
      slirp4netns version 1.2.1
      commit: 09e31e92fa3d2a1d3ca261adaeb012c8d75a8194
      libslirp: 4.7.0
      SLIRP_CONFIG_VERSION_MAX: 4
      libseccomp: 2.5.5
  swapFree: 4055887872
  swapTotal: 4055887872
  uptime: 0h 6m 33.00s
  variant: ''
plugins:
  authorization: null
  log: [k8s-file, none, passthrough, journald]
  network: [bridge, macvlan, ipvlan]
  volume: [local]
registries: {}
store:
  configFile: /usr/share/containers/storage.conf
  containerStore:
    number: 0
    paused: 0
    running: 0
    stopped: 0
  graphDriverName: overlay
  graphOptions: {}
  graphRoot: /var/lib/containers/storage
  graphRootAllocated: 210238414848
  graphRootUsed: 12464111616
  graphStatus:
    Backing Filesystem: extfs
    Native Overlay Diff: 'true'
    Supports d_type: 'true'
    Supports shifting: 'true'
    Supports volatile: 'true'
    Using metacopy: 'false'
  imageCopyTmpDir: /var/tmp
  imageStore:
    number: 0
  runRoot: /run/containers/storage
  transientStore: false
  volumePath: /var/lib/containers/storage/volumes
version:
  APIVersion: 4.9.3
  Built: 0
  BuiltTime: Thu Jan 1 08:00:00 1970
  GitCommit: ''
  GoVersion: go1.22.2
  Os: linux
  OsArch: linux/amd64
  Version: 4.9.3

卸载软件与删除容器数据是两件事。以下清理目录仅对应不再保留数据的实验主机;原稿的 /run/container* 通配符扩大了范围,改为明确目录:

apt autoremove --purge podman
rm -rf /etc/containers /var/lib/containers /run/containers /run/podman
reboot

rootless 用户数据还有各自的数据目录,卸载 rootful 软件不等于已清理全部用户数据。重启不是卸载软件包必需步骤。

1.4 Rocky 安装与信息

Docker 的安装不保证会安装 Podman;先检查现有环境。

podman --version
podman version
yum install -y podman
podman --version
podman info

原稿旧版本记录:

podman version 4.9.4-rhel
Client: Podman Engine
Version: 4.9.4-rhel
API Version: 4.9.4-rhel
Go Version: go1.21.9 (Red Hat 1.21.9-2.el9_4)
Built: Wed May 1 09:15:44 2024
OS/Arch: linux/amd64

安装更新后为 podman version 5.4.0,原稿信息:

host:
  arch: amd64
  buildahVersion: 1.39.4
  cgroupControllers: [cpuset, cpu, io, memory, hugetlb, pids, rdma, misc]
  cgroupManager: systemd
  cgroupVersion: v2
  conmon:
    package: conmon-2.1.10-1.el9.x86_64
    path: /usr/bin/conmon
    version: 'conmon version 2.1.10, commit: 3ea3d7f99779af0fcd69ec16c211a7dc3b4efb60'
  cpuUtilization:
    idlePercent: 95.37
    systemPercent: 2.21
    userPercent: 2.42
  cpus: 2
  databaseBackend: sqlite
  distribution:
    distribution: rocky
    version: '9.4'
  eventLogger: journald
  freeLocks: 2048
  hostname: rocky9-12
  idMappings:
    gidmap: null
    uidmap: null
  kernel: 5.14.0-427.13.1.el9_4.x86_64
  linkmode: dynamic
  logDriver: journald
  memFree: 2200293376
  memTotal: 3803803648
  networkBackend: netavark
  networkBackendInfo:
    backend: netavark
    dns:
      package: aardvark-dns-1.10.0-3.el9_4.x86_64
      path: /usr/libexec/podman/aardvark-dns
      version: aardvark-dns 1.10.0
    package: netavark-1.10.3-1.el9.x86_64
    path: /usr/libexec/podman/netavark
    version: netavark 1.10.3
  ociRuntime:
    name: crun
    package: crun-1.14.3-1.el9.x86_64
    path: /usr/bin/crun
    version: |-
      crun version 1.14.3
      commit: 1961d211ba98f532ea52d2e80f4c20359f241a98
      rundir: /run/user/0/crun
      spec: 1.0.0
      +SYSTEMD +SELINUX +APPARMOR +CAP +SECCOMP +EBPF +CRIU +YAJL
  os: linux
  pasta:
    executable: /usr/bin/pasta
    package: passt-0^20250217.ga1e48a0-1.el9.x86_64
    version: ''
  remoteSocket:
    exists: true
    path: /run/podman/podman.sock
  rootlessNetworkCmd: pasta
  security:
    apparmorEnabled: false
    capabilities: CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_FOWNER,CAP_FSETID,CAP_KILL,CAP_NET_BIND_SERVICE,CAP_SETFCAP,CAP_SETGID,CAP_SETPCAP,CAP_SETUID,CAP_SYS_CHROOT
    rootless: false
    seccompEnabled: true
    seccompProfilePath: /usr/share/containers/seccomp.json
    selinuxEnabled: true
  serviceIsRemote: false
  slirp4netns:
    executable: /usr/bin/slirp4netns
    package: slirp4netns-1.2.3-1.el9.x86_64
    version: |-
      slirp4netns version 1.2.3
      commit: c22fde291bb35b354e6ca44d13be181c76a0a432
      libslirp: 4.4.0
      SLIRP_CONFIG_VERSION_MAX: 3
      libseccomp: 2.5.2
  swapFree: 4206882816
  swapTotal: 4206882816
  uptime: 0h 21m 20.00s
  variant: ''
plugins:
  authorization: null
  log: [k8s-file, none, passthrough, journald]
  network: [bridge, macvlan, ipvlan]
  volume: [local]
registries:
  search: [registry.access.redhat.com, registry.redhat.io, docker.io]
store:
  configFile: /etc/containers/storage.conf
  containerStore:
    number: 0
    paused: 0
    running: 0
    stopped: 0
  graphDriverName: overlay
  graphOptions:
    overlay.mountopt: nodev,metacopy=on
  graphRoot: /var/lib/containers/storage
  graphRootAllocated: 75094818816
  graphRootUsed: 5430738944
  graphStatus:
    Backing Filesystem: xfs
    Native Overlay Diff: 'false'
    Supports d_type: 'true'
    Supports shifting: 'false'
    Supports volatile: 'true'
    Using metacopy: 'true'
  imageCopyTmpDir: /var/tmp
  imageStore:
    number: 0
  runRoot: /run/containers/storage
  transientStore: false
  volumePath: /var/lib/containers/storage/volumes
version:
  APIVersion: 5.4.0
  Built: 1750148132
  BuiltTime: Tue Jun 17 16:15:32 2025
  GitCommit: ''
  GoVersion: go1.23.9 (Red Hat 1.23.9-1.el9_6)
  Os: linux
  OsArch: linux/amd64
  Version: 5.4.0

移除实验环境:

yum remove 'podman*'
rm -rf /etc/containers /var/lib/containers /run/containers /run/podman
reboot

2. Windows 安装 Docker Desktop

2.1 下载与安装

访问 https://www.docker.com/,选择适合 Windows 和本机 CPU 架构的安装包。原课件使用 AMD64。

Docker Desktop 下载入口

直接下载地址:https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe。

下载后的安装包

双击安装包,按系统提示运行,选择所需后端和快捷方式,再确认安装。

Windows 安装选项

安装过程与完成界面

原课件点击【Close and restart】后重启 Windows,重新登录后启动 Docker Desktop,阅读并接受相应服务条款。

Docker Desktop 服务条款界面

可按实际产品要求登录,原截图提供【Continue without signing in】入口。出现更新提示时可按环境要求处理,不必为了启动容器无条件更新全部功能。

首次启动界面

跳过登录与更新提示

2.2 WSL 2 后端

Docker Desktop 运行 Linux 容器需要受支持的虚拟化后端。使用 WSL 2 时应满足 Windows、WSL、CPU 虚拟化及内核要求。网络受限可能妨碍下载,但“必须翻墙”和“默认一定无法启动”都不是普遍条件。

Docker Desktop 基本设置

在【Settings → Resources → WSL integration】查看与发行版的集成;该页面与安装/更新 WSL 本身不是完全相同的操作。

WSL 环境需要更新时的提示

根据系统提示在管理员 PowerShell 中检查并安装/更新 WSL:

wsl --status
wsl --install
wsl --update

按实际提示重启系统或 WSL,不要在已满足条件的系统上机械重复初始化。

WSL 下载与安装过程

WSL 集成配置

完成后点击【Apply】或【Apply & restart】,必要时使用界面的【Restart Docker】,确认引擎从 Stopped 变为 Running。

应用设置后检查 Docker 引擎

2.3 Hyper-V 后端的另一条安装路径

原稿安装时不勾选“Use WSL 2 instead of Hyper-V”。这表示选择其他受支持的虚拟化后端,不能保证所有 Windows 版本都支持,也不能解决未启用虚拟化等所有问题。应核对 Windows 版本、Hyper-V 功能与 Docker Desktop 当前系统要求。

选择后端的安装选项

Docker 引擎运行后的界面

2.4 内置终端

点击右下角 >_,在 Terminal 页面启用相应终端功能,再输入 Docker 命令。

启用内置终端

在内置终端运行命令

docker info

原稿输出开头:

Client:
 Version: 28.5.1
 Context: desktop-linux
 Debug Mode: false
 Plugins:
  ai: Docker AI Agent - Ask Gordon (Docker Inc.)
  ...

3. Docker 代理

3.1 场景和调用链

企业内网、受限网络或连接质量较差的环境中,可通过获准的代理访问外部 Registry。代理可以统一转发、审计和控制请求,但它不自动保护所有容器入站流量,也不代替防火墙或容器隔离。

直接拉取:Docker CLI → dockerd → Registry
配置代理:Docker CLI → dockerd → HTTP(S) 代理 → Registry
容器应用:应用自身 → 按应用配置使用代理 → 目标服务

daemon 的代理与容器内应用代理是两个层次。通过登录 Shell 设置环境变量,不保证 systemd 管理的 dockerd 自动继承;Docker Desktop 通常通过其产品设置管理代理,也不能机械照搬 Linux daemon 配置。

场景 代理的作用 镜像加速的作用
内网拉取公网镜像 提供可达的外连路径 可结合缓存减少拉取时间
到 Docker Hub 链路慢 改变网络路径,速度不一定提高 缓存或优化分发链路
多节点同时拉取 普通转发仍可能集中消耗上游带宽 P2P/缓存有助于减少重复传输
容器应用访问公网 API 需要给应用单独配置 Registry mirror 不负责普通 API 请求

代理和镜像 mirror 可以配合使用。HTTPS 目标经 HTTP CONNECT 代理时,HTTPS_PROXY 的代理地址仍常是 http://;地址协议必须与代理服务真正支持的协议一致,不能一概禁止 https://。

3.2 配置入口

参考:https://docs.docker.com/engine/daemon/proxy/。

Docker Engine 支持代理配置的版本中,可在 daemon.json 合并以下内容:

{
  "proxies": {
    "http-proxy": "http://proxy.example.com:3128",
    "https-proxy": "http://proxy.example.com:3128",
    "no-proxy": "localhost,127.0.0.0/8,.test.example.com,.example.org"
  }
}

或通过 systemd drop-in 为服务设置环境变量:

[Service]
Environment="HTTP_PROXY=http://10.0.0.1:10809/"
Environment="HTTPS_PROXY=http://10.0.0.1:10809/"
Environment="NO_PROXY=localhost,127.0.0.0/8,172.17.0.0/16,10.0.0.0/24,10.244.0.0/16,192.168.0.0/16,wang.org,cluster.local"

daemon 配置的代理通常优先于服务环境变量;尽量选择一个明确来源维护,排错时检查是否有其他来源覆盖。

容器级配置:

docker run -it \
  --env http_proxy=http://proxy.example.com:8080 \
  --env https_proxy=http://proxy.example.com:8080 \
  --env no_proxy=localhost,127.0.0.1 \
  your_image_name

Dockerfile 也可使用 ENV,但会把固定代理信息留在镜像配置中,不适合写入含凭证的代理地址:

ENV http_proxy=http://proxy.example.com:8080
ENV https_proxy=http://proxy.example.com:8080
ENV no_proxy=localhost,127.0.0.1

进入已有容器的临时 Shell 后设置:

docker exec -it container_name /bin/bash

容器 Shell 内:

export http_proxy=http://proxy.example.com:8080
export https_proxy=http://proxy.example.com:8080
export no_proxy=localhost,127.0.0.1

它只影响该 Shell 及其后续子进程,不修改已经运行的主服务或容器永久配置。

测试代理:

curl -x http://10.0.0.1:10809 https://registry-1.docker.io/v2/

原响应:

{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":null}]}

这说明通过代理得到了 Registry API 的认证响应;不等于所有镜像和认证流程都已通过。

3.3 systemd 代理实验

原实验 Linux 地址为 10.0.0.13,Windows 代理地址为 10.0.0.1:10809。

ss -nt
telnet 10.0.0.1 10809

原连接信息:

ESTAB ... [::ffff:10.0.0.13]:22 ... [::ffff:10.0.0.1]:8978 ...
Trying 10.0.0.1...
Connected to 10.0.0.1.
Escape character is '^]'.
Connection closed by foreign host.

代理必须监听 Linux 主机可达的地址,并允许相应来源;只监听 Windows 的 127.0.0.1 通常无法被另一台主机访问。监听所有地址时也要通过访问控制限制使用范围。

PROXY_SERVER_IP=10.0.0.1
PROXY_PORT=10809
mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/http-proxy.conf <<PROXYCONF
[Service]
Environment="HTTP_PROXY=http://${PROXY_SERVER_IP}:${PROXY_PORT}/"
Environment="HTTPS_PROXY=http://${PROXY_SERVER_IP}:${PROXY_PORT}/"
Environment="NO_PROXY=localhost,127.0.0.0/8,172.17.0.0/16,10.0.0.0/24,192.168.0.0/16,10.244.0.0/16,cluster.local"
PROXYCONF
systemctl daemon-reload
systemctl restart docker.service
docker info | grep -i proxy

配置后的预期:

HTTP Proxy: http://10.0.0.1:10809/
HTTPS Proxy: http://10.0.0.1:10809/
No Proxy: localhost,127.0.0.0/8,172.17.0.0/16,10.0.0.0/24,192.168.0.0/16,10.244.0.0/16,cluster.local

原稿拉取旧 Kubernetes 镜像的记录:

docker pull gcr.io/google-containers/kube-proxy:v1.18.0
v1.18.0: Pulling from google-containers/kube-proxy
597de8ba0c30: Pull complete
...
51432749cbec: Pull complete
Digest: sha256:9e858386d52d0abaf936c1d10a763648ab7d85c8eb0af08a50a64238146e5571
Status: Downloaded newer image for gcr.io/google-containers/kube-proxy:v1.18.0
gcr.io/google-containers/kube-proxy:v1.18.0

这些是历史镜像路径和版本,不保证当前仍可拉取。

取消此处代理时,只删本实验的 drop-in,不删除包含其他设置的整个目录:

rm -f /etc/systemd/system/docker.service.d/http-proxy.conf
systemctl daemon-reload
systemctl restart docker
docker info | grep -i proxy

若没有其他代理来源,查询不再输出代理。原稿取消后拉取 v1.19.0 的失败记录:

Error response from daemon: Get "https://gcr.io/v2/": context deadline exceeded

是否必须使用代理取决于本机实际网络,不能据此推断所有主机都无法直连。

3.4 daemon.json 代理实验

把以下对象合并到已有 /etc/docker/daemon.json,不要覆盖镜像加速等其他设置:

{
  "proxies": {
    "http-proxy": "http://10.0.0.1:10809",
    "https-proxy": "http://10.0.0.1:10809",
    "no-proxy": "localhost,127.0.0.0/8,172.17.0.0/16,10.0.0.0/24,192.168.0.0/16,10.244.0.0/16,.cluster.local"
  }
}
systemctl restart docker
docker info | grep -i proxy
docker pull gcr.io/google-containers/kube-proxy:v1.17.0

原稿仅用 systemctl start docker,服务已运行时不会重新读取该变化,已改为 restart。

原拉取记录:

v1.17.0: Pulling from google-containers/kube-proxy
597de8ba0c30: Already exists
...
d3609306ce38: Already exists
d7c1c982f192: Pull complete
Digest: sha256:b2ba9441af30261465e5c41be63e462d0050b09ad280001ae731f399b2b00b75
Status: Downloaded newer image for gcr.io/google-containers/kube-proxy:v1.17.0
gcr.io/google-containers/kube-proxy:v1.17.0

3.5 配套脚本入口

原稿引用 scripts/docker_set_proxy.sh,PDF 没有提供脚本全文,本文不补造实现:

Usage: docker_set_proxy.sh {daemon|service} {start|stop}
 daemon  使用 daemon.json 配置
 service 使用 systemd 配置
 start   设置代理
 stop    取消代理
/bin/bash docker_set_proxy.sh daemon start
docker info | grep -i proxy
/bin/bash docker_set_proxy.sh daemon stop
docker info | grep -i proxy
/bin/bash docker_set_proxy.sh service start
docker info | grep -i proxy
/bin/bash docker_set_proxy.sh service stop
docker info | grep -i proxy

原稿 daemon stop 输出:

正在通过daemon.json取消Docker代理配置...
已备份原配置文件到 /etc/docker/daemon.json.bak [OK]
已移除代理配置(使用jq) [OK]
重新加载systemd配置 [OK]
正在重启Docker服务...
Docker服务重启成功 [OK]
Docker代理配置已成功移除! [OK]
当前Docker代理配置:
无代理相关配置

原稿 service start 输出:

正在通过systemd配置Docker代理...
配置目录 /etc/systemd/system/docker.service.d 已存在 [OK]
写入代理配置到 /etc/systemd/system/docker.service.d/http-proxy.conf [OK]
重新加载systemd配置 [OK]
正在重启Docker服务...
Docker服务重启成功 [OK]
Docker服务代理配置验证成功! [OK]
当前Docker代理配置:
Environment=HTTP_PROXY=http://10.0.0.1:10809 HTTPS_PROXY=https://10.0.0.1:10809
NO_PROXY=127.0.0.0/8,172.17.0.0/16,10.0.0.0/24,10.244.0.0/16,192.168.0.0/16,wang.org,cluster.local

原脚本曾显示 HTTPS_PROXY=https://10.0.0.1:10809,而手工实验使用 HTTP 代理。只有服务本身支持 HTTPS 代理协议时前者才正确;脚本报告“配置成功”不能替代实际协议请求验证。

原稿 service stop 输出:

正在通过systemd取消Docker代理配置...
移除代理配置 /etc/systemd/system/docker.service.d [OK]
重新加载systemd配置 [OK]
正在重启Docker服务...
Docker服务重启成功 [OK]
Docker服务代理配置已成功移除! [OK]
当前Docker环境变量配置:
无代理相关配置

这是原脚本的行为记录。手工操作采用上一节只删除专属 drop-in 的方式,避免移除其他服务配置。

4. Rocky Linux 安装 Docker

参考:https://developer.aliyun.com/mirror/docker-ce、https://docs.docker.com/engine/install/、https://mirrors.tuna.tsinghua.edu.cn/help/docker-ce/。课程使用 EL9 环境,软件源与软件包适配性应按实际发行版确认。

4.1 软件源和安装

yum install -y yum-utils
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
yum makecache
yum list docker-ce --showduplicates

原稿候选版本:

docker-ce.x86_64   3:28.2.2-1.el9   docker-ce-stable
docker-ce.x86_64   3:28.2.1-1.el9   docker-ce-stable

指定版本与安装仓库当前版本的方式分别为:

# 版本必须真实存在;CLI 的 epoch 不一定与 Engine 相同
yum install docker-ce-3:28.2.2-1.el9 docker-ce-cli-1:28.2.2-1.el9 containerd.io

# 或安装当前仓库版本及插件
yum install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

原稿固定版本安装事务记录:

类型 软件包 版本 大小
安装 docker-ce 3:28.2.2-1.el9 20 M
升级 selinux-policy 38.1.53-5.el9_6 44 k
升级 selinux-policy-targeted 38.1.53-5.el9_6 6.5 M
依赖 container-selinux 4:2.235.0-2.el9_6 58 k
依赖 containerd.io 1.7.27-3.1.el9 44 M
依赖 docker-ce-cli 1:28.2.2-1.el9 8.5 M
依赖 fuse-overlayfs 1.14-1.el9 66 k
依赖 libslirp 4.4.0-8.el9 67 k
依赖 slirp4netns 1.3.2-1.el9 46 k
弱依赖 docker-buildx-plugin 0.24.0-1.el9 16 M
弱依赖 docker-ce-rootless-extras 28.2.2-1.el9 3.4 M
弱依赖 docker-compose-plugin 2.36.2-1.el9 15 M

原事务为安装 10 包、升级 2 包,总下载 114 M;这是当时仓库求解结果,不是每次安装都相同。

4.2 服务与环境信息

systemctl is-active docker
systemctl start docker
systemctl is-active docker
docker version
docker info

原稿第一次为 inactive,启动后为 active。客户端版本即使 daemon 不可达也可能显示;服务端部分需要能够连接 daemon。

原稿 docker version:

Client: Docker Engine - Community
 Version: 28.2.2
 API version: 1.50
 Go version: go1.24.3
 Git commit: e6534b4
 Built: Fri May 30 12:09:15 2025
 OS/Arch: linux/amd64
 Context: default
Server: Docker Engine - Community
 Engine:
  Version: 28.2.2
  API version: 1.50 (minimum version 1.24)
  Go version: go1.24.3
  Git commit: 45873be
  Built: Fri May 30 12:07:23 2025
  OS/Arch: linux/amd64
  Experimental: false
 containerd:
  Version: 1.7.27
  GitCommit: 05044ec0a9a75232cad458027ca83437aae3f4da
 runc:
  Version: 1.2.5
  GitCommit: v1.2.5-0-g59923ef
 docker-init:
  Version: 0.19.0
  GitCommit: de40ad0

原稿 docker info:

Client: Docker Engine - Community
 Version: 28.2.2
 Context: default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
   Version: v0.24.0
   Path: /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
   Version: v2.36.2
   Path: /usr/libexec/docker/cli-plugins/docker-compose
Server:
 Containers: 0
  Running: 0
  Paused: 0
  Stopped: 0
 Images: 0
 Server Version: 28.2.2
 Storage Driver: overlay2
  Backing Filesystem: xfs
  Supports d_type: true
  Using metacopy: false
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 CDI spec directories:
  /etc/cdi
  /var/run/cdi
 Swarm: inactive
 Runtimes: runc io.containerd.runc.v2
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 05044ec0a9a75232cad458027ca83437aae3f4da
 runc version: v1.2.5-0-g59923ef
 init version: de40ad0
 Security Options:
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 5.14.0-427.13.1.el9_4.x86_64
 Operating System: Rocky Linux 9.4 (Blue Onyx)
 OSType: linux
 Architecture: x86_64
 CPUs: 2
 Total Memory: 1.703GiB
 Name: rocky9-12
 ID: 7848764d-c041-4bc5-ba75-787886e367b5
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  ::1/128
  127.0.0.0/8
 Live Restore Enabled: false

4.3 docker0 与防火墙

ip addr show docker0
nft list ruleset | wc -l
iptables -vnL

原稿地址:

3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN
 link/ether 36:47:80:a6:d2:0a brd ff:ff:ff:ff:ff:ff
 inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
 valid_lft forever preferred_lft forever

没有接入的活动容器时,网桥可以显示 NO-CARRIER/DOWN,不代表 daemon 必然未启动。默认地址常为 172.17.0.1/16,但配置或地址冲突可能使其不同。

原 nft 输出提示与计数:

Warning: table ip nat is managed by iptables-nft, do not touch!
Warning: table ip filter is managed by iptables-nft, do not touch!
Warning: table ip6 nat is managed by iptables-nft, do not touch!
102

原稿 filter 表结构(计数初始为 0):

INPUT policy ACCEPT
FORWARD policy DROP
  DOCKER-USER
  DOCKER-FORWARD
OUTPUT policy ACCEPT
DOCKER:
  DROP all -- !docker0 docker0 0.0.0.0/0 0.0.0.0/0
DOCKER-BRIDGE:
  DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
DOCKER-CT:
  ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
DOCKER-FORWARD:
  DOCKER-CT
  DOCKER-ISOLATION-STAGE-1
  DOCKER-BRIDGE
  ACCEPT all -- docker0 * 0.0.0.0/0 0.0.0.0/0
DOCKER-ISOLATION-STAGE-1:
  DOCKER-ISOLATION-STAGE-2 all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
DOCKER-ISOLATION-STAGE-2:
  DROP all -- * docker0 0.0.0.0/0 0.0.0.0/0
DOCKER-USER:
  无自定义规则

网桥负责二层连接,veth 将容器接入网桥,路由和防火墙规则负责转发/NAT及访问控制;不是“网桥网络由防火墙规则本身创建”。链名和策略随 Engine 版本及 iptables/nftables 后端变化。

原稿分别展示了 Rocky/Ubuntu 的检查(其中 Rocky 小节提示符误写 Ubuntu,不据此判断发行版):

ip addr show docker0
route -n
iptables -vnL
项目 Rocky 实验 Ubuntu 实验
docker0 MAC a6:c3:c6:03:72:39 8e:3c:e9:0a:8c:4f
docker0 地址 172.17.0.1/16 172.17.0.1/16
默认路由 0.0.0.0 → 10.0.0.2,ens160,metric 100 0.0.0.0 → 10.0.0.2,ens33,metric 100
宿主机网段 10.0.0.0/24,ens160 10.0.0.0/24,ens33
容器路由 172.17.0.0/16,docker0,metric 0 172.17.0.0/16,docker0,metric 0
filter 表 FORWARD 为 DROP,经 Docker 链放行需要的流量 同类 Docker 转发与隔离链

4.4 镜像加速与 hello-world

原稿没有可用外连时:

Unable to find image 'hello-world:latest' locally
docker: Error response from daemon: Get "https://registry-1.docker.io/v2/":
net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
Run 'docker run --help' for more information

镜像加速配置示例:

{
  "registry-mirrors": [
    "https://docker.xuanyuan.me"
  ]
}

原稿还使用账户专属 https://82ue03hk.mirror.aliyuncs.com;实际应替换为自己获准使用的有效地址。这些地址本次未做联网可用性验证。JSON 最后一项不能有多余逗号,不能写注释。

systemctl restart docker
docker info | grep -A2 Regi
curl https://docker.xuanyuan.me/v2/
docker run hello-world

只改 daemon.json 不需要 systemctl daemon-reload;后者在 systemd 单元或 drop-in 改变时才需要。

原稿配置和测试记录:

Insecure Registries:
 ::1/128
 127.0.0.0/8
Registry Mirrors:
 https://docker.xuanyuan.me/
Live Restore Enabled: false
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
e6590344b1a5: Pull complete
Digest: sha256:940c619fbd418f9b2b1b63e25d8861f9cc1b46e3fc8b018ccfe8b78f19b8cc4f
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.

hello-world 的后续说明概括了四步:客户端连接 daemon,daemon 拉取镜像、创建并运行容器,将程序输出流回客户端。还提示可尝试 docker run -it ubuntu bash,参考 https://hub.docker.com/ 和 https://docs.docker.com/get-started/。

4.5 卸载与数据清理

docker ps -a
docker rm -f edc
systemctl stop docker.service docker.socket
yum remove docker-ce docker-ce-cli docker-buildx-plugin \
  docker-ce-rootless-extras docker-compose-plugin containerd.io

原 hello-world 容器 ID edcd4d02087b,状态 Exited (0),名称 objective_nightingale。原卸载事务移除了 10 个包,回收约 430 M:Docker 的 5 个软件包,以及未继续使用的 container-selinux、containerd.io、fuse-overlayfs、libslirp、slirp4netns 等依赖;实际依赖去留由包管理器和本机其他软件决定。

确认数据无需保留后,原稿的清理和重启:

rm -rf /etc/docker /var/lib/docker /var/lib/containerd
rm -f /etc/yum.repos.d/docker-ce.repo
reboot

5. Docker 服务与配置进阶

5.1 systemd 服务文件

rpm -ql docker-ce | grep docker.service
cat /usr/lib/systemd/system/docker.service

原包路径为 /usr/lib/systemd/system/docker.service,其主要内容:

[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network-online.target nss-lookup.target docker.socket firewalld.service containerd.service time-set.target
Wants=network-online.target containerd.service
Requires=docker.socket
StartLimitBurst=3
StartLimitIntervalSec=60

[Service]
Type=notify
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutStartSec=0
RestartSec=2
Restart=always
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
Delegate=yes
KillMode=process
OOMScoreAdjust=-500

[Install]
WantedBy=multi-user.target
属性 含义
After/Wants/Requires 启动顺序和依赖关系;Requires 的要求限于采用该单元的部署,不是所有 dockerd 都必须使用 docker.socket
Type=notify 通过 sd_notify 报告就绪
-H fd:// 接收 systemd socket 激活传入的监听描述符
--containerd 指定连接的 containerd socket,不是让 Docker 在这个路径对 CLI 监听
ExecReload 向主进程发 HUP,重载其支持动态变更的配置
Restart=always 自动重启进程,但 systemctl stop 等显式管理停止不会因此立即反复拉起
StartLimit* 限制一定时间内启动尝试次数
LimitNPROC/CORE、TasksMax 设置服务层进程/任务及 core 限制;不等于系统资源本身无限
Delegate=yes 允许服务管理其被委派的 cgroup 层级
KillMode=process 停止时主要针对服务主进程,不按普通 control-group 模式杀整个组
OOMScoreAdjust=-500 降低 daemon 被 OOM killer 选择的倾向,不能保证所有场景先杀容器

原单元的历史英文注释提到不默认使用 systemd cgroup;实际实验 docker info 已为 Cgroup Driver: systemd,应以运行配置为准,不能沿用旧注释得出相反结论。

本地 CLI 可连接 -H unix:///var/run/docker.sock;socket 权限控制访问,能访问 rootful Docker socket 通常就具有极高主机权限,并非“只在本机就天然安全”。--containerd=/run/containerd/containerd.sock 则是 daemon 与 containerd 的内部管理连接,也需保护访问权限。

5.2 daemon.json 常用设置

rootful daemon 常用配置文件为 /etc/docker/daemon.json,安装后不一定已创建。它使用严格 JSON,不支持 #、// 或 /* */ 注释。以下各段按用途展示,实际使用需合并为一个对象并核对当前版本支持的键,不要逐段覆盖。

基础示例:

{
  "registry-mirrors": ["https://mirror.example.com"],
  "data-root": "/var/lib/docker",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "experimental": false
}

修改后部分设置可 HUP 重载,其他设置需重启。daemon 启动参数与配置文件重复声明同一键可能导致冲突。

仓库、运行时与并发

{
  "registry-mirrors": ["https://mirror.example.com"],
  "insecure-registries": ["myregistry.local:5000"],
  "max-concurrent-downloads": 10,
  "max-concurrent-uploads": 5
}

insecure-registries 允许相应仓库使用不安全传输或证书方式;使用有效证书的 HTTPS 更合适。原稿的 registry.docker-cn.com 是历史地址,不能当成当前有效服务。

原稿还直接设 "default-runtime": "nvidia",但只有先安装并注册相应 NVIDIA runtime、且实际希望作为默认运行时时才可设置,不能单独复制该键就获得 GPU 支持。

存储

{
  "data-root": "/mnt/docker-data",
  "storage-driver": "overlay2"
}

更换数据根目录或驱动前先规划迁移和回退。原稿的 overlay2.override_kernel_check=true 属于历史驱动选项,当前版本可能不支持,不应用它绕过宿主机兼容性检查。采用 containerd image store 的 Engine 安装布局与经典 overlay2 存储也可能不同。

日志

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "labels": "production_status",
    "env": "os,customer"
  }
}

日志选项中数值常需以字符串表示。labels/env 选择可带入日志的附加属性,docker logs --details 可按日志驱动支持情况显示。新默认值一般只影响新创建容器,已有容器需重建以采用新的日志配置。

网络

{
  "bip": "172.17.0.1/16",
  "mtu": 1450,
  "default-address-pools": [
    {"base": "172.28.0.0/16", "size": 24},
    {"base": "172.29.0.0/16", "size": 24}
  ]
}

bip 配置默认 bridge 地址;地址池用于自动分配相应新网络。原稿 172.80.0.0/16、172.90.0.0/16 不是 RFC1918 私有地址,已换成私有网段示例,实际仍需避免与现有局域网、VPN、集群冲突。MTU 应匹配真实链路,不能机械固定。

安全与运行控制

{
  "userns-remap": "default",
  "seccomp-profile": "/etc/docker/seccomp.json",
  "cgroup-parent": "docker.slice",
  "live-restore": true
}
  • userns-remap 为 rootful daemon 使用的容器身份提供映射;切换时已有数据可见性、卷权限及兼容性要另外处理。
  • 自定义 seccomp 文件必须真实存在且为有效策略。
  • cgroup-parent 必须符合所用 cgroup 驱动的格式和实际层级;systemd 常用 slice 名。
  • live-restore 可在受支持的 daemon 不可用/重启场景保持容器运行,不是零停机升级或任意跨版本迁移保证。

调试和指标

{
  "debug": true,
  "experimental": true,
  "metrics-addr": "127.0.0.1:9323"
}

原稿把指标绑定到所有地址,改为本地监听示例,需远程采集时再按受控网络开放。BuildKit 不应笼统列为所有新版都需 experimental 才能启用;Docker 也不是通过 experimental 自动启用 Kubernetes CRI 插件。

原稿 "features": {"buildkit": true} 属于特定时期的启用方式,是否接受应检查当前 Engine 和 builder 配置,不要混用已移除的功能开关。

远程监听、IPv6 和默认限制

{
  "hosts": ["unix:///var/run/docker.sock", "tcp://127.0.0.1:2375"]
}

这是未加密 API 的本机监听示例;远程优先使用 SSH 或配置相互认证的 TLS。若单元已有 -H fd://,必须消除与 hosts 的重复声明,详见后面的远程管理实验。

{
  "ipv6": true,
  "fixed-cidr-v6": "2001:db8:1::/64"
}

2001:db8::/32 是文档专用地址,不用于实际互联网路由;真实 IPv6 配置需可用前缀、路由与防火墙。

{
  "default-ulimits": {
    "nofile": {"Name": "nofile", "Hard": 65536, "Soft": 65536}
  },
  "default-shm-size": "256m"
}

5.3 Podman/Buildah 的 registries.conf

/etc/containers/registries.conf 属于 containers/image 等工具栈,不是 Docker Engine 的 daemon 配置;是否预装由软件包决定。现代格式是 TOML,不是原稿所称的通用 INI,也没有原稿示例中的任意 search."docker.io"、priority、cert_dir、authfile 等同级键。

下面是按现代格式修正的同等用途示例:

unqualified-search-registries = ["docker.io", "quay.io", "registry.access.redhat.com"]
short-name-mode = "enforcing"

[[registry]]
prefix = "docker.io"
location = "docker.io"
insecure = false

[[registry.mirror]]
location = "mirror1.example.com"
insecure = false

[[registry.mirror]]
location = "mirror2.example.com"
insecure = false

[[registry]]
prefix = "myregistry.example.com"
location = "myregistry.example.com"
insecure = true

[aliases]
"nginx" = "docker.io/library/nginx"
原稿想表达的功能 正确配置入口
短名称搜索列表 unqualified-search-registries;优先使用完整镜像名减少歧义
注册表匹配和地址重写 [[registry]] 的 prefix、location
mirror 对应 registry 下的 [[registry.mirror]]
允许 HTTP/跳过相应 TLS 验证 registry/mirror 的 insecure,按工具文档语义使用
自定义 CA /etc/containers/certs.d/<host[:port]>/ 或命令支持的 --cert-dir
认证文件 podman login、--authfile、REGISTRY_AUTH_FILE 等支持入口,不是 registries.conf 随意插入的 authfile
搜索优先顺序 搜索列表顺序及具体匹配规则,不是任意 priority 数字
拆分配置 /etc/containers/registries.conf.d/*.conf 等工具支持目录,不是随意添加 include 键

示例 mirror 和 registry 域名为占位地址,实际使用需替换为确实提供对应内容的服务。prefix 匹配镜像引用前缀,不等同于“所有 nginx 自动加此前缀”;短名固定映射可通过 aliases 或完整镜像引用表达。

5.4 配置文件与 swap 警告

rpm -ql docker-ce
docker info

原包列表包含:

/etc/docker
/usr/bin/docker-proxy
/usr/bin/dockerd
/usr/lib/.build-id/29/7ae7d963b1d086bda9a175fdea23aede84174b
/usr/lib/.build-id/5e/a3b63c402bc4ede3e624dae4bba674c1a5cbdf
/usr/lib/.build-id/97/d094c5e36ef7995b54069b3c2a6f699d4df640
/usr/lib/systemd/system/docker.service
/usr/lib/systemd/system/docker.socket
/usr/libexec/docker/docker-init
/usr/share/man/man8/dockerd.8.gz

某些内核/cgroup 环境会显示:

WARNING: No swap limit support

它表示相应限制能力不可用,不等于宿主机没有 swap。先查当前内核、cgroup v1/v2 和控制器支持,不要对现代系统无条件追加旧内核启动参数。

参考:https://docs.docker.com/engine/containers/resource_constraints/、https://docs.docker.com/engine/daemon/troubleshoot/。

原稿针对需要旧式 memory/swap accounting 启动参数的环境给出以下入口;应保留已有内核参数,只添加已经确认需要的项。

Ubuntu 的 /etc/default/grub:

GRUB_CMDLINE_LINUX="<原有参数> cgroup_enable=memory swapaccount=1"
update-grub
reboot

Rocky/RHEL 原稿的示例包含原 LVM 参数:

GRUB_CMDLINE_LINUX="... rd.lvm.lv=rl_bogon/swap cgroup_enable=memory swapaccount=1"
grub2-mkconfig -o /boot/grub2/grub.cfg
reboot

<原有参数>、... 是说明占位,不能原样写入。Rocky 的 BIOS/UEFI、BLS 与 grubby 管理方式因版本而异,应按发行版确认真正的更新入口;不要写错引导配置或把另一台机器的 LVM 参数复制过来。

6. 存储驱动实验

6.1 存储驱动的职责

存储驱动/快照器管理镜像层和容器可写文件系统。经典 Docker Linux 存储可使用 overlay2、btrfs、zfs、vfs 等;rootless 环境还可能涉及 fuse-overlayfs。具体支持范围、默认驱动和 containerd image store 状态应以当前 Engine、内核与文件系统为准。

驱动 特点
overlay2 基于内核 OverlayFS,为经典 Linux Docker 安装常用选择
fuse-overlayfs 某些 rootless/内核环境使用的用户态实现,是否需要依赖实际支持
btrfs / zfs 利用相应后备文件系统的快照等能力,需正确规划和维护
vfs 使用目录深复制,不使用同样的 CoW 机制,兼容性好但磁盘和 I/O 开销较大,适合测试

更换驱动会使原驱动下的镜像和容器暂时不可见,不等于自动把旧数据物理删除。迁移前备份镜像、卷及运行配置,规划回退。

参考:https://docs.docker.com/engine/storage/drivers/、https://docs.docker.com/engine/storage/drivers/overlayfs-driver/、https://docs.docker.com/engine/storage/drivers/vfs-driver/。

6.2 查看默认布局

docker info | grep -i driver
ls /var/lib/docker
ls /var/lib/docker/overlay2
ls /var/lib/docker/overlay2/l

原稿:

Storage Driver: overlay2
Logging Driver: json-file
Cgroup Driver: systemd

buildkit containers engine-id image network overlay2 plugins runtimes swarm tmp volumes

/var/lib/docker/overlay2/:
backingFsBlockDev l
目录 含义
buildkit 构建器相关数据
containers 容器元数据和与日志驱动有关的数据
engine-id 引擎标识
image、overlay2 经典镜像元数据与分层存储
network 网络管理数据
plugins、runtimes 插件和运行时相关数据
swarm Swarm 状态
tmp 临时数据
volumes 本地卷相关数据

overlay2/l/ 中是用于缩短引用路径的符号链接,不是硬链接。Docker 的内部存储不应直接手工修改。

6.3 准备实验磁盘

原稿给虚拟机添加独立 20 GiB 磁盘;若启动顺序被改变,应在 BIOS 中把真正系统盘放回前面。

添加独立实验磁盘

调整虚拟机 BIOS 启动顺序

fdisk -l | grep dev

原设备记录:

/dev/nvme0n1                 200 GiB   214748364800 bytes   419430400 sectors
/dev/nvme0n1p1               2048 .. 2099199       1G Linux
/dev/nvme0n1p2               2099200 .. 419430399 199G Linux LVM
/dev/mapper/rl_bogon-root    70 GiB    75161927680 bytes
/dev/mapper/rl_bogon-swap    2.02 GiB  2164260864 bytes
/dev/mapper/rl_bogon-home    126.98 GiB 136344240128 bytes
/dev/sda                     20 GiB   21474836480 bytes   41943040 sectors

设备名只属于原实验:以下 /dev/sda 必须确认是独立、无需保留数据的实验盘,格式化不是普通查看命令。

6.4 XFS 的 d_type / ftype

overlay2 使用 XFS 后备文件系统时需要支持 d_type,通常表现为 ftype=1。这个要求来自 OverlayFS,不是 vfs 自身必须使用 XFS。

# 仅对应已核实为空的实验盘
mkfs.xfs -n ftype=1 /dev/sda

原格式化输出:

meta-data=/dev/sda  isize=512 agcount=4 agsize=1310720 blks
                 sectsz=512 attr=2 projid32bit=1
                 crc=1 finobt=1 sparse=1 rmapbt=0
                 reflink=1 bigtime=1 inobtcount=1 nrext64=0
data             bsize=4096 blocks=5242880 imaxpct=25
                 sunit=0 swidth=0 blks
naming=version 2 bsize=4096 ascii-ci=0 ftype=1
log=internal log bsize=4096 blocks=16384 version=2
                 sectsz=512 sunit=0 blks lazy-count=1
realtime=none    extsz=4096 blocks=0 rtextents=0

修正原稿的挂载顺序:先停止相关实验容器和 daemon,避免在使用中的存储目录上覆盖挂载。下面的独立空文件系统会遮住 /var/lib/docker 原目录内容,但不会自动搬迁旧数据。

systemctl stop docker.service docker.socket
mount /dev/sda /var/lib/docker
ls /var/lib/docker

该命令只建立当前挂载,持久化挂载另需按实际 UUID 和系统策略配置;原稿未提供 fstab 内容。

6.5 切换到 vfs 并观察

实验 daemon.json:

{
  "registry-mirrors": ["https://docker.xuanyuan.me"],
  "storage-driver": "vfs"
}
systemctl start docker
docker info | grep -i driver
ls /var/lib/docker
ls /var/lib/docker/vfs
docker pull busybox
ls /var/lib/docker/vfs
ls /var/lib/docker/vfs/dir

原记录:

Storage Driver: vfs
Logging Driver: json-file
Cgroup Driver: systemd

buildkit containers engine-id image network plugins runtimes swarm tmp vfs volumes

# 拉取前 vfs 中
backingFsBlockDev

Using default tag: latest
latest: Pulling from library/busybox
90b9666d4aed: Pull complete
Digest: sha256:f85340bf132ae937d2c2a763b8335c9bab35d6e8293f70f606b9c6178d84f42b
Status: Downloaded newer image for busybox:latest
docker.io/library/busybox:latest

# 拉取后
backingFsBlockDev dir

e8b88f6b47377e7803c60bcb49815b57298772515980f50d25f8f239291fbe86
ls /var/lib/docker/vfs/dir/e8b88f6b47377e7803c60bcb49815b57298772515980f50d25f8f239291fbe86/
ls -l /var/lib/docker/vfs
du -sh /var/lib/docker/vfs
docker images

原输出:

bin dev etc home lib lib64 root tmp usr var

brw-------. 1 root root 8,0 ... backingFsBlockDev
drwx--x---. 3 root root 78  ... dir

4.2M /var/lib/docker/vfs
REPOSITORY   TAG      IMAGE ID       CREATED       SIZE
busybox      latest   6d3e4188a38a   9 months ago  4.28MB

vfs 为各层建立目录快照,通常深复制已有内容,不能把每个目录理解为仅保存增量差异。此单层 BusyBox 样本的容量接近不证明多层镜像的磁盘利用率相同。

6.6 恢复原环境

直接卸载可能遇到:

umount: /var/lib/docker/: 目标忙.

修正后的顺序是停止 daemon/socket、卸载独立实验盘,再去掉 storage-driver 配置并启动。若仍忙,检查挂载和占用进程,不强行忽略。

systemctl stop docker.service docker.socket
umount /var/lib/docker

恢复原配置,例如:

{
  "registry-mirrors": ["https://docker.xuanyuan.me"]
}
systemctl start docker
docker info | grep -i driver
ls /var/lib/docker

原稿恢复后为 overlay2,目录重新含 overlay2。实际默认驱动仍以原配置和当前 Engine 存储模式为准。

7. 远程管理与 TLS

7.1 Docker 与 containerd 的代理补充

Docker 代理设置见第 3 章。独立管理的 containerd 服务若确实需要代理,应给自己的 systemd 服务配置,不会因为 dockerd 设置过代理而自动获得相同环境。

PROXY_SERVER_IP=10.0.0.1
PROXY_PORT=10809
mkdir -p /etc/systemd/system/containerd.service.d
cat > /etc/systemd/system/containerd.service.d/http-proxy.conf <<PROXYCONF
[Service]
Environment="HTTP_PROXY=http://${PROXY_SERVER_IP}:${PROXY_PORT}/"
Environment="HTTPS_PROXY=http://${PROXY_SERVER_IP}:${PROXY_PORT}/"
Environment="NO_PROXY=localhost,127.0.0.0/8,172.17.0.0/16,10.0.0.0/24,10.244.0.0/16,192.168.0.0/16,wang.org,cluster.local"
PROXYCONF
systemctl daemon-reload
systemctl restart containerd.service

这会影响相关容器运行时服务,应在对应实验环境操作;此处仅记录命令,未执行。

7.2 远程访问方式与配置冲突

远程管理用于跨机器操作、CI 工具连接及集中管理。Docker API 管理权限很高,原稿演示的未认证 TCP 2375 只适合理解隔离实验中的行为,不应直接公开到公网。可采用 SSH,或配置服务端/客户端证书相互认证的 TLS。

两种监听来源:

{
  "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]
}
[Service]
ExecStart=/usr/bin/dockerd -H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375

同一个 hosts 参数不能既由 daemon.json 又由 -H 启动参数提供。Unix socket 保留本地访问;0.0.0.0 是监听全部 IPv4 地址,不是客户端应该连接的目标地址。

可使用的帮助入口包括:

docker --help
dockerd --help
man docker
man dockerd

原稿称“只能 man 查看,不能 --help”不正确。客户端 docker -H 与服务端 dockerd -H 分别选择连接目标和监听地址。历史默认 TCP 端口常见未启用 TLS 时 2375、启用 TLS 时 2376;是否启用认证由实际配置决定,端口号自身不提供安全。

7.3 Rocky:通过 daemon.json 监听

原稿直接编辑包管理服务文件,容易被升级覆盖。下面用 drop-in 清除原 ExecStart,再保留 containerd 参数、去掉 -H fd://,让 hosts 由 JSON 提供。

mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/remote-api.conf <<'UNITCONF'
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --containerd=/run/containerd/containerd.sock
UNITCONF

将以下设置与已有配置合并:

{
  "registry-mirrors": ["https://docker.xuanyuan.me"],
  "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]
}
systemctl daemon-reload
systemctl restart docker
ss -lntp
docker info

原稿监听记录:

tcp6 0 0 :::2375 :::* LISTEN 2990/dockerd

端口是 2375,原稿“2357”为笔误。旧版 Engine 的警告:

[DEPRECATION NOTICE]: API is accessible on http://0.0.0.0:2375 without encryption.
Access to the remote API is equivalent to root access on the host.
In future versions this will be a hard failure preventing the daemon from starting!

实际拒绝启动或警告行为随版本变化,不要为了消除提示而忽略认证。参考:https://docs.docker.com/engine/security/protect-access/。

7.4 客户端连接

# 服务器本地 Unix socket
docker -H unix:///var/run/docker.sock ps

# 原实验中另一台主机访问 TCP API
docker -H tcp://10.0.0.12:2375 version

原结果客户端/服务端均为 Docker 28.2.2。

通过 SSH 连接无需开放 Docker 的未认证 TCP 监听:

ssh-keygen
ssh-copy-id root@10.0.0.12
docker -H ssh://root@10.0.0.12 version

这是原实验的 root 账号示例;使用其他账号时需具有远端 socket 的合法访问权限。Docker Context 只是保存连接配置,安全性来自底层 SSH 或 TLS,而不是 context 名本身。

7.5 Ubuntu:通过服务参数监听

原实验 Ubuntu 使用二进制路径 /data/server/docker/dockerd,不是所有 Ubuntu 默认都没有 fd://;包安装和自定义服务文件可能不同,先查看真实服务单元。

[Service]
ExecStart=
ExecStart=/data/server/docker/dockerd -H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375

此 drop-in 与上一节是另一种方案,不要叠加冲突的 remote-api.conf/hosts 配置。若实际二进制为 /usr/bin/dockerd,使用相应路径并保留原服务需要的其他参数。

systemctl daemon-reload
systemctl restart docker.service
ss -lntp

原监听:

tcp6 0 0 :::2375 :::* LISTEN 3180/dockerd

Rocky 客户端查询:

docker -H tcp://10.0.0.13:2375 version

原结果客户端/服务端均为 28.2.2。

7.6 REST API 查询

API 默认常通过 Unix socket 提供,也可在显式配置后通过 TCP 提供;它支持容器、镜像、网络和卷等操作。

apt install -y jq
# 明确连接本机地址,不使用 0.0.0.0 作为目标
curl -s http://127.0.0.1:2375/version | jq

# 不开 TCP 监听也可查询本地 API
curl --unix-socket /var/run/docker.sock http://localhost/version | jq

原稿响应字段:

{
  "Platform": {"Name": "Docker Engine - Community"},
  "Components": [{"Name": "Engine"}],
  "Version": "28.3.0",
  "ApiVersion": "1.51",
  "MinAPIVersion": "1.24",
  "GitCommit": "265f709",
  "GoVersion": "go1.24.4",
  "Os": "linux",
  "Arch": "amd64",
  "KernelVersion": "6.11.0-28-generic",
  "BuildTime": "2025-06-24T15:44:12.000000000+00:00"
}

上面省去原响应中未展开的 Components 细节,保留原稿给出的字段,不把它当成新版 API 的固定结构。

7.7 TLS 实验入口

原稿引用 scripts/deploy_docker_tls.sh,没有提供脚本源代码。它计划生成 CA、服务端和客户端证书并修改 daemon;仅凭没有 warning 不能证明双向 TLS 已正确启用,应核对服务端验证配置、证书用途、SAN、私钥权限和真实连接结果。

/bin/bash deploy_docker_tls.sh server 10.0.0.12

原稿输出:

正在生成 TLS 证书...
Certificate request self-signature ok
subject=CN=*
Certificate request self-signature ok
subject=CN=client
证书生成完成!
正在配置 Docker 守护进程...
重启 Docker 服务...
Docker 服务已重启!

服务端证书需要包含客户端实际访问的 IP/DNS SAN,单独 CN=* 不能保证现代客户端验证通过。只有完整脚本和证书实际符合要求后才使用。

配置客户端的入口:

/bin/bash deploy_docker_tls.sh client 10.0.0.13

原交互:

正在配置客户端...
Docker服务器地址是(10.0.0.12):
请复制如下命令进行测试

按原稿,把客户端认证文件传到 10.0.0.13:

ssh root@10.0.0.13 mkdir -p /root/.docker
scp /etc/docker/certs.d/{ca.pem,cert.pem,key.pem} root@10.0.0.13:/root/.docker/

在客户端执行:

docker --tlsverify \
  --tlscacert=/root/.docker/ca.pem \
  --tlscert=/root/.docker/cert.pem \
  --tlskey=/root/.docker/key.pem \
  -H tcp://10.0.0.12:2375 info

原脚本沿用 2375 做 TLS,并非 TLS 必须用 2375;若服务端按惯例改为 2376,客户端也要改为同一端口。TLS 是否有效由握手和认证决定,不由“端口看起来像 TLS”决定。CA 私钥不属于要发送给客户端的文件。

8. 镜像结构与镜像仓库

8.1 镜像分层与 overlay2

镜像是标准化的文件系统差异层与配置;overlay2 是经典 Docker 存储驱动对 Linux OverlayFS 的使用方式,不是内核里一个名为 overlay2 的独立文件系统。相同镜像也可以由其他受支持的快照/存储机制管理。

镜像层只读,容器增加可写层;底层内容需要修改时,OverlayFS 可能把文件 copy-up 到上层,然后修改上层。上层同路径内容会遮住下层,删除也可以用 whiteout 表达;因此不能说联合挂载“从不覆盖任何内容”。

多个容器可以共享镜像只读层,各自有可写层。构建缓存复用取决于输入和依赖,不是每条 Dockerfile 指令都对应文件系统差异层,也不能认为只重建一条变化指令而后续永不失效。

对比 镜像分层 overlay2
定位 镜像内容组织与分发模型 本地经典存储驱动
内容 文件系统差异层与配置、manifest 等 将已有层与可写层呈现为容器文件系统
机制 内容寻址、复用、构建缓存 联合挂载、copy-up、whiteout 等
收益 减少重复存储和传输 减少整棵根文件系统复制

8.2 rootfs 与容器

普通 Linux 容器镜像提供用户态根文件系统内容,不为每个容器携带并启动独立内核。原稿用 bootfs/rootfs 分层示意来解释共享内核;这不是对所有 Linux 启动流程或镜像格式的严格定义,不能把 kernel/bootloader 当成普通容器镜像的一层。

根文件系统与共享内核示意

/dev、/proc、/sys 等目录中的运行时内容通常由容器运行时挂载或生成。镜像体积受版本、架构、工具和压缩格式影响,原稿 BusyBox 1.22 MB、Alpine 5 MB、Ubuntu 60 MB、CentOS 200 MB 只是不同历史样本。

镜像层和容器可写层

父镜像为构建提供已有层和默认配置;子镜像在此基础上添加变化。容器由镜像创建;修改容器可通过 commit 生成新镜像,或优先把修改写入 Dockerfile 重新构建。重启同一个容器通常不会丢弃其可写层,删除重建才需要重新取得运行数据。

8.3 Nginx 分层实验

docker images
docker pull nginx
docker images
docker history nginx
docker image inspect nginx

初始只有表头,拉取记录:

Using default tag: latest
latest: Pulling from library/nginx
dad67da3f26b: Pull complete
4eb3a9835b30: Pull complete
021db26e13de: Pull complete
397cc88dcd41: Pull complete
5f4a88bd8474: Pull complete
66467f827546: Pull complete
f05e87039331: Pull complete
Digest: sha256:dc53c8f25a10f9109190ed5b59bda2d707a3bde0e45857ce9e1efaa32ff9cbc1
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest

REPOSITORY   TAG      IMAGE ID       CREATED     SIZE
nginx        latest   9a9a9fd723f1   2 days ago  192MB

Nginx 构建历史

原稿镜像详情中的配置与标识:

{
  "Id": "sha256:9a9a9fd723f1d2ba52b914ece050f298eec04ef490a9065c52805e46779c4c43",
  "RepoTags": ["nginx:latest"],
  "RepoDigests": ["nginx@sha256:dc53c8f25a10f9109190ed5b59bda2d707a3bde0e45857ce9e1efaa32ff9cbc1"],
  "Parent": "",
  "Comment": "buildkit.dockerfile.v0",
  "Created": "2025-06-24T20:52:14Z",
  "DockerVersion": "",
  "Author": "",
  "Config": {
    "ExposedPorts": {"80/tcp": {}},
    "Env": [
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "NGINX_VERSION=1.29.0",
      "NJS_VERSION=0.9.0",
      "NJS_RELEASE=1~bookworm",
      "PKG_RELEASE=1~bookworm",
      "DYNPKG_RELEASE=1~bookworm"
    ],
    "Entrypoint": ["/docker-entrypoint.sh"],
    "Cmd": ["nginx", "-g", "daemon off;"],
    "Labels": {"maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"},
    "StopSignal": "SIGQUIT"
  },
  "Architecture": "amd64",
  "Os": "linux",
  "Size": 192226221,
  "RootFS": {
    "Type": "layers",
    "Layers": [
      "sha256:7fb72a7d1a8e984ccd01277432de660162a547a00de77151518dc9033cfb8cb4",
      "sha256:151f9feea56362593fb4b85e2020dc8c8b69d33703c6d5cf2c8769b190cc54b3",
      "sha256:13fcb2d303e83bc04c39fdd29bd0df540067619915b5deac96fc853d5c404c47",
      "sha256:6380429cac565ac597936095d7643e6fca83c978bf2ea73078e54ed571c99d2f",
      "sha256:126eaee18409d97a383bda20c227c11cbae5a58cf204af68721dac5e0dd2be53",
      "sha256:d35594dd7e6de0e30f5d19afb38855299aec1ef5cbb70f97539eca365cab1e46",
      "sha256:cd38dca3d98283db306a43da1a7e80d6bcb722ca701ce01b91dadc6baabf8235"
    ]
  },
  "Metadata": {"LastTagTime": "0001-01-01T00:00:00Z"}
}

GraphDriver 部分另列为 JSON,便于阅读原稿长路径;它们只是该实验主机的内部位置,不应复制到另一台主机配置里:

{
  "Name": "overlay2",
  "Data": {
    "LowerDir": "/var/lib/docker/overlay2/45b817b58eb9ec12d1c6d5db87d8dec3580fca2c8b1c2bc7083562e367f74098/diff:/var/lib/docker/overlay2/2aa20330c6f353ce7faf6a6faf67c415f6709bc5ee0f2cadaa8894863c692c34/diff:/var/lib/docker/overlay2/8af8b0582053d09e6f0ccab76ec2acbdbbd52902cdbf1534452ba9ae0c1a20dc/diff:/var/lib/docker/overlay2/f4eaea38d360fa0330a71c44256b5a65c8a4393bfa8da1b679cad1d5e11acaa2/diff:/var/lib/docker/overlay2/8e189bb1298b2644b58ba1595b589211c82d771bd67e434918af59e99613540a/diff:/var/lib/docker/overlay2/49e54dd7c264c9b30d25883ce5f065804d0934b342051faf268428ecdf227777/diff",
    "MergedDir": "/var/lib/docker/overlay2/fec9ac6936916cf7f2b89976fcdc089cb86788c51e588110c8de46b4aad62d47/merged",
    "UpperDir": "/var/lib/docker/overlay2/fec9ac6936916cf7f2b89976fcdc089cb86788c51e588110c8de46b4aad62d47/diff",
    "WorkDir": "/var/lib/docker/overlay2/fec9ac6936916cf7f2b89976fcdc089cb86788c51e588110c8de46b4aad62d47/work"
  }
}

镜像 ID、仓库摘要和层 DiffID 是不同对象的摘要,不应因为同为 sha256 就混为一谈。inspect 字段也会随存储模式和 API 版本变化。

8.4 保存归档并观察布局

docker save -o nginx.tar nginx
mkdir -p data
tar xf nginx.tar -C data
ls -l data
ls -l data/blobs
ls -l data/blobs/sha256

原稿将归档命名为 nginx.tar.gz,大小 196392960 字节,但 docker save -o 不会按后缀自动 gzip;上面更名为 tar。需要压缩应使用:

docker save nginx | gzip > nginx.tar.gz

原稿解包后的列表:

drwxr-xr-x. 3 root root   20 ... blobs
-rw-r--r--. 1 root root  362 ... index.json
-rw-r--r--. 1 root root 2286 ... manifest.json
-rw-r--r--. 1 root root   31 ... oci-layout
-rw-r--r--. 1 root root   88 ... repositories

blobs/:
drwxr-xr-x. 2 root root 4096 ... sha256

blobs/sha256/:
-rw-r--r--. 1 root root 1127 ... 0ca7b78ec97fca54170e751d074142fa02f5ec6669df8048adf0bfc292b5d42c
...
-rw-r--r--. 1 root root 482 ... fcd3176f43badcbf6aa68dd3eb303c36fade3a26eeb7ab4d045e4bcd038d6c16

blobs 可能包含层、镜像配置和 manifest 等多种内容,不是每个 blob 都是文件系统层。index.json、manifest.json 描述不同格式的索引/引用关系;oci-layout 描述 OCI 布局版本;repositories 是兼容 Docker 归档格式的仓库映射,不是 OCI 的必需文件。具体归档结构随 Engine 和输出格式变化。

8.5 镜像仓库网站

正式 Docker Hub:https://hub.docker.com/。它支持发布、检索、拉取和管理公共/私有镜像,镜像来源应区分官方、上游维护者和普通用户。

Docker Hub 首页

原 PDF 此首页插图重复一次,images/image-025.png 保留为相同原图,不再重复展示。

原稿还列出 http://dockerhub.com 与 https://hub-stage.docker.com/。应优先使用正式域名;测试/预发布站点的可达性和用途不构成生产使用承诺,本次未联网核验。

Docker Hub 界面示例

其他仓库与资料入口:

镜像软件站、Registry 服务和代理加速器不是同一概念。第三方服务的内容、认证、时效及可用性需分别确认。

8.6 在 Docker Hub 查找 Nginx

搜索 nginx,区分结果中的官方镜像、下载热度和具体发布者。

Docker Hub 搜索

进入镜像概述查看使用方法、支持标签和平台。

Nginx 镜像概述

【Official Images】可打开官方镜像列表,不代表搜索出的所有相似名字都由 Docker 或 Nginx 维护。

Official Images 列表

【Tag summary】只显示摘要;完整标签和平台信息在相应 Tags 页面查看。

Nginx 标签摘要入口

Tags 标签列表

镜像信息旁给出拉取命令;点击标签/摘要可进一步检查层及详情。

镜像标签详情

8.7 Alpine 与 BusyBox

原稿“Alpha 镜像”实际指 Alpine。

Alpine 官网

Alpine 使用 musl libc、BusyBox 和 apk,镜像较小,适合能够兼容其用户态环境的应用;glibc 应用不一定可直接运行。体积小不等于自动没有安全问题,也没有“所有应用都应从 Ubuntu 改用 Alpine”的通用要求。

参考:

BusyBox 将 ls、cp、mv、grep、ping 等许多工具做成一个紧凑可执行文件;具体 applet 由构建决定,不等于完整 GNU/Linux 工具集。常用于临时调试、轻型 HTTP 服务或脚本、嵌入式用户态环境;容器仍需要宿主机内核。

镜像引用 定位与限制
docker.io/library/busybox 官方镜像,常为数 MB,平台和 libc 变体需核对标签
docker.io/radial/busyboxplus 第三方扩展镜像,不能仅凭名字保证有 opkg、nginx 或 Python;应查看标签、构建文件和内容
arm64v8/busybox 面向 ARM64 的镜像命名空间;现代官方 busybox 多平台标签通常也可选择 ARM64
docker.io/networkstatic/busybox 原稿列举的第三方网络调试镜像,工具与维护状态需另行确认

原稿给官方 BusyBox 约 1~4 MB、busyboxplus 约 5~10 MB 等大小,都是版本相关样本,不能作为固定容量保证。

9. 镜像命令扩展

9.1 标签与无标签镜像

标签不能写成空字符串,例如:

docker tag nginx:latest nginx:

原错误:

Error parsing reference: "nginx:" is not a valid repository/tag: invalid reference format

完全不提供 tag 与显式写空冒号不同:nginx 通常按 nginx:latest 解释。未命名的本地镜像可显示 <none>:<none>,可按镜像 ID 运行,并非不能使用。pull 支持标签或 digest;不能说任何拉取都必须有 tag。

情形 说明
同标签重新构建/拉取 旧镜像可能失去原标签
中间构建结果 可由构建器缓存管理,不一定都作为普通镜像列出
commit 不提供目标名称 可生成只有 ID 的镜像
删除某个标签 若还有其他引用或容器使用,不一定立即删除内容
构建中断 可能留下缓存等数据,不保证必然产生悬空镜像
tag 操作 增加新引用,不会自动删除源标签

9.2 自定义镜像显示

常用模板字段:{{.Repository}}、{{.Tag}}、{{.ID}}、{{.Digest}}、{{.CreatedAt}}、{{.Size}}。

docker images --format '{{.Repository}}\t{{.Tag}}\t{{.Size}}'
docker images --format '{{.CreatedAt}}\t{{.Repository}}:{{.Tag}}' | sort -k1 -r
docker images --format '{{.Repository}}:{{.Tag}}'

原名称列表:

nginx:latest
alpine:latest
redis:latest
busybox:latest
web-tomcat:v1
web-django:v1
web-nginx:v1
docker images --format '{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}'
nginx:latest    192MB   2025-06-25 04:52:14 +0800 CST
alpine:latest   8.31MB  2025-05-31 00:20:41 +0800 CST
redis:latest    128MB   2025-05-30 00:02:07 +0800 CST
busybox:latest  4.28MB  2024-09-27 05:31:42 +0800 CST
web-tomcat:v1   458MB   2024-01-16 09:09:19 +0800 CST
web-django:v1   436MB   2024-01-15 22:51:50 +0800 CST
web-nginx:v1    187MB   2024-01-15 22:50:23 +0800 CST
docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}'
REPOSITORY   TAG      IMAGE ID       SIZE
nginx        latest   9592f5595f2b   192MB
alpine       latest   cea2ff433c61   8.31MB
redis        latest   ed3a2af6d0d4   128MB
busybox      latest   6d3e4188a38a   4.28MB
web-tomcat   v1       299452ffece3   458MB
web-django   v1       d001c15a4640   436MB
web-nginx    v1       48d5de12bb81   187MB

原稿不同实验时间的 Nginx ID 不相同,不能把 latest 当成固定不可变版本。

docker images --format '{"name":"{{.Repository}}","tag":"{{.Tag}}","size":"{{.Size}}"}'
docker images --format '{"name":"{{.Repository}}","tag":"{{.Tag}}"}' | jq .

原逐行 JSON:

{"name":"nginx","tag":"latest","size":"192MB"}
{"name":"alpine","tag":"latest","size":"8.31MB"}
{"name":"redis","tag":"latest","size":"128MB"}
{"name":"busybox","tag":"latest","size":"4.28MB"}
{"name":"web-tomcat","tag":"v1","size":"458MB"}
{"name":"web-django","tag":"v1","size":"436MB"}
{"name":"web-nginx","tag":"v1","size":"187MB"}

jq 会把每行独立对象格式化,不自动把它们合成一个 JSON 数组。

9.3 大小统计与过滤

docker images --format '{{.Size}}'
docker system df
docker system df -v

上面七项 SIZE 的数值均以 MB 表示,原稿简单相加为 1413.59MB。原演示使用:

# 仅能演示这个“所有值都为 MB”的样本求和,不能作为通用磁盘统计
# docker images --format '{{.Size}}' | awk -F'M' '{sum+=$1} END {print sum "MB"}'

真正输出可能混有 kB、MB、GB,镜像又可能共享层或有多个标签;这个和不代表磁盘独占占用。应通过 system df 观察共享和可回收空间。

过滤语法:

docker images --filter 'dangling=true'
docker images -f 'dangling=false'

默认 docker images 也可能显示悬空镜像,并非默认等于 dangling=false。悬空对象与构建缓存的分类依存储后端/版本变化;只看到 <none> 不能断言它无用或无法运行。

原稿过滤样本:

REPOSITORY   TAG      IMAGE ID       CREATED          SIZE
<none>       <none>   e395fa7ced4e   28 minutes ago   631MB
<none>       <none>   0b75057d6a95   28 minutes ago   631MB
<none>       <none>   82053db84dd7   32 minutes ago   627MB
<none>       <none>   c173bb524ea7   32 minutes ago   627MB
<none>       <none>   ed55b7e58a39   32 minutes ago   627MB
<none>       <none>   46c144c7b667   32 minutes ago   627MB
<none>       <none>   cff6fceebb23   33 minutes ago   627MB

10. 容器状态、启动与查询

10.1 生命周期

容器生命周期状态转换

状态/事件 含义及操作
created create 已完成,但主进程尚未启动;docker start 启动
running 主进程运行中
paused pause 冻结任务,unpause 恢复
exited 主进程结束,容器元数据/可写层仍在;可 start 或 rm
restarting 正按重启策略重启
removing / dead 删除中或异常未完全清理的状态,需结合具体错误处理
删除完成 对象已移除,不再作为普通 inspect 状态存在;卷和绑定目录不一定一并删除

原图的 stopped 是教学上的“已停止”,实际常见状态为 exited;die、oom 是事件/状态属性,不能都当成 .State.Status 的独立枚举。OOM 后是否退出、重启要看被杀进程和策略。

create → start → running
running → pause → paused → unpause → running
running → 主进程退出/stop/kill → exited
exited → start → running
created/exited → rm → 删除
running → rm -f → 强制终止并删除

重启策略有 no、on-failure[:N]、always、unless-stopped;手动 stop 会暂停自动重启,always 在 daemon 再启动后与 unless-stopped 的行为不同。重启策略不是服务高可用保证。

10.2 启动流程和参数

容器创建与启动流程

整体包括检查/按策略拉取镜像、创建容器配置和可写文件系统、准备网络及挂载、设置 namespace/cgroup、安全约束,最后执行入口命令。实际执行顺序由 Engine/runtime 实现协调,不能把“create 已分配好运行进程 PID”当成所有场景的既定事实。

docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
类别 参数 用途
启动 -d, --detach 后台运行并返回 ID
启动 --rm 退出后删除容器及相关匿名卷,具名卷除外
启动 --name 自定义名称
启动 --restart 重启策略,不能与 --rm 共用
进程 -i / -t 保持标准输入 / 分配伪终端
进程 --init 使用轻量 init 处理信号转发及孤儿/僵尸回收
进程 --entrypoint 覆盖入口可执行程序
进程 --user 1000:1000 设置 UID:GID
镜像 --pull always/missing/never 运行前拉取策略,默认通常 missing
镜像 --platform linux/amd64 指定平台,仍需支持该平台运行
镜像 --disable-content-trust 历史内容信任签名流程开关,不是关闭镜像层摘要完整性校验
网络 -p 发布指定端口
网络 -P 将 EXPOSE 等声明的暴露端口发布为随机宿主机端口
网络 --network 网络模式/网络连接
网络 --dns 自定义 DNS 服务器
网络 --ip 在支持静态地址的自定义网络中指定 IP
存储 -v、--mount 卷或绑定挂载
存储 --tmpfs 临时内存文件系统;可能涉及 swap,不能保证所有字节永不落盘
存储 --volumes-from 复用另一个容器的卷挂载定义
资源 -m, --memory 内存限制
资源 --cpu-shares CPU 竞争时相对权重,不是固定 CPU 配额
资源 --cpus CPU 时间配额,不必固定到指定物理核心
资源 --pids-limit 任务数限制,线程通常也计入
安全 --privileged 大幅扩展容器权限
安全 --cap-add、--cap-drop 增减 Linux capability
安全 --security-opt seccomp、LSM 和其他支持的安全选项
资源 --cgroup-parent 使用符合驱动格式的父 cgroup/slice
日志 --log-driver、--log-opt 选择日志驱动和选项
健康 --health-cmd、--health-interval 健康检查命令和间隔
namespace --pid container:web 与指定容器共享 PID namespace
namespace --ipc host/private/... IPC namespace 模式
namespace --uts host 共享宿主机 UTS namespace

--cap-drop 是独立参数,不能笼统说所有 capability 都通过 security-opt 配置;设置系统时间的能力为 SYS_TIME,不是原稿的 SETTIME。默认能力通常已不含 SYS_TIME。配置 seccomp 可限制系统调用。

cgroup-parent 用于层级资源管控,但共享父组本身不自动产生 CPU/内存限制,仍需给父层设置额度;现代 Kubernetes 使用 CRI 运行时,不能把所有 Docker 容器默认都归属 kubepods。

10.3 ps 查询

docker ps -a
docker ps -q
docker ps -a -q
docker ps -aq
docker ps -l
docker ps --latest

-a 是 ps 的“列出全部”,不是 run 的同义选项。默认 ps 只列运行容器;-q 只输出 ID;-l 为最近创建的容器,未必运行中。

原样本:

CONTAINER ID   IMAGE   COMMAND                  CREATED         STATUS                     PORTS   NAMES
f5229c2e1d2d   nginx   "/docker-entrypoint.…"   51 seconds ago  Up 50 seconds              80/tcp  inspiring_wright
407ab2601c03   nginx   "/docker-entrypoint.…"   2 minutes ago   Exited (0) About a minute ago       loving_ishizaka

80/tcp 是容器暴露端口信息,没有宿主机端口映射。是否能从宿主机直接到容器 IP 访问还取决于运行平台、路由和防火墙,不能据此说局域网都已可访问。

# docker ps -q
f5229c2e1d2d
# docker ps -aq
f5229c2e1d2d
407ab2601c03

条件过滤

docker ps --filter 'status=running'
docker ps -a --filter 'status=exited'
docker ps -a --filter 'status=created'
docker ps --filter 'name=web'
docker ps --filter 'name=^/web$'
docker ps --filter 'label=app=web'
docker ps --filter 'label=app'
docker ps -a --filter 'since=nginx'
docker ps -a --filter 'before=db'
docker ps --filter 'status=running' --filter 'label=env=prod'
docker ps -f 'name=inspiring_wright'

不同键通常组合为 AND,同一个键重复可能为 OR;以具体过滤器为准。名称精确匹配常需考虑内部名称前缀 /。

ps 不支持原稿的 until=5m、until=24h 时间过滤,until 也不是“运行不足五分钟”。要按时间删除已停止容器,支持的入口为:

# 会删除满足条件的已停止容器;它不是只读查询
docker container prune --filter 'until=24h'

仅观察创建/启动时间时可用 inspect 提取 .Created、.State.StartedAt,不要把 prune 当查看命令。

定制输出

模板字段包括 ID、Image、Command、CreatedAt、RunningFor、Ports、Status、Names、Labels、Mounts、Networks。

docker ps --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}'
docker ps --format '{{.ID}}: {{.Names}} ({{.Status}})'
docker ps --format json
docker ps --format json | jq

原稿:

CONTAINER ID   NAMES          STATUS        PORTS
f73428f6a34e   modest_kilby   Up 3 minutes  80/tcp
f73428f6a34e: modest_kilby (Up 4 minutes)

原单条 JSON 格式化后:

{
  "Command": "\"/docker-entrypoint.…\"",
  "CreatedAt": "2025-07-08 15:07:59 +0800 CST",
  "ID": "f73428f6a34e",
  "Image": "nginx",
  "Labels": "maintainer=NGINX Docker Maintainers <docker-maint@nginx.com>",
  "LocalVolumes": "0",
  "Mounts": "",
  "Names": "modest_kilby",
  "Networks": "bridge",
  "Platform": null,
  "Ports": "80/tcp",
  "RunningFor": "4 minutes ago",
  "Size": "0B",
  "State": "running",
  "Status": "Up 4 minutes"
}

10.4 inspect

docker inspect [OPTIONS] NAME|ID [NAME|ID...]

--format/-f 用 Go 模板提取;--type 可明确 container、image、network、volume 等对象类型。默认结果是 JSON 数组,即使只查一个对象,也需注意 jq 的 [0] 索引。

docker inspect sharp_gates

完整容器 inspect 输出

原 PDF 将同一长图跨页重复展示,原文件 image-038.png 仍保留,这里只展示一次。

原稿按旧版默认 bridge 的字段 .NetworkSettings.IPAddress 查询得到 172.17.0.2;新版该字段可能为空或移除,按网络名称查询更可靠:

docker inspect -f '{{.NetworkSettings.Networks.bridge.IPAddress}}' sharp_gates
docker inspect -f '{{(index .NetworkSettings.Networks "app-net").IPAddress}}' sharp_gates

容器网络字段

查询容器创建时间,不是镜像创建时间:

docker inspect --format '{{.Created}}' sharp_gates
2025-07-08T09:55:58.998837931Z

容器 Created 字段

docker inspect -f '{{.Config.Cmd}}' sharp_gates
[nginx -g daemon off;]

Cmd 只是命令配置的一部分,完整启动还需结合 Entrypoint。

docker image inspect --format '{{json .RootFS.Layers}}' nginx | jq

原另一个 Nginx 样本的层:

[
  "sha256:1bb35e8b4de116e84b2ccf614cce4e309b6043bf2cd35543d8394edeaeb587e3",
  "sha256:1bf33238ab09f13272838701bf1b0a795f1ca25a4197d6df0e9e1033ce9f66c0",
  "sha256:81a9d30670ecf867ffd9c7b952444efce07b8712a597e6009c9188e6ec223c1f",
  "sha256:f941308035cf657934e5dbf83c5c18819c2f9aa2ed687152caec85b3774cf223",
  "sha256:e6c40b7bdc831f8cbb96c77ce6b826b7e4442373542111e0f224d60c7a9b7870",
  "sha256:de2ef8ceb76a4bcd39a8a60962963c8aee0a0460eb57e4ca1bb5107e2b826b4a",
  "sha256:07eaefc6ebf2f83b1da015d65d9179f5b952f20b670a1da6aec9a63811bb5734"
]

镜像层字段

docker network ls
docker network inspect bridge --format '{{json .IPAM.Config}}' | jq
docker inspect sharp_gates | jq '.[0].HostConfig.NetworkMode'
docker inspect sharp_gates | jq -r '.[0].NetworkSettings.Networks.bridge.IPAddress'

原结果:

NETWORK ID     NAME    DRIVER  SCOPE
89e043ba18dc   bridge  bridge  local
44cae059432e   host    host    local
68539ce0cbce   none    null    local

[{"Subnet":"172.17.0.0/16","Gateway":"172.17.0.1"}]
"bridge"
172.17.0.2

bridge 网络 IPAM 配置

NetworkMode 是网络模式/连接配置,不是“网段类型”的同义词。inspect 可能含环境变量、挂载等敏感数据,分享时应按实际内容处理;字段随 API、网络和存储模式变化。

10.5 日志命令

docker logs [OPTIONS] CONTAINER
选项 用途
--follow/-f 持续跟踪新日志
--tail N 最近 N 行
--since、--until 时间范围
--timestamps/-t 每行增加 Docker 时间戳
--details 输出配置允许的附加属性

日志主要来自容器进程 stdout/stderr,不会自动采集容器里任意日志文件。停止但未删除的容器,通常仍可读取日志驱动保留的数据。

docker ps --format 'table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.Names}}'
docker logs sharp_gates
CONTAINER ID   IMAGE   COMMAND                  NAMES
43007f11dfe8   nginx   "/docker-entrypoint.…"   upbeat_goldberg
145b5a7f6b84   nginx   "/docker-entrypoint.…"   sharp_gates

原启动日志:

/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2025/07/08 09:55:59 [notice] 1#1: using the "epoll" event method
2025/07/08 09:55:59 [notice] 1#1: nginx/1.29.0
2025/07/08 09:55:59 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14+deb12u1)
2025/07/08 09:55:59 [notice] 1#1: OS: Linux 5.14.0-427.13.1.el9_4.x86_64
2025/07/08 09:55:59 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1073741816:1073741816
2025/07/08 09:55:59 [notice] 1#1: start worker processes
2025/07/08 09:55:59 [notice] 1#1: start worker process 29
2025/07/08 09:55:59 [notice] 1#1: start worker process 30
2025/07/08 09:55:59 [notice] 1#1: start worker process 31
2025/07/08 09:55:59 [notice] 1#1: start worker process 32
docker logs --tail 5 sharp_gates
docker logs --tail 5 -t sharp_gates
docker stop sharp_gates
docker logs sharp_gates
docker start sharp_gates
docker logs -f sharp_gates

tail 5 对应上述最后五行。加时间戳的原样本:

2025-07-08T09:55:59.183974909Z 2025/07/08 09:55:59 [notice] 1#1: start worker processes
2025-07-08T09:55:59.184280893Z 2025/07/08 09:55:59 [notice] 1#1: start worker process 29
2025-07-08T09:55:59.184294912Z 2025/07/08 09:55:59 [notice] 1#1: start worker process 30
2025-07-08T09:55:59.184386036Z 2025/07/08 09:55:59 [notice] 1#1: start worker process 31
2025-07-08T09:55:59.184619227Z 2025/07/08 09:55:59 [notice] 1#1: start worker process 32

logs -f 初始还会显示已保留日志,Ctrl+C 停止客户端跟踪,通常不影响容器主进程。大量日志读取和输出仍会消耗资源。

10.6 日志存储与轮转

json-file、local、journald、syslog、none 等驱动的存储和读取能力不同。经典默认数据目录配合 json-file 时,常见文件为 /var/lib/docker/containers/<ID>/<ID>-json.log;不能把该路径当成所有日志驱动的统一位置。

docker inspect --format '{{json .HostConfig.LogConfig}}' sharp_gates | jq
docker run --log-opt max-size=10m --log-opt max-file=3 -d --name nginx-log nginx
docker inspect --format '{{json .HostConfig.LogConfig}}' nginx-log | jq
docker run --log-driver=syslog -d --name nginx-driver nginx
docker inspect --format '{{json .HostConfig.LogConfig}}' nginx-driver | jq

原结果依次为:

{"Type":"json-file","Config":{}}
{"Type":"json-file","Config":{"max-file":"3","max-size":"10m"}}
{"Type":"syslog","Config":{}}

原创建 ID:4154387d9fc56cd93a61b7c809bc0e8ed22bc5b23c11e46181f53f08431804b0(nginx-log),ee99e9380ab9ee0f221819fafe1dc1642c6cc264c36c025b0665925572211346(nginx-driver)。默认无轮转配置的 json-file 可能持续增长,限制大小应在创建时设置,或修改默认后重建相应容器。

docker logs nginx-driver
ls /var/lib/docker/containers
ls /var/lib/docker/containers/ee99e9380ab9ee0f221819fafe1dc1642c6cc264c36c025b0665925572211346/

原目录还包括 sharp_gates、upbeat_goldberg 和 nginx-log 的 ID。syslog 容器目录清单:

checkpoints config.v2.json container-cached.log hostconfig.json hostname
hosts mounts resolv.conf resolv.conf.hash

syslog 容器的日志读取结果

日志缓存文件内容示例

container-cached.log 可与支持远程日志驱动读取的本地双重日志缓存有关,内部格式不应当成普通文本日志接口。原稿用 cat 查看它,正确的业务查询入口仍是 docker logs 或配置的 syslog 接收端。某些驱动/缓存配置不支持 docker logs,需按具体版本确认。

10.7 attach 与信号

attach 连接已有主进程 I/O,不创建新 Shell,不等于 exec,也不是通用 VNC;它并非无用命令。只有容器已有交互式 Shell 主进程时才能通过其输入运行 Shell 命令。

docker attach [OPTIONS] CONTAINER

常用选项:--detach-keys 自定义脱离键,--no-stdin 不附加输入,--sig-proxy 控制信号代理。

docker run -d --name nginx-attatch nginx
docker inspect -f '{{.NetworkSettings.Networks.bridge.IPAddress}}' nginx-attatch
# A 终端
docker attach nginx-attatch
# B 终端
curl -I 172.17.0.2

原 B 终端:

HTTP/1.1 200 OK
Server: nginx/1.29.0
Date: Wed, 09 Jul 2025 13:24:55 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 24 Jun 2025 17:22:41 GMT
Connection: keep-alive
ETag: "685adee1-267"
Accept-Ranges: bytes

A 终端看到:

172.17.0.1 - - [09/Jul/2025:13:24:55 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.76.1" "-"

原实验 Ctrl+C 后,传入的是 SIGINT(2),不是 SIGKILL;原日志本身也印证了这一点:

^C
2025/07/09 13:26:45 [notice] 1#1: signal 2 (SIGINT) received, exiting
2025/07/09 13:26:45 [notice] 33#33: exiting
2025/07/09 13:26:45 [notice] 33#33: exit
2025/07/09 13:26:45 [notice] 30#30: exiting
2025/07/09 13:26:45 [notice] 30#30: exit
2025/07/09 13:26:45 [notice] 31#31: exiting
2025/07/09 13:26:45 [notice] 31#31: exit
2025/07/09 13:26:45 [notice] 32#32: exiting
2025/07/09 13:26:45 [notice] 32#32: exit
2025/07/09 13:26:45 [notice] 1#1: signal 17 (SIGCHLD) received from 32
2025/07/09 13:26:45 [notice] 1#1: worker process 32 exited with code 0
2025/07/09 13:26:45 [notice] 1#1: signal 29 (SIGIO) received
2025/07/09 13:26:45 [notice] 1#1: signal 17 (SIGCHLD) received from 31
2025/07/09 13:26:45 [notice] 1#1: worker process 30 exited with code 0
2025/07/09 13:26:45 [notice] 1#1: worker process 31 exited with code 0
2025/07/09 13:26:45 [notice] 1#1: worker process 33 exited with code 0
2025/07/09 13:26:45 [notice] 1#1: exit

容器随后为 Exited (0),原 ID eb46b2227bec。

docker start nginx-attatch
docker attach nginx-attatch
docker attach --detach-keys='ctrl-e,ctrl-d' nginx-attatch

默认脱离序列为先 Ctrl+P,再 Ctrl+Q。该机制与容器是否交互式/TTY、是否附加 stdin、终端按键传递有关;原稿用普通 -d nginx 的非交互主服务实验,不能据此断言脱离键一般无效。查看日志优先 logs -f,创建独立交互进程优先 exec -it;对主 Shell 使用 exit 会使该主进程退出,而对 Nginx 输入文字 exit 并不会自动成为 Shell 命令。

11. 端口发布、docker-proxy 与 DNS

11.1 bridge 与端口发布

默认 Linux bridge 模式下,容器通常经 veth 接入网桥,出站访问可使用 NAT。外部主机默认通常不能直接路由到容器私有 IP;访问能力由实际路由、Docker 网关模式、防火墙和端口发布决定,不能把所有容器网络都叫 NAT 网络。

docker run -d nginx
docker port heuristic_bose

原未发布容器:

23c2ea030423   heuristic_bose   80/tcp

port 没有输出宿主机映射。-P 发布的是已声明暴露的端口,不是扫描并发布容器内所有监听端口:

docker run -d -P nginx
docker port zen_kirch
6755c0badab7   zen_kirch   0.0.0.0:32768->80/tcp, [::]:32768->80/tcp
80/tcp -> 0.0.0.0:32768
80/tcp -> [::]:32768

11.2 -p 指定端口

docker run -p [宿主机IP:]宿主机端口:容器端口[/协议] [其他参数] 镜像

多个映射使用多个 -p。映射目标不必预先 EXPOSE,Docker 通常也不会因为容器内尚无进程监听就拒绝创建;这种情况下访问会失败,原稿“容器端口不存在就报错”不准确。

缺少参数的错误演示:

docker run -p -d nginx
docker: invalid containerPort: -d
Run 'docker run --help' for more information

省略宿主机端口可自动分配:

docker run -p :80 -d nginx
10afe1b4f165   clever_colden   0.0.0.0:32769->80/tcp, [::]:32769->80/tcp

指定宿主机端口:

docker run -p 80:80 -d nginx
4f1e58ff9bc8   flamboyant_jang   0.0.0.0:80->80/tcp, [::]:80->80/tcp

指定本机已有地址:

docker run -p 10.0.0.12:81:80 -d nginx
451f797d618c   priceless_panini   10.0.0.12:81->80/tcp

原课件某些命令提示符写 ubuntu24-13,但绑定地址为 10.0.0.12。实际应使用执行 run 的宿主机拥有的地址,而不是照抄另一台主机的地址。

11.3 更改已有端口发布

docker container update --help

update 支持资源与重启策略等设置,常见选项包括:

选项 内容
--blkio-weight 块 I/O 相对权重,后端支持条件不同
--cpu-period、--cpu-quota CFS 配额周期和时间
--cpu-rt-period、--cpu-rt-runtime 实时 CPU 限制,依内核及 daemon 支持
--cpu-shares、--cpus CPU 权重/配额
--cpuset-cpus、--cpuset-mems CPU/NUMA 范围
--memory、--memory-reservation 硬限制/相应软约束
--memory-swap 内存与 swap 的组合限制语义
--pids-limit 任务数
--restart 重启策略

它没有直接修改已有 -p 的受支持选项。可靠做法是记录原运行配置、持久化数据,再以新映射重建容器。原例是普通 nginx 容器,最小示意:

docker inspect priceless_panini
docker stop priceless_panini
docker rename priceless_panini priceless_panini-old
docker run -d --name priceless_panini -p 10.0.0.12:88:80 nginx

真实业务还需带回原环境变量、挂载、网络、镜像版本和其他参数,不能只复制 -p。

原稿另记录了直接改内部状态文件的实验,它不是受支持接口。路径为 /var/lib/docker/containers/<ID>/hostconfig.json,原 PortBindings:

{
  "NetworkMode": "bridge",
  "PortBindings": {
    "80/tcp": [{"HostIp":"10.0.0.12","HostPort":"81"}]
  }
}

原操作先 systemctl stop docker,用 vim 把 HostPort 81 改为 88,再 systemctl start docker、docker start priceless_panini,最后显示:

451f797d618c   priceless_panini   10.0.0.12:88->80/tcp

这只证明原版本一次内部文件修改的结果,不保证元数据、网络规则和缓存一致,也不能在 daemon 工作时直接修改。本文使用重建作为修正后的操作路径。

11.4 docker-proxy 与防火墙

netstat -tnulp
iptables -vnL DOCKER
iptables -t nat -vnL DOCKER

原用户态代理监听样本:

tcp  0 0 0.0.0.0:32768 ... LISTEN 5150/docker-proxy
tcp6 0 0 :::32768      ... LISTEN 5156/docker-proxy

rootful Linux bridge 发布端口可以涉及内核 DNAT/转发规则和用户态代理。哪些连接经过 docker-proxy 取决于来源、网络、IP 族和配置;不能说每个访问必然依次经过用户态代理。-p 和 -P 都可能创建代理,也不能说只有 -P 才有。

防火墙规则由 Docker daemon 的网络管理逻辑维护,不是 docker-proxy 自己创建和管理 iptables 规则。

原 filter 规则:

Chain DOCKER
ACCEPT tcp -- !docker0 docker0 0.0.0.0/0 172.17.0.3 tcp dpt:80
DROP   all -- !docker0 docker0 0.0.0.0/0 0.0.0.0/0

再启动:

docker run -d -P nginx

原映射:

13b17803d487   angry_proskuriakova   0.0.0.0:32769->80/tcp, [::]:32769->80/tcp
6755c0badab7   zen_kirch             0.0.0.0:32768->80/tcp, [::]:32768->80/tcp
23c2ea030423   heuristic_bose        80/tcp

filter 表相应多一条允许去 172.17.0.4:80 的规则,nat 表也会有匹配宿主机发布端口的 DNAT。具体后端并不保证都使用完全一样的链名。

11.5 禁用用户态代理实验

下面的方法不会关闭宿主机代理,只会给容器传一个普通环境变量:

# 错误思路示范
docker run -P --env DOCKER_PROXY=false nginx

真正的 daemon 设置为:

{
  "registry-mirrors": [
    "https://docker.xuanyuan.me",
    "https://82ue03hk.mirror.aliyuncs.com"
  ],
  "userland-proxy": false
}
systemctl restart docker
docker run -d -P nginx
docker port compassionate_elbakyan
netstat -tnulp | grep docker
iptables -vnL DOCKER
iptables -t nat -vnL DOCKER

原记录:

98ac63968f90   compassionate_elbakyan   0.0.0.0:32768->80/tcp
80/tcp -> 0.0.0.0:32768

tcp 0 0 0.0.0.0:32768 0.0.0.0:* LISTEN 7456/dockerd

# filter DOCKER
ACCEPT tcp -- !docker0 docker0 0.0.0.0/0 172.17.0.2 tcp dpt:80
DROP   all -- !docker0 docker0 0.0.0.0/0 0.0.0.0/0

# nat DOCKER
DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:32768 to:172.17.0.2:80

有些版本 daemon 仍可能保留绑定 socket,这不表示用户态转发仍在执行;检查配置、进程和实际连接路径。docker info 不一定显示 userland-proxy,不能凭没有字段判断配置没生效。禁用的兼容性和性能收益需按具体连接场景验证,不能保证普遍提速。

恢复时从合并配置中移除该键或恢复原值,再:

systemctl restart docker

11.6 hosts 配置

docker run -d nginx
docker exec practical_mendel cat /etc/hosts

原容器 ID b4fc910af29a,hosts:

127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00:: ip6-localnet
ff00:: ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 b4fc910af29a

默认主机名常为短 ID,也可通过参数配置。宿主机的任意 /etc/hosts 条目不会自动全部传入容器,修改容器 hosts 也不等于修改宿主机 hosts。

docker run --add-host myserver:10.0.0.100 -d nginx
docker exec unruffled_lovelace cat /etc/hosts

原新增项:

10.0.0.100 myserver
172.17.0.2 e1ef9f794280

多个条目:

docker run --add-host myserver1:10.0.0.100 \
  --add-host myserver2:10.0.0.101 -d nginx
docker exec adoring_shirley cat /etc/hosts
10.0.0.100 myserver1
10.0.0.101 myserver2
172.17.0.3 ffd55c3ba78e

11.7 resolv.conf、--dns 与全局 DNS

原默认 bridge 的宿主机和容器示例:

cat /etc/resolv.conf
docker exec adoring_shirley cat /etc/resolv.conf
# 宿主机:Generated by NetworkManager
nameserver 10.0.0.2

# 容器:Generated by Docker Engine.
# This file can be edited; Docker Engine will not make further changes once it has been modified.
nameserver 10.0.0.2
# Based on host file: '/etc/resolv.conf' (legacy)
# Overrides: []

自定义 bridge 常使用 Docker 内置 DNS(如 127.0.0.11),而非简单把主机 resolv.conf 原样复制。DNS 服务器的回退、轮换与查询行为由实际 resolver、选项和网络决定,不是一律第一个严格主、后面严格备用。

容器内部执行重定向需要由容器 Shell 处理:

docker exec adoring_shirley bash -c 'echo "nameserver 8.8.8.8" >> /etc/resolv.conf'
docker exec adoring_shirley cat /etc/resolv.conf

若把 >> /etc/resolv.conf 放在 docker exec 的外部,它会由宿主机 Shell 重定向。上面被单引号保护的命令不会正常地修改宿主机文件;原稿随后展示宿主机也多了 8.8.8.8,与该命令本身不一致,不能当作预期结果。

原稿记录容器 stop/start 后附加项消失;但同一原输出也写明手工修改后不再自动管理。不同版本和网络情况下的维护行为不应笼统概括为“修改后必定每次重启覆盖”。持久配置 DNS 应使用 --dns 或 daemon 配置,不依赖手工改生成文件。

docker stop adoring_shirley
docker start adoring_shirley
docker exec adoring_shirley cat /etc/resolv.conf

原重启后记录为 nameserver 10.0.0.2,属于原实验观察。

容器级 DNS:

docker run --dns 8.8.8.8 --dns 2.2.2.2 -d nginx
docker exec admiring_varahamihira cat /etc/resolv.conf

原结果:

nameserver 8.8.8.8
nameserver 2.2.2.2
# Overrides: [nameservers]

2.2.2.2 仅为原实验配置值,不能因成功写入就认定它是可用公共 DNS。实际应使用所在网络允许且经验证可达的 resolver。

全局默认示例:

{
  "registry-mirrors": [
    "https://docker.xuanyuan.me",
    "https://82ue03hk.mirror.aliyuncs.com"
  ],
  "dns": ["114.114.114.114", "8.8.8.8"]
}
systemctl restart docker
docker run -d nginx:latest
docker exec adoring_stonebraker cat /etc/resolv.conf

原新容器 86d185a044ec:

nameserver 114.114.114.114
nameserver 8.8.8.8
# Overrides: [nameservers]

--dns 可以覆盖 daemon 的默认:

docker run -d --dns 2.2.2.2 nginx:latest
docker exec brave_newton grep nameserver /etc/resolv.conf

原容器 6695e181c69d 显示 nameserver 2.2.2.2。这只验证配置选择,不验证外部域名解析成功;更改 daemon 默认不等于自动改写所有已有容器的配置。

12. 文件复制与时区

12.1 cp 的路径与属性

docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH
docker cp [OPTIONS] SRC_PATH CONTAINER:DEST_PATH

至少一端是容器,另一端是本地文件系统;单次 cp 不是直接在两个容器间复制。可复制文件和目录,容器不必一直运行。-L 跟随源端软链接;-a 主要保留源 UID/GID 等归档属性,不保证 ACL、扩展属性、inode、ctime、创建时间全部不变。

原稿频繁使用 rm -rf /tmp/* 清空环境,下面改为独立实验目录,避免删除无关临时文件。

docker run -d --name zealous_hofstadter nginx:latest
docker exec zealous_hofstadter bash -c 'mkdir -p /data/server/dir{1..3}'
mkdir -p /tmp/docker-copy-lab

12.2 容器目录复制到宿主机

docker cp zealous_hofstadter:/data/server/ /tmp/docker-copy-lab/
ls /tmp/docker-copy-lab
ls /tmp/docker-copy-lab/server

与原稿相同的目录内容:

Successfully copied 3.07kB ...
server
dir1 dir2 dir3

目标目录已存在时,源尾部普通 / 不会自动变成“只复制目录内容”:

docker cp zealous_hofstadter:/data/server /tmp/docker-copy-lab

结果仍为目标下的 server。只复制源目录内容应明确使用:

docker cp zealous_hofstadter:/data/server/. /tmp/docker-copy-lab/

12.3 单文件与重命名

docker cp zealous_hofstadter:/etc/hosts /tmp/docker-copy-lab/
docker cp zealous_hofstadter:/etc/hosts /tmp/docker-copy-lab/hosts-con
ls /tmp/docker-copy-lab

原稿每次显示 Successfully copied 2.05kB,得到 hosts、hosts-con。该传输大小包含归档开销,不必等于 hosts 实际文件字节数。

12.4 -a、时间戳和文件所有者

docker exec zealous_hofstadter stat /etc/hosts
docker cp -a zealous_hofstadter:/etc/hosts /tmp/docker-copy-lab/hosts-attr
stat /tmp/docker-copy-lab/hosts-attr

原 stat 记录对比(路径仍按原记录):

属性 容器 /etc/hosts 宿主机 /tmp/hosts-attr
Size 172 172
Blocks / IO Block 8 / 4096 8 / 4096
Mode 0644,普通文件 0644,普通文件
UID/GID 0/0 0/0
Device / Inode 253,0 / 68342191 fd00h/64768d / 136526159
Links 1 1
Access 2025-07-10 07:25:43.617413992 +0000 2025-07-10 15:15:20.000000000 +0800
Modify 2025-07-10 07:15:20.810039686 +0000 2025-07-10 15:15:20.000000000 +0800
Change 2025-07-10 07:25:43.616413969 +0000 2025-07-10 15:31:50.313844129 +0800
Birth 2025-07-10 07:15:20.693036971 +0000 2025-07-10 15:31:50.313844129 +0800

这组数据不是所有时间戳完全相同:mtime 到秒的时刻对应,但样本精度不同;atime、ctime、birth 也不同。时区只是显示方式,不会导致 UID/GID 或文件内容复制失效。ctime/创建时间不能通常作为普通归档属性任意恢复。

docker exec zealous_hofstadter ls -l /etc/localtime
ls -l /etc/localtime

原时区链接:容器 /usr/share/zoneinfo/Etc/UTC,宿主机 ../usr/share/zoneinfo/Asia/Shanghai。这些解释了显示时差,不能据此得出“容器时间机制使 -a 失效”。

12.5 宿主机到容器

docker run -d --name naughty_engelbart nginx:latest
docker cp /etc/hosts naughty_engelbart:/tmp/
docker exec naughty_engelbart cat /tmp/hosts
mkdir -p /tmp/docker-copy-lab/server/dir{1..3}
docker cp /tmp/docker-copy-lab/server naughty_engelbart:/tmp/
docker exec naughty_engelbart ls /tmp/
docker exec naughty_engelbart ls /tmp/server

原 hosts 内容:

127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6

原目录输出为 hosts、server,server 下为 dir1、dir2、dir3。

12.6 UID/GID 的数字与名称

原宿主机已有实验用户 sswang,UID/GID 为 1000,加入 wheel:

touch /tmp/docker-copy-lab/sswang.txt
chown sswang:sswang /tmp/docker-copy-lab/sswang.txt
id sswang
docker cp -a /tmp/docker-copy-lab/sswang.txt naughty_engelbart:/tmp/
docker exec naughty_engelbart ls -l /tmp/sswang.txt
用户id=1000(sswang) 组id=1000(sswang) 组=1000(sswang),10(wheel)
-rw-r--r-- 1 1000 1000 0 Jul 10 08:40 /tmp/sswang.txt

文件保存的是数字 UID/GID;本地名称服务没有相应条目时显示数字。有对应数字但名字不同则显示该端解析出的名字,并不会为了同名用户自动重映射。

docker exec naughty_engelbart grep nginx /etc/passwd
# 原样本:nginx:x:101:101:nginx user:/nonexistent:/bin/false
getent passwd 101

docker exec naughty_engelbart touch /tmp/nginx.txt
docker exec naughty_engelbart chown nginx:nginx /tmp/nginx.txt
docker exec naughty_engelbart chmod +x /tmp/nginx.txt
docker exec naughty_engelbart ls -l /tmp/nginx.txt
docker cp naughty_engelbart:/tmp/nginx.txt /tmp/docker-copy-lab/
ls -l /tmp/docker-copy-lab/nginx.txt

原容器内为:

-rwxr-xr-x 1 nginx nginx 0 Jul 10 08:48 /tmp/nginx.txt

原宿主机以 root 执行不带 -a的复制后:

-rwxr-xr-x 1 root root 0 Jul 10 16:48 /tmp/nginx.txt

默认复制到宿主机时通常归属于执行命令的用户;通过 sudo 默认以 root 执行,得到的仍为 root:

su - sswang
sudo docker cp naughty_engelbart:/tmp/nginx.txt /tmp/nginx-sswang.txt
ls -l /tmp/nginx-sswang.txt

原输出同样为 root root。需要保留源数字所有者时使用 -a 并具备相应权限;userns-remap/rootless 下还需考虑用户映射。

12.7 TZ 与本地时间文件

TZ 影响支持该变量的进程如何显示时间,不修改宿主机系统时钟,不一定改写容器里的 /etc/localtime、/etc/timezone。还需镜像内存在相应 zoneinfo 或运行库支持。

docker run -d --name silly_leakey -e TZ=Asia/Shanghai nginx
docker exec silly_leakey printenv TZ
docker exec silly_leakey ls -l /etc/localtime
docker exec silly_leakey cat /etc/timezone

原结果:

Asia/Shanghai
/etc/localtime -> /usr/share/zoneinfo/Etc/UTC
Etc/UTC

可以绑定宿主机时区文件:

docker run -d --name angry_kilby \
  -v /etc/localtime:/etc/localtime:ro -e TZ=Asia/Shanghai nginx
docker exec angry_kilby ls -l /etc/localtime
date
docker exec angry_kilby date
docker exec angry_kilby md5sum /etc/localtime
md5sum /etc/localtime
docker exec angry_kilby printenv TZ

原时间和摘要:

2025年 07月 10日 星期四 16:06:40 CST
Thu Jul 10 16:06:40 CST 2025
09dd479d2f22832ce98c27c4db7ab97c /etc/localtime
09dd479d2f22832ce98c27c4db7ab97c /etc/localtime
Asia/Shanghai

ls -l 仍可能显示镜像原符号链接名,需结合挂载和实际读取内容理解。相同摘要表示内容相同,不表示是同一个 inode;这里调整的是时区表现,不是另给容器进行系统时钟同步。

13. 清理、export/import 与 commit

13.1 系统清理

正确命令为 docker system prune,原文 purge 为错误命令名。

docker system prune
docker system df
docker system prune -a

默认清理停止容器、无容器使用的网络、悬空镜像和未使用构建缓存;默认不清理卷。-a 扩大到所有未被容器引用的镜像;--volumes 在现代常见版本中另清理未使用匿名卷,并非无条件删除一切具名卷;-f 只跳过确认,不杀运行容器。

原默认清理交互:

WARNING! This will remove:
 - all stopped containers
 - all networks not used by at least one container
 - all dangling images
 - unused build cache
Are you sure you want to continue? [y/N] y
Deleted build cache objects:
ufaaz35mq69lnxqz7nbevigo1
6tc9414h9feyp6mxn67wwzk08
Total reclaimed space: 19B
TYPE           TOTAL ACTIVE SIZE     RECLAIMABLE
Images         9     1      2.838GB  2.721GB (95%)
Containers     1     1      1.253kB  0B (0%)
Local Volumes  7     0      1.134GB  1.134GB (100%)
Build Cache    1     0      0B       0B

-a 的镜像警告改成 all images without at least one container associated to them。原清理后还剩运行中的 naughty_engelbart,引用的 Nginx 镜像 9592f5595f2b、192 MB。

13.2 准备镜像环境

ls ./*.tar
for archive in ./*.tar; do
  [ -f "$archive" ] || continue
  docker load -i "$archive"
done
/bin/bash get_ali_images.sh -a
docker images

get_ali_images.sh 是配套脚本,PDF 未包含实现。原归档为 alpine.tar、busybox.tar、debian.tar、multiple_images.tar、nginx.tar、ubuntu.tar。

原镜像表:

REPOSITORY TAG             IMAGE ID       CREATED       SIZE
debian     latest          b3a422523a11   10 days ago   117MB
nginx      latest          9592f5595f2b   2 weeks ago   192MB
wordpress  php8.4-apache   d65e5ede33da   4 weeks ago   715MB
alpine     latest          cea2ff433c61   5 weeks ago   8.31MB
redis      latest          ed3a2af6d0d4   5 weeks ago   128MB
ubuntu     latest          bf16bdcff9c9   6 weeks ago   78.1MB
mysql      oraclelinux9    6f11f923fa04   2 months ago  859MB
busybox    latest          6d3e4188a38a   9 months ago  4.28MB

13.3 命令模型

方法 对象/内容 保留范围
export 容器根文件系统 tar 无镜像历史、默认入口和环境配置,不保存内存,也不导出挂载卷数据
import 从根文件系统 tar、URL 或 stdin 创建镜像 新的扁平层,可指定名称/标签,并通过支持的 change 设置配置
commit 从容器可写文件系统变化生成镜像 保留/修改镜像配置,不含内存、挂载卷内容及全部 HostConfig
save/load 保存/加载镜像归档 镜像层、配置和归档中保存的引用,不等于迁移完整运行容器

归档大小由内容、历史层和压缩决定,不能保证 export 永远小于 save。完整业务迁移还需卷、绑定目录、运行参数、网络及外部依赖。

docker export -o container_fs.tar my_container
docker export my_container | gzip > container_fs.tar.gz
docker import container_fs.tar my_new_image:latest
gunzip -c container_fs.tar.gz | docker import - my_new_image:latest
docker export [OPTIONS] CONTAINER
docker import [OPTIONS] FILE|URL|- [REPOSITORY[:TAG]]
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

commit 的 -a 设置镜像作者元数据,-m 设置说明,-p 默认暂停容器以减少提交过程变化,-c/--change 修改支持的镜像配置。暂停不等于保证外部数据库或多服务事务一致性。

手工 commit 难以复现,多次提交会增加层,密码或密钥可能被封进镜像;可重复交付通常优先 Dockerfile。

13.4 自定义 Nginx 文件系统

docker run -d --name determined_hoover nginx:latest
dcn determined_hoover
docker exec -it determined_hoover bash

容器内:

ls /usr/share/nginx/html
history -c
echo 'Nginx Web Page by sswang' > /usr/share/nginx/html/index.html
exit

原目录为 50x.html、index.html。清除当前交互历史不是清理镜像内全部敏感信息的保证。

curl 172.17.0.2
Nginx Web Page by sswang

13.5 export/import 实验

docker export -o my_nginx.tar determined_hoover
ls -lh my_nginx.tar
tar -tvf my_nginx.tar | head -n5
docker import my_nginx.tar mynginx:v1
docker images mynginx
docker history mynginx:v1

原结果:

-rw------- 1 root root 186M Jul 10 17:55 my_nginx.tar

-rwxr-xr-x 0/0 0 2025-07-10 17:45 .dockerenv
lrwxrwxrwx 0/0 0 2025-06-30 08:00 bin -> usr/bin
drwxr-xr-x 0/0 0 2025-05-09 22:50 boot/
drwxr-xr-x 0/0 0 2025-07-10 17:45 dev/
-rwxr-xr-x 0/0 0 2025-07-10 17:45 dev/console

sha256:2ad366d4f19db4704a6278dbd78817990ef24acf756ec1e0cff1ed331822a716
REPOSITORY TAG IMAGE ID      CREATED        SIZE
mynginx    v1  2ad366d4f19d 23 seconds ago 191MB

IMAGE        CREATED         CREATED BY SIZE   COMMENT
2ad366d4f19d 44 seconds ago              191MB  Imported from -

镜像没有默认命令时:

docker run -d --name mynginx mynginx:v1
docker: Error response from daemon: no command specified
Run 'docker run --help' for more information

显式传入入口命令:

docker run -d --name mynginx mynginx:v1 nginx -g 'daemon off;'
dcn mynginx
curl 172.17.0.3
docker rm -f mynginx

原新容器 9625dc6cd579,IP 为 172.17.0.3,返回 Nginx Web Page by sswang。原列表中它的 PORTS 为空,但程序依然能监听 80;EXPOSE 元数据不是程序能否监听的开关。

13.6 commit 实验

docker exec determined_hoover bash -c "echo 'Nginx Web Page by sswang with commit' > /usr/share/nginx/html/index.html"
docker commit -a 'sswang' -m 'new image' determined_hoover mynginx:v2
docker images mynginx
docker run -d --name mynginx mynginx:v2
dcn mynginx
curl 172.17.0.3

原结果:

sha256:7e1858cc3051d2078f765d2692085b41bffc341e6394bf1447e4559d6b47d2fa
REPOSITORY TAG IMAGE ID      CREATED         SIZE
mynginx    v2  7e1858cc3051 7 seconds ago   192MB
mynginx    v1  2ad366d4f19d 29 minutes ago  191MB

Nginx Web Page by sswang with commit

新容器 88b6afcf528c 可以继承源 Nginx 镜像的入口配置运行。卷数据、端口发布和全部宿主机运行设置仍不在 commit 中。

docker rm -f mynginx determined_hoover

14. CRIU 检查点与跨主机恢复实验

14.1 能力与边界

CRIU(Checkpoint/Restore In Userspace)可以冻结 Linux 进程,将内存、进程关系、打开文件及部分内核状态保存到检查点,再在兼容条件下恢复。它起源于 Virtuozzo 相关项目,并与 OpenVZ、LXC/LXD、Podman、Docker 等生态存在集成。

CRIU 项目介绍

参考:https://criu.org/Main_Page、https://docs.docker.com/reference/cli/docker/checkpoint/。

Docker checkpoint 属于实验能力,支持范围与 Engine、runtime、CRIU、内核、权限、CPU 和应用使用的资源有关。下面不是保证服务零中断的通用热迁移流程:原实验创建检查点后源容器停止,过几分钟才在目标恢复。

检查点不自动包含完整镜像、容器可写层和挂载卷数据;目标还需相同镜像、兼容运行参数,以及一致的文件、挂载和网络环境。网络连接、设备、外部资源和分布式数据一致性不能靠复制一个目录就自动恢复。

Kubernetes 通常在另一节点重建 Pod,不是默认冻结内存再在另一节点恢复;Swarm 的重新调度通常也创建新的任务,而非透明迁移原进程内存。高可用常需副本、负载均衡和应用层恢复,不能把普通重新调度与 CRIU 热迁移混为一谈。

14.2 Docker 接口

Docker checkpoint 文档

docker checkpoint create CONTAINER CHECKPOINT
docker checkpoint ls CONTAINER
docker checkpoint rm CONTAINER CHECKPOINT
docker start --checkpoint CHECKPOINT CONTAINER

原稿的 docker checkpoint restore 不是有效子命令,恢复通过 docker start --checkpoint 完成。

典型流程为:准备兼容环境与文件系统 → 创建检查点 → 传输检查点及必需数据 → 在目标创建等价容器 → 从检查点启动 → 验证状态、网络与数据。灾难恢复也必须有可用备份/检查点和外部状态,不能在源主机已损坏后凭空提取状态。

14.3 三台实验主机

角色 原主机 原版本记录
源 Rocky1,10.0.0.12 CRIU 3.19,Rocky 9 系列内核
目标一 Ubuntu,10.0.0.13 CRIU 4.1,6.11.0-28-generic
目标二 Rocky2,10.0.0.15 CRIU 3.19,5.14.0-427.13.1.el9_4.x86_64

原稿部分 Rocky1 提示符误写 ubuntu24-13,应按角色/地址理解。原结果不能证明任意 Rocky 与 Ubuntu 组合都可以恢复。

Rocky1 / Rocky2 的 CRIU

两台 Rocky 分别准备:

yum install -y criu
criu --version
criu check
grep CHECKPOINT "/boot/config-$(uname -r)"

原输出:

Version: 3.19
Warn (criu/kerndat.c:1593): CRIU was built without libnftables support
Looks good.
CONFIG_CHECKPOINT_RESTORE=y

原软件来源为其启用的 Rocky 软件源;并非所有桌面版都已经安装,也不能不看仓库状态就承诺有相同版本。Looks good 检查基础能力,不保证某个真实应用可完整恢复;没有 libnftables 支持提示相关功能缺失,是否影响场景需要具体测试,不能保证“基本端口映射一定无影响”。

Rocky 的 Docker 安装入口:

yum install -y yum-utils
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
yum makecache
yum install -y docker-ce docker-ce-cli containerd.io

Ubuntu 的 CRIU

原稿使用 PPA 获取 CRIU,实际先检查发行版包是否满足要求;第三方源还需匹配 Ubuntu 版本。

add-apt-repository ppa:criu/ppa -y
apt update
apt install criu
criu --version
criu check
grep CHECKPOINT "/boot/config-$(uname -r)"

原记录:

Version: 4.1
Looks good.
CONFIG_CHECKPOINT_RESTORE=y

Ubuntu Docker 安装入口:

apt-get update
apt-get install -y ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg \
  | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  > /etc/apt/sources.list.d/docker.list
apt update
apt install -y docker-ce docker-ce-cli containerd.io

三台主机启用实验功能

在各自主机合并配置:

{
  "registry-mirrors": [
    "https://docker.xuanyuan.me",
    "https://82ue03hk.mirror.aliyuncs.com"
  ],
  "experimental": true
}
systemctl restart docker
docker checkpoint

原未开启时:

docker checkpoint is only supported on a Docker daemon with experimental features enabled

开启后帮助:

Usage: docker checkpoint COMMAND
Manage checkpoints
Commands:
 create  Create a checkpoint from a running container
 ls      List checkpoints for a container
 rm      Remove a checkpoint

14.4 源主机创建计数容器

三台主机需使用同一 BusyBox 镜像;原实验已通过 docker save 得到 busybox.tar。

源 Rocky1:

docker load -i busybox.tar
docker run --name cr -d --network host --security-opt seccomp=unconfined \
  busybox /bin/sh -c 'i=0; while true; do echo "$i $(date +%F_%T)"; i=$((i + 1)); sleep 1; done'
docker ps
docker logs cr

这里保留原实验的 host 网络和关闭 seccomp 设置,用于减少该受控恢复实验的约束,不代表业务容器默认应该如此设置。host 网络仍需与目标主机资源兼容。

原日志:

0 2025-07-15_02:17:51
1 2025-07-15_02:17:52
2 2025-07-15_02:17:53
3 2025-07-15_02:17:54
4 2025-07-15_02:17:55
...

原源码写法用 expr 和反引号,本文改为等价 Shell 算术/命令替换。源容器 ID 在原不同截图出现 f1a17acae68f、f3bc052e27b2,显然来自不同轮演示,不能把所有 ID 当同一次执行。

14.5 两个目标主机创建等价容器

Ubuntu 与 Rocky2 分别执行:

scp root@10.0.0.12:busybox.tar ./
docker load -i busybox.tar
docker images
docker create --name cr --network host --security-opt seccomp=unconfined \
  busybox /bin/sh -c 'i=0; while true; do echo "$i $(date +%F_%T)"; i=$((i + 1)); sleep 1; done'
docker ps -a

原加载记录:

65014c70e84b: Loading layer 4.516MB/4.516MB
Loaded image: busybox:latest
REPOSITORY TAG    IMAGE ID       CREATED       SIZE
busybox    latest 6d3e4188a38a   9 months ago  4.28MB

原目标容器:

# Ubuntu
917ded84818e   busybox   "/bin/sh -c 'i=0; wh…"   Created   cr
# Rocky2
c4afb52c5b8b   busybox   "/bin/sh -c 'i=0; wh…"   Created   cr

源和目标的镜像、启动命令及会影响恢复的配置应一致;不要在目标先普通 start 使其从零运行。

14.6 创建并检查检查点

源 Rocky1:

mkdir -p /data/server/srv
docker checkpoint create --checkpoint-dir /data/server/srv cr checkpoint1
ls /data/server/srv
du -sh /data/server/srv
ls /data/server/srv/checkpoint1
docker checkpoint ls cr
docker checkpoint ls --checkpoint-dir /data/server/srv cr
docker ps -a
docker logs cr | tail -n2

原数据量 408K,自定义目录下为 checkpoint1。文件清单包含:

cgroup.img core-1.img core-624.img descriptors.json fdinfo-2.img fdinfo-3.img
files.img fs-1.img fs-624.img ids-1.img ids-624.img inventory.img ipcns-var-11.img
mm-1.img mm-624.img mountpoints-13.img pagemap-1.img pagemap-624.img
pages-1.img pages-2.img pstree.img seccomp.img timens-0.img
utsns-12.img tmpfs-dev-50.tar.gz.img tmpfs-dev-53.tar.gz.img
tmpfs-dev-54.tar.gz.img tmpfs-dev-55.tar.gz.img tmpfs-dev-56.tar.gz.img
tmpfs-dev-57.tar.gz.img

默认路径通常为 /var/lib/docker/containers/<ID>/checkpoints/<名称>;创建在自定义 checkpoint-dir 后,查询也需指明同一目录。原默认 checkpoint ls cr 只显示表头,自定义路径查询显示 checkpoint1。

默认创建完成后源容器停止,原记录:

cr   Exited (137) 38 seconds ago
23 2025-07-15_02:18:15
24 2025-07-15_02:18:16

该 137 在这里属于原 checkpoint 终止行为记录,不能只凭退出码认定发生 OOM。某些版本支持 leave-running 参数,但保留源运行会产生状态继续变化和双实例问题,不自动构成安全迁移。

14.7 Ubuntu 恢复

原稿把检查点手工复制到目标容器内部目录;为避免依赖固定容器 ID,下面使用 Docker 支持的外部 checkpoint-dir 参数表达同一流程:

mkdir -p /data/server/srv
scp -r root@10.0.0.12:/data/server/srv/checkpoint1 /data/server/srv/
du -sh /data/server/srv/checkpoint1
docker checkpoint ls --checkpoint-dir /data/server/srv cr
docker start --checkpoint-dir /data/server/srv --checkpoint checkpoint1 cr
docker logs cr

原内部目录为:

/var/lib/docker/containers/917ded84818e4e6566c1700cadc0239a6477cef6d4c07c70ff93aea4c197c45b/checkpoints/

原复制后的大小 408K,列表含 checkpoint1;原恢复日志:

25 2025-07-15_02:21:35
26 2025-07-15_02:21:36
27 2025-07-15_02:21:37
28 2025-07-15_02:21:38
29 2025-07-15_02:21:39
30 2025-07-15_02:21:40

计数继续从 25 开始体现了进程状态恢复;时间重新读取当前时钟,不能把中间三分多钟的缺口称为没有停机。以上为课件成功记录,未在本次整理中重做实验。

14.8 Rocky2 恢复

在 Rocky2 使用等价的外部目录流程:

mkdir -p /data/server/srv
scp -r root@10.0.0.12:/data/server/srv/checkpoint1 /data/server/srv/
docker checkpoint ls --checkpoint-dir /data/server/srv cr
docker start --checkpoint-dir /data/server/srv --checkpoint checkpoint1 cr
docker ps
docker logs cr

原稿该节在复制后漏写恢复命令,本文已补上 docker start --checkpoint ...。原内部路径:

/var/lib/docker/containers/c4afb52c5b8b907409826424fe7034f93ae07d8617772f3514d731687a7f9722/checkpoints/

原列表 cr 为 Up 2 seconds,日志:

25 2025-07-15_02:23:04
26 2025-07-15_02:23:05
27 2025-07-15_02:23:06
28 2025-07-15_02:23:07
29 2025-07-15_02:23:08
30 2025-07-15_02:23:09
31 2025-07-15_02:23:10
32 2025-07-15_02:23:11

同时在两个目标恢复同一个源进程是课堂验证,不是有状态业务的生产切换方案。

14.9 网络 namespace 错误

原非 host 网络/不兼容环境出现:

Error response from daemon: bind-mount /proc/0/ns/net ->
/var/run/docker/netns/9aef6091180e: no such file or directory

另一轮路径末尾为 c5fa75f5cf66。查询:

journalctl -u docker.service | tail -n20

原错误日志的关键内容:

2025-07-15T10:09:53.643257910+08:00 level=error msg="copy stream failed" error="reading from a closed fifo" stream=stdout
2025-07-15T10:09:53.643329889+08:00 level=error msg="copy stream failed" error="reading from a closed fifo" stream=stderr
2025-07-15T10:09:53.655749638+08:00 level=info msg="ignoring event"
container=fed1cee27ad98bae4440eb67db809c4ed52faf449eb3beaaa1fc9efdaaf74453
module=libcontainerd namespace=moby topic=/tasks/delete type="*events.TaskDelete"
2025-07-15T10:09:53.685333795+08:00 level=error
msg="Handler for POST /v1.51/containers/cr/start returned error: bind-mount /proc/0/ns/net -> /var/run/docker/netns/9aef6091180e: no such file or directory"

/proc/0/ns/net 不存在,表示恢复流程没得到可用的网络 namespace 所属进程等状态;这可能涉及 Docker/runtime/CRIU 兼容性及网络恢复实现,不能仅凭它断言“换 CRIU 版本必然解决”或“原 namespace 必须始终保持不变”。应检查 CRIU 日志、进程 PID 和完整版本组合。

原稿建议 host 网络以绕过该实验的一部分网络 namespace 限制,但 host 不是所有应用恢复问题的通用修复。

原稿另一条手工尝试的思路为读取:

CONTAINER_ID=$(docker inspect -f '{{.Id}}' cr)
NS_PATH=$(docker inspect -f '{{.NetworkSettings.SandboxKey}}' cr)
CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' cr)

然后把 /proc/<PID>/ns/net 链接到 netns 目录。对尚未运行/已停止容器,PID 常是 0,此链接不会重建任何 namespace;符号链接也不会创建或恢复网络状态。原稿通过 cut 固定取路径段、再 ln 到不存在 PID 的方案不能作为可执行修复,因此这里不提供一个假称有效的恢复命令。应使用被当前运行时支持并验证过的恢复流程。

15. 手工制作镜像的进一步实践

15.1 主进程必须正确前台运行

容器生命周期由其主进程决定。应用自行 daemonize 后,如果启动它的 PID 1 随即退出,容器也会停止。后台运行并非容器内完全不允许,而是要有正确的主进程/监督机制负责生命周期、信号和退出状态。

Apache 的前台参数示例,二进制路径和启动环境应匹配镜像:

ENTRYPOINT ["httpd"]
CMD ["-D", "FOREGROUND"]

Nginx:

ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"]

HAProxy 启动脚本可以使用:

#!/bin/bash
exec haproxy -db -f /etc/haproxy/haproxy.cfg
CMD ["run_haproxy.sh"]

脚本必须放在 PATH 中并可执行,或使用绝对路径。原稿用 tail -f /etc/hosts、top 等作为保活进程,这只能让容器留在 running,不能证明应用健康,也可能使应用退出后容器仍存活;生产服务应前台 exec 真正应用或使用明确的监督机制。

镜像生命周期为 build → tag → push/pull → run → save/load → 更新/淘汰。镜像更新需要重新构建、分发及替换容器,不会自动改变已有容器文件系统。

docker tag myimage:latest username/myimage:v1.0
docker push username/myimage:v1.0
docker pull username/myimage:v1.0
docker run -p 80:80 nginx
docker save -o backup.tar myimage:latest
docker load -i backup.tar
docker rmi myimage:latest

推送前需要合法账号和目标仓库权限,示例这里只转写命令。

15.2 commit 的名称与标签实验

commit 可用于运行或停止的容器,保存文件系统变化及镜像配置;不会保存进程内存、挂载卷或所有宿主机运行参数。-c/--change 可以修改受支持的镜像指令配置。

docker run -d --name loving_chaum nginx:latest
dcn loving_chaum
docker exec loving_chaum bash -c "echo 'Nginx Web Page by sswang' > /usr/share/nginx/html/index.html"
curl 172.17.0.2

原源容器 4fad98b2fa48,初始返回 Nginx Web Page by sswang。

不指定目标镜像名:

docker commit -a 'sswang' -m 'new image' loving_chaum
docker images

原结果:

sha256:81a1bc8dd2f69893c1f7f7c092424a2590b480069d0a6c9b2b3001117993e33e
REPOSITORY TAG    IMAGE ID       CREATED         SIZE
<none>     <none> 81a1bc8dd2f6   44 seconds ago  192MB

指定名称、不写 tag:

docker exec loving_chaum bash -c "echo 'Nginx Web Page by sswang with commit-1' > /usr/share/nginx/html/index.html"
docker commit -a 'sswang' -m 'new image' loving_chaum mynginx
docker images

原结果:

sha256:0942a94177dd47854fdc1adc4744ae73f9ea2f63cf415b13ae6206ef073007b3
REPOSITORY TAG    IMAGE ID       CREATED        SIZE
mynginx    latest 0942a94177dd   4 seconds ago  192MB
<none>     <none> 81a1bc8dd2f6   2 minutes ago  192MB

显式名称与版本:

docker exec loving_chaum bash -c "echo 'Nginx Web Page by sswang with commit-2' > /usr/share/nginx/html/index.html"
docker commit -a 'sswang' -m 'new image' loving_chaum mynginx:v2
docker images
sha256:1124dae0cf712ae4f8fb8adc3f92eb8ad2af5bc12abae313a1d8ca1d793833c1
REPOSITORY TAG    IMAGE ID       CREATED            SIZE
mynginx    v2     1124dae0cf71   4 seconds ago      192MB
mynginx    latest 0942a94177dd   About a minute ago 192MB
<none>     <none> 81a1bc8dd2f6   4 minutes ago      192MB

这些提交都来自 loving_chaum 的当前状态;命名并不自动把容器切换为基于刚提交的新镜像。

15.3 运行三个版本

docker run --name mynginx1 -d 81a1bc8dd2f6
dcn mynginx1
curl 172.17.0.3

docker run --name mynginx2 -d mynginx
dcn mynginx2
curl 172.17.0.4

docker run --name mynginx3 -d mynginx:v2
dcn mynginx3
curl 172.17.0.5

原结果对应:

容器 引用 原 IP 原响应
mynginx1 无标签镜像 ID 172.17.0.3 Nginx Web Page by sswang
mynginx2 mynginx,即 latest 172.17.0.4 Nginx Web Page by sswang with commit-1
mynginx3 mynginx:v2 172.17.0.5 Nginx Web Page by sswang with commit-2

镜像即使没有 tag 仍可按 ID 使用。实际 IP 和 ID 应重新查询,不把原稿值当固定分配。

15.4 Ubuntu 中安装 Nginx 并提交

初始环境

清理上一组实验容器后:

docker images ubuntu
docker run -it -p 80 --name nginx_ubuntu ubuntu bash

-p 80 表示容器 80 发布到自动分配的宿主机端口,不是固定占用宿主机 80。原镜像 bf16bdcff9c9、78.1 MB。

容器内:

cat /etc/os-release
ls -l /etc/apt/sources.list
cat /etc/apt/sources.list
grep -v '^#' /etc/apt/sources.list.d/ubuntu.sources

原系统记录:

PRETTY_NAME="Ubuntu 24.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04.2 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo

原 sources.list 仅提示实际源已经迁移到 deb822 文件 sources.list.d/ubuntu.sources,原源内容:

Types: deb
URIs: http://archive.ubuntu.com/ubuntu/
Suites: noble noble-updates noble-backports
Components: main universe restricted multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

Types: deb
URIs: http://security.ubuntu.com/ubuntu/
Suites: noble-security
Components: main universe restricted multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

更换软件源和安装

sed -i 's/archive\.ubuntu\.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/ubuntu.sources
sed -i 's/security\.ubuntu\.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/ubuntu.sources
apt update
apt install -y nginx
nginx -v
grep include /etc/nginx/nginx.conf
grep root /etc/nginx/sites-enabled/default | grep -v '#'
echo 'Nginx Web Page by sswang' > /var/www/html/index.nginx-debian.html
nginx -g 'daemon off;'

中科大镜像资料:https://mirrors.ustc.edu.cn/。如果需要用传统 sources.list 重写源,要先明确禁用/替换现有重复条目,不要无条件同时配置两套。

原 Nginx 版本 nginx/1.24.0 (Ubuntu),配置 include:

include /etc/nginx/modules-enabled/*.conf;
include /etc/nginx/mime.types;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
root /var/www/html;

容器常不包含 systemd/systemctl;安装软件后是否已启动由包维护脚本和容器策略决定。若已启动,应先确认进程和端口,避免重复启动 Nginx。

第一次提交

保持容器运行,在另一宿主机终端:

dps
dcn nginx_ubuntu
curl 172.17.0.2
docker commit -m 'ubuntu-nginx' -a 'sswang' nginx_ubuntu ubuntu_nginx:v1
docker run --name nginx -d ubuntu_nginx:v1 nginx -g 'daemon off;'
dcn nginx
curl 172.17.0.3

原发布端口为 0.0.0.0:32768->80/tcp,首页返回 Nginx Web Page by sswang。原提交 ID:

sha256:e9b18e63cd632ff0a509a03993ceef81f24474795d3f1e750ebe3e379e5868c8

这里必须指定 Nginx 命令的原因是源容器的默认命令原为 bash,交互中手动启动 nginx 不会自动变成镜像默认入口;不是 commit 一概不保存启动配置。

设置默认入口再提交

docker commit -a 'sswang' -m 'new image' \
  -c 'ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"]' \
  -c 'CMD []' nginx_ubuntu ubuntu_nginx:v2

docker history ubuntu_nginx:v2
docker inspect --format '{{.Config.Entrypoint}}' ubuntu_nginx:v2
docker inspect ubuntu_nginx:v2 | jq '.[0].Config.Entrypoint'
docker run -d --name nginx-2 ubuntu_nginx:v2
dcn nginx-2
curl 172.17.0.5

显式清空 CMD,防止不需要的原默认参数继续作为入口参数;原稿只设置 ENTRYPOINT,其提交 ID 与此修正模板不要求相同。原历史记录:

74db7f31add6  2 minutes ago  bash                                 65.8MB  new image
bf16bdcff9c9  6 weeks ago    /bin/sh -c #(nop) CMD ["/bin/bash"]    0B
<missing>     6 weeks ago    /bin/sh -c #(nop) ADD file:598ca...     78.1MB
<missing>     6 weeks ago    /bin/sh -c #(nop) LABEL ...            0B
<missing>     6 weeks ago    /bin/sh -c #(nop) LABEL ...            0B
<missing>     6 weeks ago    /bin/sh -c #(nop) ARG LAUNCHPAD_BUILD_ARCH 0B
<missing>     6 weeks ago    /bin/sh -c #(nop) ARG RELEASE          0B

原 ENTRYPOINT 查询结果:

["/usr/sbin/nginx", "-g", "daemon off;"]

history 适合观察历史和层大小,不应单靠它确认最终运行命令;使用 inspect 的 Config。原测试容器 nginx-2 返回同一首页,实际运行时二进制路径必须存在。

15.5 Rocky 中编译 Nginx 并提交

编译准备

docker images rockylinux
docker run -it -p 80 --name nginx_rocky rockylinux:9 bash

原基础镜像 9cc24f05f309、176 MB。容器内安装默认 Nginx 模块真正需要的依赖:

yum install -y gcc make pcre2-devel zlib-devel wget tar gzip
wget https://nginx.org/download/nginx-1.29.0.tar.gz
tar xf nginx-1.29.0.tar.gz
cd nginx-1.29.0
./configure --prefix=/data/server/nginx
make && make install
echo 'Nginx Web Page from Rocky' > /data/server/nginx/html/index.html
/data/server/nginx/sbin/nginx -g 'daemon off;'

原稿还装 gcc-c++、glibc-devel、pcre/pcre-devel、openssl-devel、systemd-devel、libxml2-devel、libxslt-devel、php-gd、gd-devel 等,未启用对应可选模块,且部分包依赖额外仓库,已改为本实验的必需集合。若另启用 SSL、XSLT、image_filter 等模块,应再加入对应依赖和 configure 选项。

宿主机测试:

dcn nginx_rocky
curl 172.17.0.2

原响应为 Nginx Web Page from Rocky。回到容器终端用 Ctrl+C 停止前台 Nginx,Shell 本身仍在运行,再从源码目录上一级清理:

cd /
rm -rf nginx-1.29.0*
yum autoremove

autoremove 是否删除依赖由包管理器决定,并不自动等于卸载全部编译环境;确认动态运行依赖仍满足。

提交前台启动镜像

宿主机:

docker commit -a 'sswang' -m 'new image' \
  -c 'ENTRYPOINT ["/data/server/nginx/sbin/nginx", "-g", "daemon off;"]' \
  -c 'CMD []' nginx_rocky rocky_nginx:v1

docker run -d --name nginx-3 rocky_nginx:v1
dcn nginx-3
curl 172.17.0.2

原提交 ID 779073b2ad8711bff9a6a210da7d2791785278eab61a7d6544671b7e53eaa22f,新容器 7ada6229909d。原稿在不同实验轮中给 nginx_rocky 和 nginx-3 都写了 172.17.0.2;同时存活的同网段容器不会正常共享同一地址,应使用 dcn 实际结果。

启动脚本与日志

原稿脚本后台启动 Nginx,再 tail access.log 来保活,文件大小 102 字节。这个模式不能让容器生命周期跟随 Nginx 故障,信号也可能不正确传递。下面修正为准备日志输出后 exec 前台主进程:

docker exec -it nginx_rocky bash

容器内 /start.sh:

#!/bin/bash
set -e
ln -sf /dev/stdout /data/server/nginx/logs/access.log
ln -sf /dev/stderr /data/server/nginx/logs/error.log
exec /data/server/nginx/sbin/nginx -g 'daemon off;'
chmod +x /start.sh
exit

宿主机:

docker commit -a 'sswang' -m 'new image' \
  -c 'ENTRYPOINT ["/start.sh"]' -c 'CMD []' nginx_rocky rocky_nginx:v2
docker images rocky_nginx
docker run -d --name nginx-4 rocky_nginx:v2
dcn nginx-4
curl 172.17.0.4
docker logs nginx-4

原脚本版本镜像表,仅作为历史记录;修正后文件和元数据不同,ID/容量不会要求相同:

REPOSITORY  TAG IMAGE ID      CREATED         SIZE
rocky_nginx v2  9d9c02097518 12 seconds ago  563MB
rocky_nginx v1  779073b2ad87 28 minutes ago  563MB

原 nginx-4 6ad9fa3b2010,IP 172.17.0.4,首页返回 Nginx Web Page from Rocky。原 tail 日志包含旧记录:

172.17.0.1 - - [15/Jul/2025:05:58:27 +0000] "GET / HTTP/1.1" 200 26 "-" "curl/7.76.1"
172.17.0.1 - - [15/Jul/2025:06:28:24 +0000] "GET / HTTP/1.1" 200 26 "-" "curl/7.76.1"
172.17.0.1 - - [15/Jul/2025:06:34:21 +0000] "GET / HTTP/1.1" 200 26 "-" "curl/7.76.1"

修正脚本将新产生日志直接写入 stdout/stderr,不再用 tail 重放镜像中已有的访问日志。实验清理仍应只针对本组容器;dka 的作用见附录。

16. Dockerfile 扩展指令

16.1 SHELL

SHELL 更改后续 Shell 形式 RUN、CMD、ENTRYPOINT 使用的解释器及参数;不改变 exec 形式自己明确选择的可执行程序。它在 Bash 特性、错误传播和 Windows PowerShell 场景中有实际作用。

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

Windows 示例:

SHELL ["powershell", "-Command"]

镜像必须提供相应 Shell。显式 exec 形式不受默认 SHELL 影响:

SHELL ["/bin/bash", "-c"]
RUN echo "Hello" | cat -
RUN ["/bin/sh", "-c", "echo 'Hello' | cat -"]

可以在阶段中多次切换:

SHELL ["/bin/bash", "-c"]
RUN echo "Using bash"
SHELL ["/bin/sh", "-c"]
RUN echo "Using sh"

pipefail 使管道中任一环节非零时管道返回相应失败,而默认通常仅取最后一项状态;它本身不等于给任意脚本自动加 set -e。

SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update && apt-get install -y curl | tee /tmp/packages.log

-c 表示后面是命令字符串。RUN 最终非零会使构建步骤失败,脚本内部若显式吞掉错误,pipefail 也不能神奇检测所有逻辑故障。

16.2 MAINTAINER

MAINTAINER 是已弃用的旧指令,推荐 LABEL。原稿称 Docker 20.10 已彻底移除并不准确;某些构建器仍为兼容接受它,但不应作为新写法。

MAINTAINER John Doe <john.doe@example.com>

现代写法:

LABEL maintainer="John Doe <john.doe@example.com>"

这里是镜像元数据指令示例,不是保留文档作者介绍。

16.3 HEALTHCHECK

HEALTHCHECK [选项] CMD <命令>

禁用继承的检查:

HEALTHCHECK NONE
选项 含义
--interval 检查间隔,常见默认 30s
--timeout 单次检查超时,常见默认 30s
--start-period 启动宽限期,期间失败通常不累计为不健康;不是禁止开始检查的固定延迟
--retries 连续失败到标记 unhealthy 的阈值,常见默认 3

健康状态通常为 starting、healthy、unhealthy。单机 Docker 不会仅因 unhealthy 自动按 restart 策略重启;restart 主要依据进程退出。Compose 可以用健康状态控制依赖就绪等;Kubernetes 使用它自己的 probes,不能把 Dockerfile HEALTHCHECK 当成 Kubernetes 自动采用的探针。

Nginx 示例要求镜像内提供 curl:

HEALTHCHECK --interval=5s --timeout=3s \
    CMD curl -f http://localhost/ || exit 1
EXPOSE 80

curl -f 对许多 HTTP 4xx/5xx 失败,并不是“只要不等于 200 就失败”;若必须严格 200,应在专门脚本中比较状态码。

原 MySQL 进程可达性示例,变量由容器 Shell 展开:

HEALTHCHECK --interval=10s --timeout=5s --retries=3 \
    CMD mysqladmin -u root -p"$MYSQL_ROOT_PASSWORD" ping || exit 1

mysqladmin ping 主要验证服务进程能响应,某些认证失败也可能返回服务器存活;它不等于数据库账号、表和业务查询均可用。需要业务就绪时使用具备最小权限的探针账号执行实际必要查询,并按真实凭证管理方式配置。

自定义脚本:

COPY healthcheck.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/healthcheck.sh
HEALTHCHECK --interval=1m --timeout=10s \
    CMD /usr/local/bin/healthcheck.sh || exit 1

原稿没有给 healthcheck.sh 正文,需提供实际检测逻辑,不能创建空脚本冒充有效检查。

16.4 STOPSIGNAL

STOPSIGNAL SIGTERM

也可以按应用需要选择 SIGINT、SIGQUIT 等。最后一次有效声明决定镜像的停止信号;容器运行参数也可覆盖。docker stop 先发配置的停止信号,超时后通常再 SIGKILL,因此 STOPSIGNAL 本身不保证一定优雅退出。

信号 常见 Linux 编号 含义
SIGTERM 15 默认终止请求,应用可捕获并清理
SIGINT 2 常由 Ctrl+C 产生,应用可自行处理
SIGQUIT 3 默认处置通常终止并生成 core,但 Nginx 等应用可将它定义为优雅退出
SIGKILL 9 无法捕获/忽略的强制终止,不适合作为常规优雅关闭信号
SIGTSTP 常见为 20 作业控制暂停信号,编号可因架构不同,不适合作为普通服务停止策略

例如:

STOPSIGNAL SIGQUIT

适合配合正确前台运行的 Nginx。原稿 STOPSIGNAL 9 是强制终止语法示例,不应被误当作数据安全配置。

附录:课程快捷命令

本文使用的 dps、dcn、dka 是主教材自定义快捷命令,不是 Docker 自带子命令。仅需理解其作用时,可直接用原生命令替代。以下定义供同样的单 bridge 实验使用:

alias dps='docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Ports}}"'
alias dcn='docker inspect --format "{{.NetworkSettings.Networks.bridge.IPAddress}}"'
alias dka='cs=$(docker ps -aq); [ -z "$cs" ] || docker rm -f $cs'

dcn 这里明确查询默认 bridge;host 网络没有独立容器 IP,自定义网络应按实际名称查询。dka 会强制删除当前 daemon 的全部容器,课程末尾的“清理环境”不应被误解成实际业务主机的常规无害操作。

评论