企业级容器技术 Docker

企业级容器技术 Docker 1. 容器基础知识 1.1 容器存在的意义 集装箱使用标准尺寸,把货物封装成可以运输、装卸和隔离的单元。软件容器借用这一思路,把应用与依赖封装成标准交付物,减少环境差异,便于迁移、横向扩容和重复部署。 容器技术能够提高资源利用率,缩短应用启动和扩容时间。容器启动可以很快

企业级容器技术 Docker

1. 容器基础知识

1.1 容器存在的意义

集装箱使用标准尺寸,把货物封装成可以运输、装卸和隔离的单元。软件容器借用这一思路,把应用与依赖封装成标准交付物,减少环境差异,便于迁移、横向扩容和重复部署。

标准化集装箱与运输成本

集装箱运输的标准化过程

容器技术能够提高资源利用率,缩短应用启动和扩容时间。容器启动可以很快,但真实业务的就绪速度还取决于镜像下载、初始化、数据和依赖服务,不能保证所有应用都在秒级就绪。

学习容器技术的配图

容器技术从 chroot 等隔离机制发展而来,逐渐结合 namespaces、cgroups 和文件系统技术。Docker 于 2013 年推出,以镜像、构建、分发和运行工具链降低使用门槛,推动应用交付标准化。

容器技术演进

1.2 Docker 简介

Docker 是面向开发者和团队的容器化工具平台。

Docker 官网介绍

早期 Docker 使用 LXC,后来采用 libcontainer,相关底层实现发展为 runc。这个变化与 KVM 是否作为内核模块实现没有因果关系。Docker Engine 使用客户端/服务器架构,通过 API 管理镜像、容器、网络和卷,核心理念是 Build、Ship、Run。

Docker Engine 的开源组件通常采用 Apache 2.0 等开源许可;Docker Desktop 是独立产品,其订阅和使用条件应单独查看。Linux 容器共享宿主机内核,通过 namespaces 隔离资源视图、通过 cgroups 管理资源。容器通常比完整虚拟机轻,但仍有存储、网络和运行时开销,不能把虚拟机开销统一写成固定百分比,也不能说容器没有开销。

参考:

Docker 管理应用的封装、分发、部署和运行,方便开发、测试与生产复用环境。镜像仍受操作系统、CPU 架构、内核能力和外部依赖约束;跨平台运行需要适配平台镜像或额外的虚拟化/模拟支持。

使用 Docker 前后的交付流程

1.3 容器与虚拟机

虚拟机虚拟硬件,并运行独立的客户机内核和操作系统。普通 Linux 容器中的应用直接使用宿主机 Linux 内核,不为每个容器再启动一套内核。

容器可以有隔离的根文件系统、进程编号、网络设备、主机名和 IPC 资源。隔离配置与运行参数有关:共享网络、共享 PID、特权模式等会改变边界。共享内核也意味着容器不能被视为与虚拟机完全等价的安全隔离。

虚拟机与容器结构对比

1.4 Docker 核心结构

CLI 接收命令,通过 API 与 Docker daemon 通信;daemon 管理镜像、容器、网络和数据卷。常见调用链为 Docker CLI → Docker daemon → containerd → shim/runc → 容器进程。

CLI、API、daemon 与资源

客户端、Docker 主机和镜像仓库

Namespace

Namespace 是 Linux 内核提供的资源视图隔离机制。

类型 隔离内容
PID 进程编号和可见进程树;不是按用户自动划分
Network 网络设备、IP 地址、路由表等
IPC System V IPC、POSIX 消息队列等
Mount 挂载点视图;配合根文件系统构建容器文件视图
UTS hostname、domainname
User 用户和组 ID 映射;Docker 是否启用取决于配置
Cgroup cgroup 根目录视图
Time 部分系统时钟的视图,依内核和配置支持

不同 namespace 的资源视图

检查内核配置时使用实际内核版本:

grep -A10 CONFIG_NAMESPACES "/boot/config-$(uname -r)"

参考:https://man7.org/linux/man-pages/man7/namespaces.7.html、https://en.wikipedia.org/wiki/Linux_namespaces。

Cgroups

Linux Control Groups 最早由 Google 工程师发起,早期称为 process containers,后更名为 cgroups,合入 Linux 2.6.24。它负责对进程组进行资源限制、分配、统计和控制,例如 CPU、内存、块设备 I/O 和进程数量。

网络带宽一般需要结合 tc 等网络机制控制,不能把 cgroups 理解为自动完成所有网络限速的工具。可用控制器与 cgroup v1/v2、内核配置、systemd 设置有关。

1.5 容器管理与标准

容器管理工具与运行时关系

工具 定位与参考
LXC 使用 namespaces、cgroups 的系统容器工具;https://linuxcontainers.org/
Docker 镜像、容器、网络、卷的开发和运行管理平台;https://www.docker.com/
Pouch 阿里巴巴发起的容器项目;https://github.com/alibaba/pouch、https://gitee.com/mirrors/Pouch
Podman 支持无守护进程、rootless 和 Pod 的容器管理工具;https://podman.io/、https://github.com/containers/podman
containerd 从 Docker 项目独立出来的高层容器运行时;https://containerd.io/
nerdctl 面向 containerd 的 Docker 风格 CLI;https://github.com/containerd/nerdctl

OCI

Open Container Initiative(开放容器倡议)推动容器标准化与互操作,减少厂商绑定。其规范包括 Runtime、Image 和 Distribution,不只有两项。

OCI 标识

运行时规范定义容器配置及创建、启动、状态查询、终止、删除等生命周期。runc 等实现遵循这些规范。

运行时规范与容器生命周期

镜像规范定义分层文件系统、镜像配置、manifest、image index 等内容。镜像层按内容寻址,配置中包含环境变量、默认命令等元数据。

OCI 镜像索引与分层结构

镜像层、镜像索引与配置关系

Distribution 规范定义镜像和其他内容的分发交互。标准化促进不同构建工具、镜像仓库和运行时之间的协作,但镜像仍需适配目标平台。

低层运行时接近内核,负责真正创建和启动容器;高层运行时通常还负责镜像、快照和生命周期管理。下图包含部分历史项目,用于理解抽象层次。

低层与高层运行时

参考:https://opencontainers.org/。

镜像仓库与编排

  • Docker Hub:公共镜像服务。
  • Distribution Registry:可自行部署的镜像仓库服务,通常不自带完整 Web 管理界面。
  • Harbor:具有 Web 界面、认证、权限与镜像管理能力的开源仓库项目。

编排涉及容器调度、集群、服务发现和生命周期管理:

  • Docker Compose:定义并运行多容器应用,常用于单机环境。
  • Docker Swarm:Docker 集成的集群编排能力,支持 overlay 网络。
  • Mesos + Marathon:历史上使用过的分布式资源管理和编排组合。
  • Kubernetes:源自 Google 的容器编排系统,通过 CRI 使用 containerd、CRI-O 等运行时。Kubernetes 1.24 移除了内置 dockershim;仍可运行符合标准的 Docker 构建镜像。

容器与编排生态配图

2. Docker 快速入门

2.1 安装环境

Docker Engine 可安装于受支持的 Linux 发行版。macOS、Windows 通常通过 Docker Desktop 等产品运行,Linux 容器依赖其中提供的 Linux 环境。内核最低要求随 Docker 和发行版变化,不应将历史上的“3.10 以上”当成所有新版的支持承诺。

支持平台与架构示例

历史上产品分 CE 与 EE;Docker 企业业务后来整合进 Mirantis 相关产品,不能把 Docker Business 直接当成旧 EE 的改名。

Docker 退役产品说明

参考:

以下均为课程实验命令。安装、删除和清理操作应在对应实验主机进行;文档整理过程未运行这些操作。

2.2 使用 APT 安装

参考:https://docs.docker.com/engine/install/ubuntu/、https://developer.aliyun.com/mirror/docker-ce、https://mirrors.tuna.tsinghua.edu.cn/help/docker-ce/。

基础环境和软件源

以下示例以 root 执行:

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-cache madison docker-ce | head -n2

# 安装仓库当前提供的版本及配套组件
apt install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

# 如需固定版本,先从 madison 输出复制实际存在的版本号
# apt install docker-ce=<实际版本> docker-ce-cli=<实际版本> containerd.io

systemctl is-active docker
docker version
docker info
ip addr show docker0
nft list ruleset | wc -l
iptables -vnL

PDF 中的 5:29.8.1-1~ubuntu.24.04~noble 是原稿的版本字符串示例,未在本次整理中联网验证,不应直接认定仓库一定存在该版本。Docker 启动后通常创建默认 bridge 网络和相应防火墙规则;具体规则取决于 Docker 版本、网络模式和防火墙后端。

镜像加速配置

镜像加速地址存在白名单、认证、访问地区、同步范围和服务变更等差异。以下为原稿列举的候选地址,没有在整理时验证可用性:

https://hub-mirror.c.163.com
https://f1361db2.m.daocloud.io
https://docker.1ms.run
https://docker.xuanyuan.me
https://docker.m.daocloud.io
https://docker.1panel.live
https://hub.rat.dev
https://doublezonline.cloud
https://docker.mrxn.net
https://docker.anyhub.us.kg
https://dislabaiot.xyz
https://docker.fxxk.dedyn.io

清华 Docker CE 软件包镜像与 Docker Hub 镜像加速不是同一个服务,不能把任意软件镜像下载目录当成 Registry mirror。

curl https://docker.xuanyuan.me/v2/

原稿示例响应:

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

这个响应说明请求到达了 Registry API 并需要认证,不能单凭它证明目标镜像一定可以拉取。

合并修改已有配置,不要覆盖其他必要设置:

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

文件位置为 /etc/docker/daemon.json。Docker 支持通过 SIGHUP 重载部分配置;并非所有选项都能热重载。修改镜像加速等设置后,按所用版本支持方式操作;以下课程示例采用重启。

systemctl restart docker
docker info | grep -A2 Regi
docker run hello-world
pstree -p | grep docker

首次运行时可能显示:

Unable to find image 'hello-world:latest' locally

随后 Docker 尝试拉取镜像。耗时取决于网络,不能以固定等待十秒判断成功或失败。

阿里云专属加速

访问 https://cr.console.aliyun.com/,登录后查看镜像加速器及当前账户的使用说明。

阿里云容器镜像控制台

阿里云镜像加速说明

原稿账户示例为 https://82ue03hk.mirror.aliyuncs.com,应替换为自己的可用地址。多个地址使用 JSON 数组配置:

{
  "registry-mirrors": [
    "https://docker.xuanyuan.me",
    "https://82ue03hk.mirror.aliyuncs.com"
  ],
  "insecure-registries": []
}
cp /etc/docker/daemon.json{,.bak}
systemctl restart docker

移除 APT 安装的 Docker

先查看、删除实验容器,再停止服务。下面清理数据目录会删除本机容器、镜像或卷等数据,应与卸载软件包区分。

docker ps -a
docker rm -f 9e
systemctl stop docker.service docker.socket
apt purge -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

# 仅在确认实验数据不再需要时执行
rm -rf /etc/docker /var/lib/docker /var/lib/containerd
rm -f /etc/apt/sources.list.d/docker.list
rm -f /etc/apt/keyrings/docker.gpg
reboot

原稿示例容器 ID 为 9e8df1829222,可用唯一前缀 9e。重启可使临时网络设备和规则重新初始化;不是软件包卸载的必需步骤,也不能替代检查实际遗留配置。

2.3 二进制离线安装

适用于不能通过在线包仓库安装的主机。需预先获取适配架构的压缩包及依赖环境,避免与已有 Docker 服务文件、进程和数据目录冲突。原稿建议恢复干净快照,这属于实验环境准备,不是安装方式的绝对要求。

参考:

压缩包命名形如 docker-<版本>.tgz。原稿使用 29.7.2,下载前需要确认目录中实际存在的版本。

mkdir -p /data/{server,softs}
# 将 <实际版本> 替换为已核实存在的版本
wget 'https://mirrors.aliyun.com/docker-ce/linux/static/stable/x86_64/docker-<实际版本>.tgz' \
  -O /data/softs/docker.tgz
tar xf /data/softs/docker.tgz -C /data/server/
chown -R root:root /data/server/docker
echo 'export PATH=$PATH:/data/server/docker' > /etc/profile.d/docker.sh
source /etc/profile.d/docker.sh

命令启动

dockerd >/tmp/dockerd-lab.log 2>&1 &
# 原稿后台作业 PID 示例:4985
docker version

# 使用实际 PID,优先正常发送 SIGTERM
kill -TERM 4985

原稿用 kill -9 4985 结束 daemon;正常停止应先用 SIGTERM,SIGKILL 仅在无法正常退出时使用。

systemd 服务

服务文件放入 /etc/systemd/system/docker.service:

[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network-online.target firewalld.service
Wants=network-online.target
StartLimitBurst=3
StartLimitIntervalSec=60s

[Service]
Type=notify
Environment="PATH=/data/server/docker:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"
ExecStart=/data/server/docker/dockerd -H unix:///var/run/docker.sock
ExecReload=/bin/kill -s HUP $MAINPID
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
TimeoutStartSec=0
Delegate=yes
KillMode=process
Restart=on-failure

[Install]
WantedBy=multi-user.target

如果使用 Shell heredoc 写入服务文件,使用 <<'EOF',使 $MAINPID 保持原样留给 systemd。

systemctl daemon-reload
systemctl start docker
docker info | grep Version
mkdir -p /etc/docker
# 按上一节写入 daemon.json
systemctl restart docker
docker info | grep -A2 Regi
docker run hello-world

移除二进制安装

docker ps -a
docker rm -f 27a21e5655da
systemctl disable --now docker
rm -rf /etc/docker /var/lib/docker /var/lib/containerd
rm -rf /var/run/docker /var/run/containerd
rm -f /var/run/docker.sock /var/run/docker.pid
rm -rf /data/server/docker
rm -f /etc/systemd/system/docker.service /etc/profile.d/docker.sh
systemctl daemon-reload
reboot

根据真实安装位置清理服务文件;原稿使用 /lib/systemd/system/docker.service,如采用该位置应相应调整。

2.4 脚本部署入口

原稿引用配套 scripts/onekey_deploy_docker_offline.sh,PDF 不含完整脚本,以下保留调用与交互,不补造实现。

/bin/bash onekey_deploy_docker_offline.sh
systemctl status docker.service
source /etc/profile.d/docker.sh
docker info

已安装时再次执行的交互示例:

[INFO] ... 22:59:18 ========== 开始执行Docker安装脚本 ==========
[INFO] ... 22:59:18 开始检查系统环境...
[WARNING] ... 22:59:18 系统已安装Docker ...
是否继续安装?(y/n): y

取消示例:

[INFO] ... 22:59:41 ========== 开始执行Docker安装脚本 ==========
[INFO] ... 22:59:41 开始检查系统环境...
[WARNING] ... 22:59:41 系统已安装Docker ...
是否继续安装?(y/n): n
[INFO] ... 22:59:43 用户取消安装

2.5 Docker 命令环境

Docker CLI 到容器的调用链

Usage: docker [OPTIONS] COMMAND
Common Commands:
Management Commands:
Swarm Commands:
Commands:
Global Options:

管理命令按资源分组;许多传统命令仍保留,例如 docker images 与 docker image ls。

docker
docker help
docker --help
docker help COMMAND
docker COMMAND --help
docker version
docker info

参考:https://docs.docker.com/reference/、https://docs.docker.com/reference/cli/docker/。不同宿主机发行版上 CLI 的基本语法相同,但字段、默认行为与输出随版本和配置变化。

3. 容器镜像管理

3.1 镜像结构

镜像是创建容器的只读模板,包含根文件系统内容、应用和启动配置。容器在镜像基础上增加独立的可写层。

镜像、容器与宿主机关系示意

图中的内核属于宿主机,不是随普通 Linux 镜像分发的一层。/proc、/sys、/dev 中的运行时内容也通常由运行时挂载或生成。

镜像通过多个只读差异层复用内容,容器看到叠加后的文件系统。底层文件被上层修改时,上层记录变化;删除底层文件可通过 whiteout 标记隐藏,并不直接改写底层。

只读镜像层与可写容器层

联合挂载或快照实现取决于存储驱动/快照器。不要把 whiteout 当成与只读、读写并列的通用权限,也不是每条 Dockerfile 指令都产生文件系统差异层。

按基础环境与业务组织镜像

3.2 常见镜像与名称

Alpine 是小型 Linux 发行版,常用 musl libc 与 apk;BusyBox 将许多常用 UNIX 工具整合为一个可执行程序。小镜像往往不包含 bash、编译器或完整 GNU 工具。镜像大小随版本和平台改变,不能固定为 5 MB 或用单一 Ubuntu 大小作普遍比较。

docker images
docker image ls

原稿本地镜像输出:

REPOSITORY   TAG      IMAGE ID       CREATED        SIZE
debian       latest   b3a422523a11   30 hours ago   117MB
nginx        latest   9a9a9fd723f1   6 days ago     192MB
alpine       latest   cea2ff433c61   4 weeks ago    8.31MB
ubuntu       latest   bf16bdcff9c9   4 weeks ago    78.1MB
busybox      latest   6d3e4188a38a   9 months ago   4.28MB
字段 含义
REPOSITORY 仓库路径
TAG 可变标签;省略时一般使用 latest
IMAGE ID 镜像内容标识,默认通常显示哈希前缀
CREATED 镜像创建时间
SIZE 报告的镜像大小,多镜像间可能共享层

镜像引用通常写成:

[registry[:port]/][namespace/]repository[:tag]
registry/namespace/repository@sha256:<digest>
示例 解释
nginx docker.io/library/nginx:latest
ubuntu:22.04 官方 Ubuntu 的 22.04 标签
library/nginx:latest 显式写出 Docker Hub 官方镜像命名空间
nginx:alpine nginx 的 alpine 标签
registry.cn-hangzhou.aliyuncs.com/library/nginx:latest 指定仓库地址及路径;需确认目标仓库实际提供它

官方镜像通常位于 Docker Hub 的 library;用户/组织镜像使用相应命名空间;私有镜像需明确仓库地址。latest 是发布者可任意指向的普通标签,不保证最新、稳定,也不由 Docker 自动按时间排序选择。

3.3 拉取、历史与详情

docker pull --help
docker image pull --help
docker pull redis
docker pull registry.cn-hangzhou.aliyuncs.com/sswang-cloudnative/web-nginx:v1
docker pull registry.cn-hangzhou.aliyuncs.com/sswang-cloudnative/web-tomcat:v1
docker pull registry.cn-hangzhou.aliyuncs.com/sswang-cloudnative/web-django:v1
docker images

# 其他练习镜像;是否可拉取取决于仓库状态
docker pull busybox
docker pull alpine
docker pull debian
docker pull ubuntu
docker pull nginx
docker pull centos
docker pull centos:7
docker pull rockylinux
docker pull rockylinux:9

docker history --help
docker image history --help
docker history registry.cn-hangzhou.aliyuncs.com/sswang-cloudnative/web-nginx:v1

docker inspect --help
docker image inspect --help
docker image inspect registry.cn-hangzhou.aliyuncs.com/sswang-cloudnative/web-nginx:v1

history 查看构建历史;image inspect 查看镜像元数据;通用 inspect 还可检查容器、网络、卷等对象,必要时指定 --type 消除同名歧义。

3.4 搜索和替代获取方式

docker search [OPTIONS] TERM
选项 含义
-f, --filter 按条件过滤
--format Go 模板格式化输出
--limit 限制条数,通常默认 25,受版本/API 限制
--no-trunc 不截断描述
docker search --filter is-official=true nginx
docker search --filter stars=1000 nginx
docker search --format '{{.Name}}: {{.Description}}' nginx

原稿还列出 --filter automated=true,该历史自动构建字段和过滤器在不同 Docker Hub/API 版本中可能不再支持,不应作为稳定筛选方式。

镜像 mirror 配置主要作用于拉取;docker search 仍要访问 Docker Hub 搜索 API。原稿网络故障输出:

Error response from daemon: Get "https://index.docker.io/v1/search?q=nginx&n=25":
dial tcp 199.59.149.237:443: connect: connection refused

如为 daemon 配代理,HTTPS_PROXY 值的协议应与代理服务本身匹配。通过 HTTP CONNECT 代理访问 HTTPS 目标时,代理地址使用 http:// 很常见;并非 HTTPS_PROXY 的值一律不能使用 https://。

docker pull nginx
docker pull ccr.ccs.tencentyun.com/library/nginx:latest

# 原稿历史 Kubernetes 镜像及其替代路径示例
docker pull k8s.gcr.io/coredns:1.2.2
docker pull registry.aliyuncs.com/google_containers/coredns:1.2.2

Kubernetes 社区当前镜像仓库使用 registry.k8s.io。历史路径及第三方同步仓库不保证继续提供特定旧版本,改写仓库地址也不保证内容相同,应检查目标镜像来源、平台与摘要。

原稿历史请求可能出现:

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

第三方查询站点示例为 https://docker.aityp.com/。原稿将 docker.io/flannel/flannel:v0.24.1 输入查询站点,按页面提供的替代仓库命令获取镜像。

镜像查询站点

搜索 flannel 镜像

选择镜像和平台

替代拉取命令示例

原稿直接拉取时的超时示例:

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)

第三方站点可能因 DNS、服务调整或上游访问问题失效,页面给出的命令也需要核对。

Docker Proxy 站点示例

3.5 删除、导出与加载镜像

删除镜像

docker rmi --help
docker image rm --help
docker rmi centos:7
docker rmi rockylinux:9 ubuntu:latest debian:latest
docker images

docker rmi 删除镜像引用,必要时清理不再引用的层;-f 可强制处理某些冲突,但不能把它理解成删除运行容器及其数据的通用选项。先检查关联容器。

save / load

save 保存镜像层、配置及所选镜像引用,用于镜像备份和离线传输;load 读取该格式,恢复归档中保存的标签和镜像数据。

docker save --help
docker images nginx
docker save -o nginx.tar nginx:latest
docker save -o multiple_images.tar nginx redis alpine busybox
docker save nginx:latest | gzip > nginx.tar.gz

docker load --help
docker rmi nginx redis alpine busybox
docker load -i multiple_images.tar

docker rmi nginx
gunzip -c nginx.tar.gz | docker load

docker rmi nginx
docker load -i nginx.tar -q

load 解析归档元数据并注册镜像,相关数据保存在 Docker 当前配置的数据存储中。docker export 生成的根文件系统 tar 应使用 docker import;不能写成 load 会自动把它加载为匿名镜像。

3.6 镜像标签

docker image tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]

标签是同一镜像的另一个引用,不复制整个镜像。省略 tag 通常使用 latest,不是创建一个空标签。<none> 常见于旧标签被新镜像取代等情形,并不专指构建失败。

docker tag --help
docker image tag --help
docker images | grep web-nginx
# 原稿镜像 ID:48d5de12bb81

docker tag registry.cn-hangzhou.aliyuncs.com/sswang-cloudnative/web-nginx:v1 web-nginx:v1
docker rmi registry.cn-hangzhou.aliyuncs.com/sswang-cloudnative/web-nginx:v1

REPO_ADDR='registry.cn-hangzhou.aliyuncs.com/sswang-cloudnative'
docker tag "$REPO_ADDR/web-tomcat:v1" web-tomcat:v1
docker tag "$REPO_ADDR/web-django:v1" web-django:v1
docker tag registry.aliyuncs.com/google_containers/coredns:1.2.2 coredns:1.2.2
docker rmi "$REPO_ADDR/web-tomcat:v1"
docker rmi "$REPO_ADDR/web-django:v1"
docker rmi registry.aliyuncs.com/google_containers/coredns:1.2.2
docker images

3.7 定制镜像输出

可用字段包括 .Repository、.Tag、.ID、.Digest、.CreatedAt、.Size。

docker images --format '{{.Repository}}:{{.Tag}}'
docker images --format '{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}'
docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}'
docker images --format '{"name":"{{.Repository}}","tag":"{{.Tag}}","size":"{{.Size}}"}'
docker images --format '{"name":"{{.Repository}}","tag":"{{.Tag}}"}' | jq .
docker images --format '{{.Size}}'

# 查看实际存储占用及可回收情况
docker system df
docker system df -v

不要对 {{.Size}} 的结果直接用 awk -F'M' 相加:输出可能同时有 kB、MB、GB,多个标签和镜像还会共享层;即使统一单位,也不能代表独占磁盘用量。

4. 容器管理

4.1 生命周期

容器经历创建、运行、暂停/恢复、停止、重新启动和删除等状态。镜像用于创建容器,容器主进程结束后容器停止;停止不等于删除。

容器生命周期状态转换

容器创建和启动流程

4.2 第一个容器与常用参数

前台和后台

docker run nginx
docker run -d nginx

默认运行时 CLI 附加到容器的标准输出/标准错误并等待退出;这叫前台附加,不自动等于交互式输入,也不自动分配 TTY。-d 使 CLI 返回,容器在后台运行。无论前台还是后台,容器主进程结束都会使容器停止。

前台可使用 Ctrl+C 发送中断信号,具体效果受信号代理、PID 1 和应用处理方式影响。

查看容器

docker ps --help
docker ps
docker ps -a
docker ps -q
docker ps -l
docker ps --filter 'status=running'
docker ps --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}'

原稿示例:

CONTAINER ID   IMAGE   COMMAND                  CREATED          STATUS         PORTS    NAMES
f5229c2e1d2d   nginx   "/docker-entrypoint.…"   45 seconds ago   Up 44 seconds  80/tcp   inspiring_wright
字段 含义
CONTAINER ID 容器 ID,可用不冲突的前缀引用
IMAGE 基础镜像引用
COMMAND 启动命令,默认可能截断
CREATED 创建时间
STATUS 运行、停止、重启或健康状态等
PORTS 暴露端口及发布到宿主机的映射;80/tcp 本身不表示已发布
NAMES 容器名称

ps 列出状态,不实时显示 CPU、内存资源占用,资源监控使用 stats。过滤器包括 status、label、name、exited 等;多个不同键通常共同约束,同一键的重复过滤可能为 OR,应以具体过滤器语义为准。

格式字段还包括 .Image、.Command、.CreatedAt、.RunningFor、.Labels、.Mounts、.Networks。

名称与重命名

docker run --name my-nginx -d nginx
docker ps
docker rename my-nginx new-nginx

同一 Docker daemon 内容器名必须唯一,包含已停止容器。容器名称与网络 --network-alias 不是完全相同的概念;网络内名称解析还取决于网络类型。

交互式启动

参数 作用
-i, --interactive 保持标准输入打开
-t, --tty 分配伪终端
docker run --name nginx-it -it nginx /bin/bash

原稿容器内交互:

root@a8783baff9bc:/# hostname
a8783baff9bc
root@a8783baff9bc:/# pwd
/
root@a8783baff9bc:/# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
root@a8783baff9bc:/# exit
exit

镜像不一定提供 bash,可尝试其实际提供的 sh:

docker run --name nginx-it2 -it nginx /bin/sh

原稿终端遇到退格显示 ^H 的现象:

# ls^H^H^H^C
# ls^H^H
/bin/sh: 2: : not found

该问题与终端 erase 配置、行编辑和 Shell 有关,并非 sh 普遍不能正常输入。可以检查 stty -a、终端退格设置,或使用支持行编辑的 Shell。Ctrl+C 可取消当前输入,Ctrl+U 通常可清除输入行;Ctrl+退格的行为随终端而变。

退出 Shell 使用 exit 或 Ctrl+D;如果它是容器主进程,容器会停止。保持容器继续运行并脱离交互会话的默认按键是先 Ctrl+P,再 Ctrl+Q,不是 Ctrl+Q+P;终端、输入模式和 --detach-keys 可影响该行为。

自动删除

--rm 在容器退出后自动删除容器及相应匿名卷,不删除具名卷;它不等于所有挂载数据都会被清空。

docker run --rm -d --name nginx-rm nginx
docker stop nginx-rm
docker ps -a -f 'name=nginx-rm'

docker run -it --name nginx-rm --rm nginx /bin/bash
# 容器内:exit
docker ps -a -f 'name=nginx-rm'

原稿退出后的列表只有表头:

CONTAINER ID   IMAGE   COMMAND   CREATED   STATUS   PORTS   NAMES

适合一次性测试和构建任务。--rm 不能与 --restart 同时使用。

重启策略

策略 行为
no 默认,不自动重启
on-failure[:N] 非零退出码时重试,可限制次数;daemon 重启本身不触发该策略恢复
always 退出后重启;手动 stop 后暂停重启,直到手动 start 或 daemon 再启动
unless-stopped 类似 always,但手动停止的容器在 daemon 再启动后仍保持停止

长期服务可根据维护流程选择 always/unless-stopped;需失败重试的任务使用 on-failure;一次性任务通常为 no。重启策略不能替代健康检查、数据持久化或服务高可用架构。

docker run -d --restart=always --name nginx-restart nginx
docker ps
docker stop nginx-restart
docker ps -a
systemctl restart docker
docker ps

原稿把手动 stop 后的暂停重启说成 Docker 20.10 才引入,并描述旧版必然不断重启,缺乏依据;这里按当前文档语义说明,不保留该版本分界断言。

4.3 删除容器

docker rm [OPTIONS] CONTAINER [CONTAINER...]
选项 含义
-f, --force 强制删除运行容器,涉及 SIGKILL
-v, --volumes 删除关联匿名卷,不删除具名卷
-l, --link 删除旧式 link 关系;不是任意 network disconnect
docker ps -a
docker rm e1cb8ec5d341
docker rm e7
docker rm my-nginx

默认删除运行容器的错误示例:

Error response from daemon: cannot remove container "96110f4b62a0": container is
running: stop the container before removing or force remove
docker rm -f 96110f4b62a0

# 删除创建超过 24 小时的已停止容器,并清理匿名卷
docker container prune --filter 'until=24h'
# 如果还需清理关联卷,应单独按卷的实际用途检查处理

# 仅查看所有容器 ID
docker ps -a -q

# 删除全部实验容器,空列表时不执行 rm
cs=$(docker ps -a -q)
[ -z "$cs" ] || docker rm -f $cs

原稿的 docker ps --filter until=24h 过滤方式不成立,改用 container prune 的时间过滤。docker rm -f $(docker ps -a -q) 在没有容器时会缺少参数;上例增加空列表判断。原稿另引用配套 scripts/docker_clean_all_container.sh,PDF 未提供完整实现。

4.4 进程、资源、详情与日志

top

docker top 使用类似 ps 的方式列出容器进程,是快照查询,不是交互式 Linux top。

docker ps
docker top <容器名或ID>
docker top <容器名或ID> aux

stats

# 准备两台实验容器
docker run -d nginx
docker run -d nginx
docker ps -q

docker stats
docker stats 43007f11dfe8
docker stats 43007f11dfe8 145b5a7f6b84
docker stats --no-stream 145b5a7f6b84
docker stats --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemPerc}}' --no-stream

默认持续显示 CPU、内存、网络 I/O、块 I/O 和进程数等;Ctrl+C 退出。--no-stream 只取一次。

CONTAINER ID   NAME   CPU%   MEM USAGE/LIMIT   MEM%   NET I/O   BLOCK I/O   PIDS

原稿自定义输出:

NAME              CPU %   MEM %
upbeat_goldberg   0.00%   0.29%
sharp_gates       0.00%   0.29%

inspect 与网络地址

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

--format/-f 使用 Go 模板;--type 指定 container、image、network、volume 等对象类型。

docker inspect sharp_gates

# 默认 bridge 网络,bridge 是具体网络名称
docker inspect --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' sharp_gates

# 自定义网络应按实际名称查询,不能一律写 bridge
docker inspect --format '{{(index .NetworkSettings.Networks "app-net").IPAddress}}' sharp_gates

# 列出所有网络及地址,避免多地址无分隔地拼接
docker inspect -f '{{range $name, $net := .NetworkSettings.Networks}}{{printf "%s %s\n" $name $net.IPAddress}}{{end}}' sharp_gates

上面的 bridge 示例补入了归档短版的独有内容。旧字段 .NetworkSettings.IPAddress 在部分旧版默认 bridge 场景可用,新版本可能为空或移除,优先使用 .NetworkSettings.Networks。

logs

docker logs 获取日志驱动所提供的容器标准输出/标准错误日志,并非自动读取容器中所有日志文件。

选项 用途
-f, --follow 持续跟踪
--tail N 最近 N 行
--since、--until 按时间过滤
-t, --timestamps 增加时间戳
--details 显示配置允许输出的附加属性
docker logs sharp_gates
docker logs --tail 5 sharp_gates
docker logs --tail 5 -t sharp_gates
docker logs -f sharp_gates
docker logs --since '2025-10-01T00:00:00' --until '2025-10-02T12:00:00' sharp_gates

使用默认数据根目录和 json-file 日志驱动时,日志常位于 /var/lib/docker/containers/<ID>/<ID>-json.log。日志驱动、数据根目录可改变位置,不应手动改写由 Docker 管理的日志文件。

4.5 stop、kill 与 exec

停止和发送信号

docker ps
docker kill <容器名>
docker ps -a
docker kill -s SIGTERM my-container
命令 信号及过程 用途
docker stop 先发送配置的停止信号,超时再 SIGKILL;Linux 默认等待通常为 10 秒,可配置 正常关闭
docker kill 默认 SIGKILL,可用 -s 指定其他信号 无响应或定向发送信号

停止信号通常默认 SIGTERM,也可能由镜像 STOPSIGNAL 或运行参数指定。优先优雅退出以便应用刷盘和清理资源。

docker stop my-container
docker kill my-container

仍无法停止时,应检查进程是否处于不可中断等待、runtime/daemon 日志等;重启 Docker 服务会影响其他容器,不能当作对所有故障必然有效的步骤。

exec

exec 在运行中的容器里创建新进程;attach 附加现有主进程的 I/O,两者不同。

docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
选项 含义
-i 保持标准输入
-t 分配伪终端
-d 后台执行
-e KEY=VALUE、--env-file 为新执行的进程传入变量
docker run -d --name nginx-exec nginx
docker exec -it nginx-exec /bin/bash
# 使用 exit 或 Ctrl+D 退出新 Shell,主服务通常继续运行

docker exec -it nginx-exec sh
docker exec nginx-exec ls /etc/hosts
docker exec nginx-exec cat /etc/hosts

Docker 管理容器的 /etc/hosts,其中包含回环及容器主机名等映射;主机名默认常为短 ID,也可指定,不能理解为所有容器都固定用相同的 ID 映射规则。

ll 常是 Shell 别名,不能直接当作二进制执行:

OCI runtime exec failed: exec failed: unable to start container process: exec:
"ll": executable file not found in $PATH: unknown
docker exec nginx-exec ls -l

环境变量和引用

docker exec -e 'DEBUG1=true1' nginx-exec env
# 输出包含 DEBUG1=true1

# 未保护的 $DEBUG1 先由宿主机 Shell 展开,可能为空
docker exec -e 'DEBUG1=true1' nginx-exec echo $DEBUG1

# 交给容器内的 Shell 展开
docker exec -e 'DEBUG1=true1' nginx-exec bash -c 'echo "$DEBUG1"'
# true1

# 双引号也能使用,但必须保护美元符号
docker exec -e 'DEBUG1=true1' nginx-exec bash -c "echo \$DEBUG1"

-e 只影响此次 exec 进程,不永久修改容器启动配置。是否用单引号并非唯一条件,关键是避免宿主机提前展开变量。

4.6 LAMP:MySQL 与 WordPress

获取镜像

aliyun_url='registry.cn-hangzhou.aliyuncs.com/sswang-image'
for i in wordpress:php8.4-apache mysql:oraclelinux9; do
  docker pull "$aliyun_url/$i"
  docker tag "$aliyun_url/$i" "$i"
  docker rmi "$aliyun_url/$i"
done

配套 scripts/get_ali_images.sh 未包含在 PDF 中;保留其调用方式:

用法: get_ali_images.sh [选项] [镜像名称[:标签]]
从阿里云镜像仓库拉取Docker镜像并打标签
-h, --help   显示帮助
-l, --list   列出预定义镜像
-a, --all    处理全部预定义镜像
/bin/bash get_ali_images.sh wordpress:php8.4-apache
/bin/bash get_ali_images.sh mysql:oraclelinux9
/bin/bash get_ali_images.sh -a

MySQL

参考:https://hub.docker.com/_/mysql。

MySQL 官方镜像用法

docker run -p 3306:3306 \
  -e MYSQL_ROOT_PASSWORD=123456 \
  -e MYSQL_DATABASE=wordpress \
  -e MYSQL_USER=wordpress -e MYSQL_PASSWORD=123456 \
  --name mysql -d --restart=always mysql:oraclelinux9

docker inspect mysql -f '{{.NetworkSettings.Networks.bridge.IPAddress}}'
docker inspect mysql -f '{{range $name, $net := .NetworkSettings.Networks}}{{printf "%s %s\n" $name $net.IPAddress}}{{end}}'
docker inspect mysql | jq -r '.[0].NetworkSettings.Networks.bridge.IPAddress'

apt install -y mysql-client-core-8.0
mysql -uroot -h172.17.0.2 -p123456 -e 'select version();'

172.17.0.2 为原实验地址,实际使用检查结果。客户端包名随 Ubuntu 版本和软件源变化。MYSQL_ALLOW_EMPTY_PASSWORD=yes 可允许空 root 密码,TZ=Asia/Shanghai 设置时区;这些初始化变量主要在数据目录首次初始化时生效。

WordPress

参考:https://hub.docker.com/_/wordpress。

WordPress 官方镜像用法

docker run -d -p 80:80 --name wordpress \
  -e WORDPRESS_DB_HOST=10.0.0.13:3306 \
  -e WORDPRESS_DB_USER=wordpress \
  -e WORDPRESS_DB_PASSWORD=123456 \
  -e WORDPRESS_DB_NAME=wordpress \
  --restart=always wordpress:php8.4-apache

此例经宿主机 10.0.0.13:3306 连接已发布的数据库端口;要保证容器能够到达该地址。自定义 bridge 网络中也可使用数据库服务名直接访问,避免依赖固定容器 IP。

浏览器访问 http://10.0.0.13/,选择语言并填写站点信息、管理员账号和密码,再安装并登录。数据库尚未就绪或配置错误时,应先处理数据库连接问题。

WordPress 站点安装流程

WordPress 登录后的管理界面

通过变量文件部署

mkdir -p /data/server/lamp
cat > /data/server/lamp/mysql_env.list <<'ENVFILE'
MYSQL_ROOT_PASSWORD=123456
MYSQL_DATABASE=wordpress
MYSQL_USER=wordpress
MYSQL_PASSWORD=123456
TZ=Asia/Shanghai
ENVFILE

cat > /data/server/lamp/wordpress_env.list <<'ENVFILE'
WORDPRESS_DB_HOST=10.0.0.13:3306
WORDPRESS_DB_USER=wordpress
WORDPRESS_DB_PASSWORD=123456
WORDPRESS_DB_NAME=wordpress
TZ=Asia/Shanghai
ENVFILE

cd /data/server/lamp
docker run -p 3306:3306 --env-file=mysql_env.list \
  --name mysql -d --restart=always mysql:oraclelinux9
docker run -d -p 80:80 --env-file=wordpress_env.list \
  --name wordpress --restart=always wordpress:php8.4-apache
docker ps --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}'

变量文件的值中尾随空格可能成为密码等值的一部分,应逐行检查,不要把 Shell 的完整解析规则套到 --env-file。同名容器与同一宿主机端口不能重复使用;进行本组实验前清理上一组实验容器:

docker rm -f wordpress mysql

安装 WordPress 的步骤与上一节相同,实验后同样按具体名称清理。

4.7 管理别名

alias dps='docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Ports}}"'
alias dpas='docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Ports}}"'
alias dka='cs=$(docker ps -a -q); [ -z "$cs" ] || docker rm -f $cs'
alias dcn='docker inspect --format "{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}"'
alias dcc="docker inspect -f '{{join .Config.Entrypoint \" \"}} {{join .Config.Cmd \" \"}}'"
alias dims='docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"'

dcn 可能返回多个地址;后面的 curl $(dcn ...) 实验仅在容器只有一个相应网络地址时适用。多网络环境应按网络名称明确选择。

原稿引用 scripts/docker_define_my_cmd.sh,可复制至 /etc/profile.d/ 后重新登录;PDF 未给完整脚本。其帮助表如下:

命令 用途
dps 运行中容器精简列表
dpas 全部容器精简列表
dka 强制删除全部容器
dcn 容器 查询容器 IP
dcc 容器 查看 Entrypoint 与 Cmd
dims 镜像仓库、标签和大小
dcl 带 IP 的运行容器清单,需配套脚本实现
dhelp 打印帮助,需配套脚本实现
cp docker_define_my_cmd.sh /etc/profile.d/
dhelp
dcn mysql
dcc mysql

4.8 容器与宿主机文件复制

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

-a 主要保留源端 UID/GID 等归档属性;默认也尽可能保留权限,但不能保证复制所有扩展属性、ACL 和任意元信息。-L 跟随源软链接。容器无需处于运行中;不能直接在两个容器之间用一次 cp 完成复制。

以下把原稿清空整个 /tmp/* 的操作改为独立实验目录,复制语义不变:

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

# 复制 server 目录到宿主机
# 目标已存在时,这两种写法均复制为 /tmp/docker-cp-lab/server
docker cp nginx_cp:/data/server/ /tmp/docker-cp-lab/
docker cp nginx_cp:/data/server /tmp/docker-cp-lab

# 只复制目录内容时使用 /.
docker cp nginx_cp:/data/server/. /tmp/docker-cp-lab/

# 单个文件及重命名
docker cp nginx_cp:/etc/hosts /tmp/docker-cp-lab/
docker cp nginx_cp:/etc/hosts /tmp/docker-cp-lab/hosts-con

# 宿主机到容器
docker cp /etc/hosts nginx_cp:/tmp/
mkdir -p /tmp/docker-cp-lab/server/dir{1..3}
docker cp /tmp/docker-cp-lab/server nginx_cp:/tmp/

目标路径存在与否、是否为目录、源尾部是否为 /. 都会改变结果。大量持续数据同步应使用适合的数据卷、备份或同步工具。

4.9 清理未使用资源

正确命令为 docker system prune,不存在原稿标题中的 docker system purge。

docker system prune
docker system df
docker system prune -a

默认清理停止的容器、未被容器使用的网络、悬空镜像和未使用构建缓存;默认不清理卷。

选项 行为
-a, --all 将镜像范围扩大到所有未被容器引用的镜像
--volumes 同时清理未使用的匿名卷;具名卷请按所用版本的 volume prune 行为另行处理
-f, --force 跳过确认,不是杀死运行容器

原稿交互和结果:

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。正在运行容器及其引用的镜像不会被 system prune 直接删除。

4.10 export、import 与 commit

准备实验容器

# 当前目录下已有镜像归档时,逐个加载
for i in ./*.tar; do
  [ -f "$i" ] || continue
  docker load -i "$i"
done
/bin/bash get_ali_images.sh -a

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

原稿响应:

Nginx Web Page by sswang

export / import

export 导出容器根文件系统,不包含镜像历史、环境变量、默认启动命令和网络配置,也不导出挂载卷中的数据。导出路径上的卷会遮蔽底层目录时,实际导出的仍是容器底层文件系统内容。

docker export -o nginx_test.tar nginx_test
docker export nginx_test | gzip > nginx_test.tar.gz

docker import nginx_test.tar mynginx:v1
gunzip -c nginx_test.tar.gz | docker import - mynginx:v2
docker import [OPTIONS] FILE|URL|- [REPOSITORY[:TAG]]

import 建立新的扁平镜像,通常只有一个新文件系统层;原启动配置不保留,运行时需指定命令,或通过支持的 --change 选项补充镜像配置。

commit

docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
选项 用途
-a, --author 镜像作者元数据
-m, --message 提交说明
-p, --pause 提交期间暂停容器,默认开启
--change 修改支持的镜像配置
docker exec nginx_test bash -c "echo 'Nginx Web Page by sswang with commit' > /usr/share/nginx/html/index.html"
docker commit -m 'new image' nginx_test mynginx:v3
docker run -d --name mynginx mynginx:v3
dcn mynginx
curl 172.17.0.3

原稿示例:

172.17.0.3
Nginx Web Page by sswang with commit

commit 保存容器可写文件系统的变化及可用于镜像的配置,不包含内存状态、挂载卷内容或完整宿主机运行配置。端口发布、网络连接、绑定挂载等还需另行记录。

多次 commit 增加层,手工修改不易复现,敏感信息也可能进入镜像。生产交付通常优先使用 Dockerfile。

目的 方法
备份/迁移镜像及其镜像配置 save / load
导出/导入容器根文件系统 export / import
从已修改的容器生成镜像 commit
完整迁移应用运行环境 还需单独迁移卷数据、运行参数和外部依赖

4.11 项目实践:Spug

Spug 是轻量运维平台,提供主机管理、发布、任务、监控等能力。参考:https://www.spug.cc/、https://ops.spug.cc/docs/about-spug。

Spug 管理界面示例

其技术体系包括 Python/Django 后端、React/Ant Design 前端、MySQL/MariaDB 数据存储、Redis 等任务相关组件,以及 SSH 主机操作。原稿将前端写为 Vue.js + Element UI,已修正;不同 Spug 版本和镜像的内置组件以项目文档为准。

获取镜像

docker pull registry.cn-hangzhou.aliyuncs.com/openspug/spug-service
docker tag registry.cn-hangzhou.aliyuncs.com/openspug/spug-service spug-service
docker rmi registry.cn-hangzhou.aliyuncs.com/openspug/spug-service

docker pull registry.cn-hangzhou.aliyuncs.com/openspug/mariadb:10.8.2
docker tag registry.cn-hangzhou.aliyuncs.com/openspug/mariadb:10.8.2 mariadb:10.8.2
docker rmi registry.cn-hangzhou.aliyuncs.com/openspug/mariadb:10.8.2

数据库与服务

原稿使用默认 bridge 加旧式 link。下面统一 MYSQL_HOST=db 与 --link spug-db:db,避免别名不一致。

mkdir -p /data/spug/mysql
docker run -d \
  --name spug-db --restart always --network bridge \
  --mount type=bind,source=/data/spug/mysql,target=/var/lib/mysql \
  -e MYSQL_DATABASE=spug -e MYSQL_USER=spug \
  -e MYSQL_PASSWORD=123456 -e MYSQL_ROOT_PASSWORD=123456 \
  mariadb:10.8.2 \
  --port=3306 --character-set-server=utf8mb4 \
  --collation-server=utf8mb4_unicode_ci

mysql -uspug -h$(dcn spug-db) -p123456 -e 'show databases;'

mkdir -p /data/spug/{service,repos}
docker run -d \
  --name spug --restart always --network bridge --privileged=true \
  --mount type=bind,source=/data/spug/service,target=/data/spug \
  --mount type=bind,source=/data/spug/repos,target=/data/repos \
  -p 80:80 -e MYSQL_DATABASE=spug -e MYSQL_USER=spug \
  -e MYSQL_PASSWORD=123456 -e MYSQL_HOST=db \
  -e MYSQL_PORT=3306 --link spug-db:db \
  spug-service:latest

挂载用于持久化数据库、服务数据和仓库数据。原稿采用 privileged 参数,此为特定镜像部署示例,不代表普通应用容器都需要特权模式。长期部署可按对应镜像支持方式改用自定义 bridge 网络和服务名解析。

Docker 参数通常既支持 --name value,也支持 --name=value;不能把空格写法一概说成容易报错。镜像名之后的参数属于容器命令参数。

CONTAINER ID   NAMES     PORTS
fbcbad0d7c93   spug      0.0.0.0:80->80/tcp, [::]:80->80/tcp
85d383a05eaf   spug-db   3306/tcp

初始化和访问

等待数据库和服务真正就绪后初始化;原稿提示约等 10 秒,但该时间不是可靠的就绪条件。

docker logs spug
docker exec spug init_spug admin 123456

浏览器访问 http://10.0.0.13/,以实验账号 admin / 123456 登录。原稿登录后显示一个提示框,点击“知道了”进入管理界面。

Spug 登录与提示

Spug 管理首页

docker rm -f spug spug-db

移除容器不会自动删除上面的宿主机绑定目录。

5. Dockerfile

5.1 构建与分层

镜像提供应用所需的根文件系统和配置,普通 Linux 容器使用宿主机内核。隔离由运行时和内核机制实现,并非“镜像之间不能通信”;实际通信主体是容器进程。

镜像层与容器的关系

镜像生命周期包括构建、打标签、运行、存储/分发、更新和淘汰。

镜像构建、运行、保存与发布

制作镜像的主要方式:

docker commit <容器> <镜像:标签>
docker build -t <镜像:标签> .
docker export <容器> -o rootfs.tar
docker import rootfs.tar <镜像:标签>

Dockerfile 把环境制作过程写成可审核、可重复的构建定义,通常优于手工 commit。基础镜像是后续构建的起点,衍生镜像通过 FROM 继承文件系统与配置,并不要求使用 ONBUILD。基础镜像更新后,已有衍生镜像不会自动变化,必须重新构建并重新部署。

Dockerfile 指令不是都一一对应 Linux 命令:RUN 执行命令,COPY/ADD 改变文件系统,ENV/CMD 等配置镜像元数据。RUN 中执行 cd、export 不会自动改变后续 RUN 的 Shell 状态;需要使用 WORKDIR、ENV 或在同一个 RUN 中连接命令。

构建缓存依赖指令和输入文件等条件,变化可能使后续步骤失效。将稳定依赖放前、频繁变化的源代码放后,通常能提高缓存命中率。

镜像使用流程

docker build -t my-image:v1.0 .
docker tag my-image:v1.0 registry.example.com/team/my-image:v1.0
docker run -d --name my-container -p 8080:80 my-image:v1.0
docker stop my-container
docker start my-container
docker restart my-container

# 登录并确保具有目标仓库权限后才可推送
docker login registry.example.com
docker push registry.example.com/team/my-image:v1.0
docker pull registry.example.com/team/my-image:v1.0

docker save -o my-image.tar my-image:v1.0
docker load -i my-image.tar

镜像是只读模板,容器是运行实例。一个镜像可创建多个容器,各容器可写层相互独立。停止或重启同一个容器通常保留可写层;删除后重建容器才不保留该可写层。需要独立于容器生命周期保存的数据应使用卷或绑定挂载。

分层构建示意

图中 Debian 上依次安装 emacs 与 apache2,展示差异层叠加。内核仍由宿主机提供,不是镜像里的 bootfs/Kernel 层。RUN、COPY、ADD 等产生文件系统差异;LABEL、CMD、EXPOSE 等主要修改配置,不能说每条指令都生成独立文件系统层。

层可以共享和复用,也便于观察构建历史。仅仅减少指令数不保证缩小镜像:应在同一 RUN 中完成相关安装和缓存清理,避免在后续层删除已进入底层的大文件。

RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

业务镜像可按系统、业务和版本组织目录,前文“按基础环境与业务组织镜像”的图在原 PDF 第 65 页再次出现,此处不重复展示,原图仍保存在 images/image-047.png。

5.2 Dockerfile 文件与 build

默认文件名为 Dockerfile,不是必须把任意文件名首字母大写;其他名称通过 -f 指定。指令惯例使用大写,指令名本身不区分大小写;以 # 开头的独立行可作注释。多行指令可使用反斜杠续行。

常见文件名包括 Dockerfile.dev、Dockerfile.prod、Dockerfile.alpine、Dockerfile.arm64。指令按阶段顺序解释,构建器会按依赖关系和缓存执行构建;现代 BuildKit 与早期逐条创建临时容器的实现细节不同。

Docker 无内置功能从任意镜像恢复完整原始 Dockerfile。history 和第三方工具只能辅助推断,不能恢复所有上下文与构建逻辑。

参考:https://docs.docker.com/reference/dockerfile/。

docker build [OPTIONS] PATH|URL|-
选项 含义
-t, --tag 设置镜像名和标签,可重复;-t name 的标签为 latest,完全不传 -t 并不会自动得到名为 latest 的镜像
-f, --file 指定 Dockerfile 路径
--no-cache 不使用构建缓存
--pull 尝试获取更新的基础镜像
--build-arg 传递 ARG 参数
--rm 旧式构建器的中间容器清理选项,现代构建行为以实际构建器为准
--progress=plain 输出详细构建日志
docker build -t my-image:v1 .
docker build -t my-image:v1 -f /path/to/Dockerfile /path/to/context
docker build --build-arg APP_VERSION=1.2 -t my-image:v1 .

-f 指定文件不替代构建上下文参数,原稿缺少上下文的示例已补齐。

5.3 builder 缓存管理

docker builder prune
docker builder prune -a --force
docker builder prune --keep-storage=5GB
docker builder prune -f
docker system prune -a --volumes
选项 含义
-a, --all 清理所有未使用构建缓存,而不只悬空缓存
-f, --force 取消确认
--filter 按支持的条件过滤,例如 until=24h
--keep-storage 缓存清理的保留量策略;不是预留磁盘,也不是无条件保证至少存在这么多缓存

不同 builder 的过滤能力不同。type=exec.cachemount 等过滤通常见于 docker buildx prune,不能假定 docker builder prune 在每个版本都支持同一组条件。

删除构建缓存会使后续构建重做步骤,但不等于删除所有当前使用镜像的文件系统层。应根据磁盘压力清理,不必为整洁而在每次构建后清空缓存。

5.4 核心指令

FROM

FROM [--platform=<platform>] <image>[:<tag>|@<digest>] [AS <name>]

FROM 开始新构建阶段,可指定 linux/amd64、linux/arm64 等平台并命名阶段。FROM 前可以出现全局 ARG 和解析器指令,因此不能说 FROM 必须是第一条非注释指令。

FROM ubuntu:20.04
FROM nginx:latest
FROM python:3.9-slim

固定摘要时使用完整真实值:FROM ubuntu@sha256:<完整摘要>,省略号不是可执行摘要。

LABEL

镜像元数据采用键值对,可用于版本、用途、来源和维护信息;它与仓库 tag 是不同概念。

LABEL version="1.0"
LABEL author="John Doe" \
      description="This is a sample Docker image"
LABEL description="This image is used to run a web server."

可以在一条指令里设置多个标签,也可分开写;带空格的值需要正确引用。

RUN

RUN <command>
RUN ["executable", "param1", "param2"]

Shell 形式在 Linux 上通常使用 /bin/sh -c,支持管道、重定向和变量展开;exec 形式直接执行,不自动解释这些 Shell 语法。

用途包括安装依赖、配置文件、下载和解包、编译、生成静态文件等。

RUN apt-get update && apt-get install -y curl wget vim
RUN yum install -y curl wget
RUN wget https://example.com/file.tar.gz -O /tmp/file.tar.gz && \
    tar -xzf /tmp/file.tar.gz -C /opt && rm /tmp/file.tar.gz
RUN ["apt-get", "update"]

不要在构建步骤中使用依赖终端交互的 vim、top 等。关联操作应放同一 RUN 或通过 WORKDIR 传递目录设置:

RUN cd nginx-1.29.0 && ./configure

单独一条 RUN cd nginx-1.29.0 不会影响下一条 RUN。

EXPOSE

EXPOSE 声明预期监听端口及协议,不自动启动监听,也不自动发布宿主机端口。

EXPOSE 80
EXPOSE 80 443
EXPOSE 80/tcp
EXPOSE 53/udp

实际发布用 docker run -p 宿主机端口:容器端口。

CMD

CMD 定义默认命令,或在 exec 形式 ENTRYPOINT 存在时定义其默认参数。

CMD command param1 param2
CMD ["executable", "param1", "param2"]
CMD ["/bin/bash", "-c", "echo hello"]
CMD ["param1", "param2"]

同一阶段多个 CMD 只取最后一个。当前阶段显式设置的 CMD 与 ENTRYPOINT 可以前后排列;不能说“CMD 写在 ENTRYPOINT 前一定被覆盖”。设置 ENTRYPOINT 会重置从基础镜像继承的 CMD,这是另一种情况。

CMD ["echo", "Hello, Docker"]
CMD ["nginx", "-g", "daemon off;"]
CMD ["/apps/nginx/sbin/nginx", "-g", "daemon off;"]

ENTRYPOINT

ENTRYPOINT command param1 param2
ENTRYPOINT ["executable", "param1", "param2"]

推荐 exec 形式,让应用成为容器主进程,减少中间 Shell 对信号传递的影响。Shell 形式不是语法错误,但如果 Shell 不转发信号、也不 exec 应用,优雅停止可能受影响。

ENTRYPOINT ["nginx", "-g", "daemon off;"]

或将主命令与默认参数分离:

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

docker run 镜像 <参数...> 覆盖 CMD;--entrypoint 覆盖 ENTRYPOINT。参数需要分成正确 argv,不能把 "-s reload" 当成一个参数传给 Nginx。

5.5 核心指令练习:编译 Nginx

准备和 Dockerfile

mkdir -p /data/images/nginx
cd /data/images/nginx
FROM rockylinux:9
LABEL description="Nginx compiled on Rocky Linux 9"
RUN yum install -y gcc make pcre2-devel zlib-devel wget tar gzip
RUN wget https://nginx.org/download/nginx-1.29.0.tar.gz
RUN tar xf nginx-1.29.0.tar.gz
RUN cd nginx-1.29.0 && ./configure --prefix=/data/server/nginx
RUN cd nginx-1.29.0 && make && make install
RUN echo "Nginx Web Page from Rocky" > /data/server/nginx/html/index.html
RUN rm -rf nginx-1.29.0*
RUN yum autoremove -y && yum clean all && rm -rf /var/cache/yum
EXPOSE 80
ENTRYPOINT ["/data/server/nginx/sbin/nginx", "-g", "daemon off;"]

原稿安装了 PHP、GD、XML、XSLT、systemd 等开发包,但没有启用对应 Nginx 模块;部分包还依赖额外仓库。这里改为默认模块实际需要的编译依赖。若启用 SSL 等模块,应增加相应配置选项和依赖。此例保留多条 RUN 以观察分层,不代表推荐用它构建最小生产镜像;后续删除源码不能回收前面层已经占用的大小。

docker build -t nginx-test:v1 .
docker run --name nginx_test -d nginx-test:v1
dps
curl $(dcn nginx_test)
docker rm -f nginx_test

原稿记录构建约一分多钟,为其当时环境数据,实际时间不同。

分离 ENTRYPOINT / CMD

cp Dockerfile{,.bak}

将结尾改成:

ENTRYPOINT ["/data/server/nginx/sbin/nginx"]
CMD ["-g", "daemon off;"]
docker build -t nginx-test:v2 .
docker run --name nginx_test -d nginx-test:v2
dps
dcc nginx_test

# 覆盖默认参数:检查配置,是一次性任务
docker run --rm --name nginx_test1 nginx-test:v2 -t

# 让已运行的服务重新加载配置,应在原容器内执行
docker exec nginx_test /data/server/nginx/sbin/nginx -s reload

新建容器执行 -s reload 找不到原容器主进程,不能用它给另一容器的 Nginx 重载。原稿的 "-s reload" 单参数写法也不正确。

5.6 环境与文件指令

ENV

ENV 设置后续构建和容器运行环境变量。推荐 key=value 形式;历史 ENV key value 形式不便表达多个变量。

ENV APP_PORT=8080 DB_HOST=localhost
ENV APP_ENV=production
CMD ["node", "app.js"]

下载所需版本:

ENV NODE_VERSION=16.14.2
RUN curl -fsSL "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz" \
    | tar -xz -C /usr/local/

此例固定 x64,其他 CPU 平台需要改用匹配构建产物。示例版本用于说明变量,不代表推荐新项目继续使用已过支持期的运行时。

ARG DB_HOST
ENV DB_HOST=$DB_HOST

由 ARG 赋给 ENV 后,该值才会保留为运行时环境。不要用 ARG/ENV 存储构建密钥或密码,镜像元数据和历史可能暴露这些值。

COPY

COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>", "<目标路径>"]

源来自构建上下文,源路径以 / 开头也不是任意读取宿主机根目录。目标可为绝对路径或相对 WORKDIR 的路径,不存在时会按规则创建。

COPY app.js /app/
COPY src/ /app/src/
COPY *.txt /app/config/
COPY --chown=www-data:www-data html/ /var/www/html/
COPY file.txt /app/new.txt

目标尾部 / 明确表示目录。没有尾部 / 时还要看目标是否已存在且为目录,不能一概断定是重命名。多个源要求目标为目录。

分开复制依赖清单和频繁变化的代码可利用缓存;只复制必要文件。

ADD

ADD [--chown=<user>:<group>] <源路径>... <目标路径>
ADD [--chown=<user>:<group>] ["<源路径1>", "<目标路径>"]

ADD 支持上下文文件、HTTP(S) URL,以及新版支持的 Git 来源等。对可识别的本地 tar 归档可自动解压,包括其 gzip/bzip2/xz 压缩形式;不能说任意 gzip 文件或 zip 都会自动解压。通过远程 URL 下载的归档默认也不能等同本地 tar 自动解压。

ADD app.js /app/
ADD src/ /app/src/
ADD https://example.com/file.txt /app/data/
ADD archive.tar.gz /app/
ADD --chown=www-data:www-data html/ /var/www/html/

普通复制优先 COPY;本地 tar 解包可用 ADD;下载需要校验和解包控制时可用 RUN 配合 curl/wget,并验证来源。

WORKDIR

WORKDIR 为后续 RUN、CMD、ENTRYPOINT、COPY、ADD 设置工作目录,不存在会创建。相对目录基于上一个 WORKDIR。

WORKDIR /app
RUN echo "Hello from $(pwd)"
COPY . .
WORKDIR /tmp
RUN touch test.txt
WORKDIR /app
RUN cp /tmp/test.txt .

原稿在同一阶段用 COPY --from=0 复制自己的文件,会产生循环阶段依赖;上例改为同阶段 RUN cp。跨阶段 COPY 见多阶段构建章节。

WORKDIR /app
WORKDIR src
# 当前工作目录为 /app/src
WORKDIR /app
CMD ["node", "server.js"]
对比 WORKDIR RUN cd
生效范围 后续相关指令及运行默认目录 当前 RUN 的 Shell
持久性 写入镜像配置 Shell 结束即失效
示例 WORKDIR /app 后 RUN pwd 输出 /app RUN cd /app && pwd 不改变下一条 RUN 的目录

优先清楚的绝对路径,避免不必要的多次切换。

USER

USER <用户名或UID>[:<组名或GID>]

USER 指定后续 RUN 和默认运行命令的用户。非 root 运行是有实际价值的最小权限措施,容器独立空间不意味着 root 权限无风险。

RUN useradd -m appuser
WORKDIR /app
RUN chown appuser:appuser /app
USER appuser
RUN touch /app/file
RUN echo "Hello" > /app/hello.txt
CMD ["node", "server.js"]

示例需要提供实际的 server.js。名称形式要求镜像内存在对应用户/组;数字 UID 不要求一定有 passwd 项,但应用可能依赖用户信息。确保工作目录、日志、PID 文件等可写,docker run -u 可以覆盖默认用户。Alpine 通常使用 adduser/addgroup。

绑定低端口能否成功取决于目标 network namespace 的 net.ipv4.ip_unprivileged_port_start 和进程能力,不是 host 网络必然不能使用普通用户。可使用高端口,或按需要配置 CAP_NET_BIND_SERVICE;不应因此一概放弃 USER。

ARG

ARG <参数名>[=<默认值>]

ARG 用于构建时参数,可通过 --build-arg 覆盖;不自动进入运行时环境。作用域受阶段和声明位置影响。FROM 前的全局 ARG 可用于 FROM;在阶段内部使用该参数通常还需重新声明。

ARG APP_ENV=development
RUN echo "Building for $APP_ENV"

该片段放在包含 FROM 的完整 Dockerfile 阶段内:

docker build --build-arg APP_ENV=production -t myapp .
ARG NODE_VERSION=16
ENV NODE_VERSION=$NODE_VERSION
LABEL runtime.node=$NODE_VERSION

ARG 本身不会执行 $(date ...)。构建日期应由调用端传入:

ARG BUILD_DATE
LABEL build.date=$BUILD_DATE
docker build --build-arg BUILD_DATE="$(date +%Y-%m-%d)" -t myapp .

仅在需要的位置之前声明 ARG 可减少无关缓存失效;具体缓存依赖由构建器决定。

5.7 环境指令综合练习

准备

mkdir -p /data/images/nginx1/{softs,conf}
cd /data/images/nginx1/softs
wget https://nginx.org/download/nginx-1.29.0.tar.gz
wget https://nginx.org/download/nginx-1.28.0.tar.gz
cd ../conf
echo 'Nginx VERSION Web Page from Rocky' > index.html
cd ..

本例改用容器内 8080,避免非 root 绑定低端口的配置依赖。

FROM rockylinux:9
LABEL description="Versioned Nginx on Rocky Linux 9"
ENV NGINX_PREFIX=/data/server/nginx \
    BUILD_DIR=/tmp/build \
    NGINX_USER=nginx \
    NGINX_GROUP=nginx
RUN groupadd -r "$NGINX_GROUP" && \
    useradd -r -g "$NGINX_GROUP" "$NGINX_USER"
WORKDIR $BUILD_DIR
RUN yum install -y gcc make pcre2-devel zlib-devel wget tar gzip && \
    yum clean all && rm -rf /var/cache/yum
ARG NGINX_VERSION=1.29.0
ADD softs/nginx-${NGINX_VERSION}.tar.gz .
WORKDIR nginx-${NGINX_VERSION}
RUN ./configure --prefix="$NGINX_PREFIX" \
      --user="$NGINX_USER" --group="$NGINX_GROUP" && \
    make && make install
WORKDIR $NGINX_PREFIX
COPY conf/index.html html/
RUN sed -i "s/VERSION/$NGINX_VERSION/" html/index.html && \
    sed -i -E 's/(listen[[:space:]]+)80;/\18080;/' conf/nginx.conf && \
    chown -R "$NGINX_USER:$NGINX_GROUP" "$NGINX_PREFIX"
RUN yum autoremove -y && rm -rf /tmp/build
USER $NGINX_USER
EXPOSE 8080
ENTRYPOINT ["sbin/nginx", "-g", "daemon off;"]

上面的 sed 匹配默认源码配置中的 listen 行并保留原缩进;若使用其他配置,直接修改实际 listen 指令并检查 nginx -t。

docker build -t rocky_nginx:v1.29.0 .
docker build --build-arg NGINX_VERSION=1.28.0 -t rocky_nginx:v1.28.0 .
docker run --name nginx-1.29 -d rocky_nginx:v1.29.0
docker run --name nginx-1.28 -d rocky_nginx:v1.28.0
dps
curl "$(dcn nginx-1.29):8080"
curl "$(dcn nginx-1.28):8080"

原稿记录构建不到两分钟。修正后首页应分别含 1.29.0 与 1.28.0,这里是按配置推导的预期,未进行实机构建。

收尾:

dka
docker builder prune -a --force
docker images --filter dangling=true
docker image prune -f

原稿悬空镜像示例:

<none>   <none>   8d4884557405   46 hours ago   563MB
<none>   <none>   81a1bc8dd2f6   2 days ago     192MB

5.8 其他指令

VOLUME

VOLUME /path/to/volume
VOLUME ["/path1", "/path2"]

VOLUME 声明挂载点;运行时未显式提供挂载时,Docker 通常创建匿名卷。数据是否能在容器删除后保留,还与 --rm、rm -v 和卷清理有关;VOLUME 本身不自动完成多容器共享配置或备份。

挂载会影响容器看到的内容与权限。宿主机绑定目录的所有者由宿主机决定,镜像中的 chown 不能保证覆盖它。应按运行 UID/GID 准备目录,避免为修复权限而给予应用任意 sudo 权限。

ONBUILD

ONBUILD <Dockerfile 指令>

父镜像记录触发器,在子镜像 FROM 继承时执行,不在构建父镜像时执行。适合统一依赖安装、代码复制等流程;最好在镜像名或标签中明确标记 onbuild,减少意外触发。

父镜像示例:

FROM node:18
WORKDIR /app
ONBUILD COPY package*.json ./
ONBUILD RUN npm install
ONBUILD COPY . .
CMD ["node", "index.js"]

子镜像示例:

FROM my-node-base
ENV NODE_ENV=production
CMD ["npm", "start"]

子镜像的构建上下文必须提供被触发器引用的文件。ONBUILD 不允许再嵌套 ONBUILD,也不能任意触发 FROM 等指令。此例不是多阶段构建;继承与多阶段是不同概念。

原稿为解决非 root 权限给用户配置 NOPASSWD:ALL。它会使该用户可重新获得任意 root 权限,抵消最小权限目标。下面通过构建时 root 设置权限、运行时非 root 启动来修正,保留 ONBUILD 的教学功能。

HEALTHCHECK 与 STOPSIGNAL

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD curl -f http://localhost:8080/ || exit 1
STOPSIGNAL SIGQUIT

健康检查命令所需工具必须存在;失败会改变健康状态,并不自动使普通 Docker 容器重启。STOPSIGNAL 指定停止容器时发给主进程的信号,Nginx 的 SIGQUIT 用于优雅退出。

5.9 VOLUME / ONBUILD 综合练习

配置和启动脚本

cd /data/images
cp -a nginx1 nginx2
cd nginx2
mkdir -p scripts

conf/nginx.conf:

worker_processes 1;
events {
    worker_connections 1024;
}
http {
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;
    server {
        listen 8080;
        server_name localhost;
        location / {
            root html;
            index index.html index.htm;
        }
        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root html;
        }
    }
    include conf.d/*.conf;
}

conf/extra.conf:

log_format custom_format '$remote_addr - $remote_user [$time_local] '
                         '"$request" $status $body_bytes_sent '
                         '"$http_referer" "$http_user_agent" '
                         '"$http_x_forwarded_for"';
access_log /data/server/nginx/logs/access_custom.log custom_format;
server {
    listen 8081;
    server_name localhost;
    location /test {
        return 200 "Hello from extra.conf!\n";
    }
}

scripts/start-nginx.sh:

#!/bin/sh
set -eu
exec "$NGINX_PREFIX/sbin/nginx" -g 'daemon off;'
chmod +x scripts/start-nginx.sh

原稿在启动脚本中 sudo chown 日志目录,改为构建时设置权限。若将日志目录另外绑定到宿主机,应在宿主机设置对应 UID/GID;本例声明的卷是 conf/conf.d,不是日志目录。

基础镜像 Dockerfile

FROM rockylinux:9
LABEL description="Nginx ONBUILD base on Rocky Linux 9"
ENV NGINX_PREFIX=/data/server/nginx \
    BUILD_DIR=/tmp/build \
    NGINX_USER=nginx \
    NGINX_GROUP=nginx
RUN groupadd -r "$NGINX_GROUP" && \
    useradd -r -g "$NGINX_GROUP" "$NGINX_USER"
WORKDIR $BUILD_DIR
RUN yum install -y gcc make pcre2-devel zlib-devel wget tar gzip curl && \
    yum clean all && rm -rf /var/cache/yum
ARG NGINX_VERSION=1.29.0
ADD softs/nginx-${NGINX_VERSION}.tar.gz .
WORKDIR nginx-${NGINX_VERSION}
RUN ./configure --prefix="$NGINX_PREFIX" \
      --user="$NGINX_USER" --group="$NGINX_GROUP" && \
    make && make install
WORKDIR $NGINX_PREFIX
COPY conf/index.html html/
COPY conf/nginx.conf conf/
COPY scripts/start-nginx.sh sbin/
RUN mkdir -p conf/conf.d && \
    sed -i "s/VERSION/$NGINX_VERSION/" html/index.html && \
    chmod +x sbin/start-nginx.sh && \
    chown -R "$NGINX_USER:$NGINX_GROUP" "$NGINX_PREFIX" && \
    rm -rf /tmp/build
EXPOSE 8080 8081
VOLUME ["/data/server/nginx/conf/conf.d"]
ONBUILD USER root
ONBUILD COPY conf/extra.conf /data/server/nginx/conf/conf.d/extra.conf
ONBUILD RUN chown -R "$NGINX_USER:$NGINX_GROUP" "$NGINX_PREFIX"
ONBUILD USER nginx
ONBUILD RUN /data/server/nginx/sbin/nginx -t
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD curl -f http://localhost:8080/ || exit 1
STOPSIGNAL SIGQUIT
USER nginx
ENTRYPOINT ["sbin/start-nginx.sh"]

此例要求 BuildKit。旧式构建器对 VOLUME 声明之后写入该路径的处理不同,可能丢弃后续写入;不能不加限定地把 ONBUILD 写卷路径当成所有构建器通用方案。若需兼容旧构建器,应将 VOLUME 声明移到最终子镜像中,并在配置复制完成后声明。

Dockerfile.derived:

FROM nginx-base

FROM 会触发父镜像记录的命令,不需重复手工写 COPY。

构建与验证

docker build -t nginx-base .
docker build -f Dockerfile.derived -t nginx-derived .
docker run -d -p 80:8080 -p 8080:8081 --name nginx-test nginx-derived
docker inspect nginx-test --format '{{json .Mounts}}'
docker ps
docker inspect nginx-test --format '{{json .State.Health}}'
curl localhost
curl localhost:8080
curl localhost:8080/test

修正后的预期:主站可通过宿主机 80 访问,额外站点 /test 通过宿主机 8080 返回 Hello from extra.conf!;额外站点根路径没有专门配置,不能保证返回同样内容。healthy 表示最近健康检查成功,不表示业务各方面均正常。

原稿成功状态示例:

STATUS                         PORTS                      NAMES
Up About a minute (healthy)    0.0.0.0:80->80/tcp, ...     nginx-test

原稿故意破坏日志权限后的失败示例:

CREATED         STATUS                       PORTS   NAMES
6 seconds ago   Exited (1) 6 seconds ago               nginx-test
nginx: [alert] could not open error log file: open()
"/data/server/nginx/logs/error.log" failed (13: Permission denied)
[emerg] 1#0: open() "/data/server/nginx/logs/access_custom.log"
failed (13: Permission denied)

该输出是原始实验数据,不是本次整理后重新运行的结果。

5.10 .dockerignore

.dockerignore 从构建上下文排除不需要的内容,减少传输、避免不相关缓存失效,防止凭证误入镜像。被排除文件通常不能再由 COPY/ADD 使用。

规则使用单独注释行、*、?、** 和 ! 反向包含;行尾 # 注释 不能当成通用内联注释语法。后出现的匹配规则通常决定最终结果。

# 版本控制
.git
.gitignore

# 依赖与构建产物
node_modules
dist
build
*.log

# 环境与编辑器
.env
.ENV
.idea
.vscode

# 如前面有规则排除了该文件,可重新包含
!package.json

只排除不必要内容;项目依赖缓存是否应该重新安装取决于构建方案,不能把所有目录一律排掉。Dockerfile 和 ignore 文件即使被排除,构建器仍可能为完成构建而读取,但不能据此假定 COPY 能复制它们。

可以用构建日志、容器内文件检查和镜像分析工具辅助验证;history 不能单独证明所有文件已经正确排除。

专属忽略文件实验

本例继续使用 nginx2。基础镜像仍需要 softs/ 下源码,不能像原稿那样把整个 softs/ 排除。修正后的 .dockerignore:

.dockerignore
Dockerfile
Dockerfile.derived
tmp/
build/
*.log
*.tmp
.cache/
logs/
*.key
*.pem
.env
passwords.txt
.git/
.gitignore
.idea/
.vscode/
**/*.md
**/*.txt
**/*.log
mkdir -p files
touch files/file-{1..5}.txt
touch files/file-{1..5}.log
touch files/file-{1..5}.md
touch files/file-{1..5}.png

在基础镜像 Dockerfile 中、切换 USER 之前添加:

COPY files files
docker build --progress=plain -t nginx-test .
docker run -d --name nginx-ignore-test nginx-test
docker exec nginx-ignore-test ls files

原稿构建日志节选:

#18 [14/14] COPY files files
#18 DONE 0.0s

构建日志不逐个显示 ignore 的过滤结果。修正后预期只看到五个 PNG 文件;TXT、LOG、MD 被排除。若原有同名容器占用名称,先清理相应实验容器。

旧式构建器可用“运行中间容器、提交中间结果”理解构建;现代 BuildKit 不必逐条生成可见中间容器,排错应结合其日志、阶段和缓存机制。

收尾

dka
docker image prune -f
docker builder prune -a --force

原稿的 drmn 未提供定义,已用明确的镜像清理命令代替。

6. Dockerfile 业务案例

6.1 目录规划与 Alpine 基础镜像

按系统、业务和版本划分构建上下文,便于维护。

# 仅当备份目标不存在时使用该示例
mv /data/images /data/images-bak
mkdir -p /data/images/{web/{nginx,apache,tomcat,jdk},system/{centos,ubuntu,alpine,debian}}

# 明确拉取指定版本,不能给任意 latest 改标签就认为它是 3.22.0
docker pull alpine:3.22.0
docker history alpine:3.22.0
cd /data/images/system/alpine

原稿把 alpine:latest 直接重新标记为 3.22.0,标签并不会改变实际发行版内容,已改为拉取明确版本。

repositories:

https://mirrors.aliyun.com/alpine/v3.22/main
https://mirrors.aliyun.com/alpine/v3.22/community

Dockerfile:

ARG VERSION=3.22.0
FROM alpine:$VERSION
LABEL description="Alpine Linux 3.22 common build environment"
COPY repositories /etc/apk/repositories
RUN apk update && apk add --no-cache \
    tzdata gcc make curl zip unzip net-tools psmisc wget \
    libgcc musl-dev pcre2-dev zlib-dev iproute2 tar gzip && \
    ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo 'Asia/Shanghai' > /etc/timezone
docker build -t alpine-base:3.22 .

原稿记录约 30 秒,实际取决于软件源和缓存。原稿还列了 libcurl、libc-utils、pcre-dev/pcre、libnfs、libevent/libevent-dev 等通用库,但后续默认 Nginx 构建没有使用它们;这里使用 musl-dev、pcre2-dev 及实际工具集,pstree 由 psmisc 提供。需要额外应用库时再按对应 Alpine 分支确认包名。这个基础镜像包含编译工具,适合实验复用,并不是最小运行时镜像。更换 Alpine 小版本时,基础镜像与 repositories 的版本分支应同步调整。

6.2 基于 Alpine 编译 Nginx

准备文件

mkdir -p /data/images/web/nginx/1.29.0/{softs,html,conf}
cd /data/images/web/nginx/1.29.0/softs
wget https://nginx.org/download/nginx-1.29.0.tar.gz
cd ../html
echo 'Test Page based nginx-alpine' > index.html
cd ../conf

conf/nginx.conf:

worker_processes 1;
daemon off;
events {
    worker_connections 1024;
}
http {
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;
    server {
        listen 80;
        server_name localhost;
        location / {
            root html;
            index index.html index.htm;
        }
    }
}

Dockerfile

ARG VERSION=3.22
FROM alpine-base:$VERSION
ARG NGINX_VERSION=1.29.0
LABEL description="Nginx compiled on Alpine Linux"
ADD softs/nginx-${NGINX_VERSION}.tar.gz /usr/local/src/
ENV NGINX_DIR=/data/server/nginx
RUN cd "/usr/local/src/nginx-$NGINX_VERSION" && \
    ./configure --prefix="$NGINX_DIR" --user=nginx --group=nginx && \
    make && make install && \
    ln -s "$NGINX_DIR/sbin/nginx" /usr/sbin/nginx && \
    addgroup -g 888 -S nginx && \
    adduser -s /sbin/nologin -S -D -u 888 -G nginx nginx
COPY conf/nginx.conf ${NGINX_DIR}/conf/nginx.conf
COPY html/index.html ${NGINX_DIR}/html/index.html
RUN chown -R nginx:nginx "$NGINX_DIR"
EXPOSE 80
CMD ["nginx"]

本例保留 root master 启动,并通过编译默认用户 nginx 运行 worker。单纯创建 nginx 用户和 chown 不会自动切换运行身份。若改为整个进程非 root,应同时调整端口和可写路径。

原稿声明 443 但未配置 TLS 或监听 443,此处只声明实际使用的 80;需要 HTTPS 时再配置证书、监听和发布端口。

cd /data/images/web/nginx/1.29.0
docker build -t nginx-alpine:1.29.0 .
docker run -d -p 80:80 nginx-alpine:1.29.0
curl localhost

按配置预期返回 Test Page based nginx-alpine。

6.3 定制 JDK 镜像

mkdir -p /data/images/app/{java,golang,python}
cd /data/images/app/java

Dockerfile:

FROM alpine-base:3.22
LABEL description="Alpine Linux 3.22 with OpenJDK 17"
ARG JDK_PACKAGE=openjdk17
RUN apk update && \
    apk add --no-cache "$JDK_PACKAGE" tzdata && \
    rm -rf /var/cache/apk/* && \
    ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo 'Asia/Shanghai' > /etc/timezone
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk
ENV PATH=${JAVA_HOME}/bin:${PATH}
RUN java -version && javac -version
CMD ["sh"]

原稿声明 ARG JDK_VERSION=17.0.11_p9-r0,但安装时没有引用,实际上不能固定版本。这里改为被真正使用的包名参数;要精确固定 APK 版本,需要确认对应仓库存在该版本,并在包参数中使用 包名=版本。

docker build -t jdk-alpine:17 .
docker run --rm jdk-alpine:17 java -version

6.4 Java 应用镜像

编译应用

mkdir -p /data/images/web/halo/jar
cd /data/images/web/halo
# 将实际配套 HelloWorld.java 放入 jar/,不要用空文件替代
docker run -it --name java_test -v ./jar:/jar jdk-alpine:17 sh

原稿引用 scripts/HelloWorld.java,PDF 未给源码,本文不补造实现。能否监听 8000 以及是否产生内部类均由该实际源码决定。

容器内操作:

cd /jar
javac HelloWorld.java
printf 'Main-Class: HelloWorld\n\n' > MANIFEST.MF
jar cfm hello-world.jar MANIFEST.MF *.class
ls -l hello-world.jar
jar tf hello-world.jar
exit

原稿结果:

-rw-r--r-- 1 root root 1192 Jul 22 14:39 hello-world.jar
META-INF/
META-INF/MANIFEST.MF
HelloWorld$1.class
HelloWorld.class

原配套程序生成了 HelloWorld$1.class,打包应包含它;其他写法的源码不一定生成该文件。使用 *.class 会包含本次目录中的全部类文件,应避免混入无关旧类。

应用 Dockerfile

FROM jdk-alpine:17
WORKDIR /app
COPY jar/hello-world.jar /app/hello-world.jar
EXPOSE 8000
ENTRYPOINT ["java", "-jar", "hello-world.jar"]
docker build -t hello-world-app:1.0 .
docker run -d -p 8000:8000 --name hello-app hello-world-app:1.0
curl localhost:8000
docker logs hello-app

该应用须在容器中监听 0.0.0.0:8000 或相应可达地址,单写 EXPOSE 不会让 Java 程序变成 HTTP 服务。

收尾:

dka
docker image prune -f
docker builder prune -a --force

6.5 多阶段构建基础

在一个 Dockerfile 中使用多个 FROM,将编译工具、源码和构建过程留在 builder 阶段,只把运行产物及所需依赖复制到最终阶段。

主要作用:

  • 减少最终镜像中的构建工具与临时文件。
  • 将构建过程纳入一致环境,减少本地工具链差异。
  • 减少不必要组件带来的攻击面。

实际缩小比例由基础镜像和复制范围决定,不能保证每个项目都减少 50% 以上。使用同样臃肿的基础镜像作为运行阶段会限制收益。

关键语法:

FROM <构建镜像> AS builder
# 编译、打包

FROM <运行镜像>
COPY --from=builder /绝对产物路径 /运行目录/

COPY --from 的源路径基于被引用阶段/镜像的根文件系统解析,使用绝对路径最清楚。不要依赖源阶段 WORKDIR;变量替换支持取决于指令位置与构建器语义,使用明确路径可避免歧义。

docker build --target builder -t app-builder .

默认输出最后一个阶段,--target 可选择命名阶段。常见用途包括 Java、Go、C++ 编译,或 Node 构建前端后用 Nginx 提供静态文件。

Maven 示例

FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml ./
COPY src/ ./src/
RUN mvn package -DskipTests

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8000
ENTRYPOINT ["java", "-jar", "app.jar"]

此处拆开复制 pom.xml 与 src,修正原稿 COPY pom.xml src/ ./ 将源目录内容放错位置的问题。构建结果必须恰好有一个匹配的可运行 JAR;多个 JAR 时应改成确定产物名。示例跳过测试仅为原稿构建演示,实际交付按项目要求运行测试。

原稿使用的 openjdk:17-jre-alpine 不应假定存在,改用有明确 Java 17 JRE 定位的 Temurin 镜像系列;具体 tag、架构及获取权限仍需实际核实。

6.6 Java 多阶段实验

准备

cd /data/images/web
cp -a halo halo2
cd halo2
rm -f jar/*.class jar/hello-world.jar
ls jar

原稿剩余文件:

HelloWorld.java   MANIFEST.MF

Dockerfile:

FROM jdk-alpine:17 AS builder
WORKDIR /build
COPY jar/HelloWorld.java .
RUN javac HelloWorld.java
RUN printf 'Main-Class: HelloWorld\n\n' > MANIFEST.MF
RUN jar cfm hello-world.jar MANIFEST.MF *.class

FROM jdk-alpine:17
WORKDIR /app
COPY --from=builder /build/hello-world.jar .
EXPOSE 8000
ENTRYPOINT ["java", "-jar", "hello-world.jar"]
docker build -t hello-world-app:multi-stage .

该示例把源码、class 文件和构建步骤隔离在 builder;最终阶段仍用完整 jdk-alpine:17,所以它说明的是阶段隔离,不代表已做到最小运行镜像。若要进一步缩小,可使用兼容的 JRE 或经验证的定制运行时。

6.7 Python:单阶段与多阶段比较

单阶段

mkdir -p /data/image/case/python
cd /data/image/case/python

Dockerfile:

FROM alpine-base:3.22
ENV PYTHON_VERSION=3.11.0 \
    PYTHON_HOME=/usr/local
RUN apk add --no-cache \
    gcc g++ make musl-dev libffi-dev openssl-dev zlib-dev bzip2-dev \
    readline-dev sqlite-dev wget tar xz && \
    cd /tmp && \
    wget "https://mirrors.aliyun.com/python-release/source/Python-${PYTHON_VERSION}.tgz" && \
    tar -xzf "Python-${PYTHON_VERSION}.tgz" && \
    cd "Python-${PYTHON_VERSION}" && \
    ./configure --enable-optimizations --with-ensurepip=install && \
    make -j"$(nproc)" && \
    make install && \
    ln -sf "${PYTHON_HOME}/bin/python3.11" /usr/bin/python3 && \
    cd / && \
    rm -rf /var/cache/apk/* \
      "/tmp/Python-${PYTHON_VERSION}" \
      "/tmp/Python-${PYTHON_VERSION}.tgz" && \
    python3 --version

已将清理路径改为绝对路径,修正原稿在源码目录内删除同名相对路径导致未清理源码的问题。这个例子仍保留编译依赖,因此镜像较大。Python 3.11.0 是原稿对比版本,不代表建议继续采用最初发布版本进行新部署。

docker build -t python-alpine:3.11-single .
docker images python-alpine

原稿构建日志:

[+] Building 359.2s (6/6) FINISHED

多阶段

使用独立目录保留比较结果:

mkdir -p /data/image/case/python2
cd /data/image/case/python2

Dockerfile:

FROM alpine-base:3.22 AS builder
RUN apk add --no-cache \
    gcc g++ make musl-dev libffi-dev openssl-dev zlib-dev bzip2-dev \
    readline-dev sqlite-dev wget tar xz
RUN cd /tmp && \
    wget -q 'https://mirrors.aliyun.com/python-release/source/Python-3.11.0.tgz' && \
    tar -xzf Python-3.11.0.tgz && \
    cd Python-3.11.0 && \
    ./configure --enable-optimizations --with-ensurepip=install && \
    make -j"$(nproc)" && \
    make install

FROM alpine-base:3.22
RUN apk add --no-cache \
    libffi openssl zlib bzip2 readline sqlite-libs
COPY --from=builder /usr/local/ /usr/local/
RUN ln -sf /usr/local/bin/python3.11 /usr/bin/python3 && \
    python3 --version && \
    python3 -m pip --version

这里复制整个 /usr/local/ 安装前缀,包含解释器、标准库、扩展模块、pip 启动脚本等;修正原稿“只复制四条路径,却声称已使 pip3 全局可用”的遗漏。若进一步裁减目录,应先验证实际应用导入及依赖,不能只看 python3 --version。

运行库必须覆盖所构建扩展模块需要的动态库;依赖由实际 Python 配置决定。上述模板延续原稿使用重型 alpine-base 的比较环境,不是生产最小化方案。

# 原稿为了比较执行过缓存清理,实际不必每次构建都清空缓存
docker builder prune -a --force
docker build -t python-alpine:3.11-multi .
docker images python-alpine
docker run --rm python-alpine:3.11-multi python3 --version
docker run --rm python-alpine:3.11-multi python3 -m pip --version

原稿多阶段构建记录(原稿仍使用相同 3.11 标签,本文改为 single/multi 方便并存比较):

[+] Building 335.7s (13/13) FINISHED
REPOSITORY      TAG    IMAGE ID       CREATED              SIZE
python-alpine   3.11   5cefcdd95937   About a minute ago   520MB
Python 3.11.0

原稿称单阶段与多阶段容量差约 500 MB,但没有在末尾同时给出单阶段大小。该数值是原始环境观察,不能当作本文修正模板重新构建后的保证。实际比较时应同时查看两个不同标签、基础镜像、平台和共享层占用。

评论