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


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

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

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

早期 Docker 使用 LXC,后来采用 libcontainer,相关底层实现发展为 runc。这个变化与 KVM 是否作为内核模块实现没有因果关系。Docker Engine 使用客户端/服务器架构,通过 API 管理镜像、容器、网络和卷,核心理念是 Build、Ship、Run。
Docker Engine 的开源组件通常采用 Apache 2.0 等开源许可;Docker Desktop 是独立产品,其订阅和使用条件应单独查看。Linux 容器共享宿主机内核,通过 namespaces 隔离资源视图、通过 cgroups 管理资源。容器通常比完整虚拟机轻,但仍有存储、网络和运行时开销,不能把虚拟机开销统一写成固定百分比,也不能说容器没有开销。
参考:
- Docker:https://www.docker.com/
- 文档:https://docs.docker.com/
- Docker Hub:https://hub.docker.com/
- 中文资料站:http://www.docker.org.cn
Docker 管理应用的封装、分发、部署和运行,方便开发、测试与生产复用环境。镜像仍受操作系统、CPU 架构、内核能力和外部依赖约束;跨平台运行需要适配平台镜像或额外的虚拟化/模拟支持。

1.3 容器与虚拟机
虚拟机虚拟硬件,并运行独立的客户机内核和操作系统。普通 Linux 容器中的应用直接使用宿主机 Linux 内核,不为每个容器再启动一套内核。
容器可以有隔离的根文件系统、进程编号、网络设备、主机名和 IPC 资源。隔离配置与运行参数有关:共享网络、共享 PID、特权模式等会改变边界。共享内核也意味着容器不能被视为与虚拟机完全等价的安全隔离。

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


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

检查内核配置时使用实际内核版本:
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,不只有两项。

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

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


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 的改名。

参考:
- https://docs.docker.com/engine/install/
- https://docs.docker.com/desktop/setup/install/linux/
- https://github.com/moby/moby
- https://docs.docker.com/retired/
- https://www.mirantis.com/
- https://docs.docker.com/subscription/details/
- https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-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 服务文件、进程和数据目录冲突。原稿建议恢复干净快照,这属于实验环境准备,不是安装方式的绝对要求。
参考:
- https://docs.docker.com/engine/install/binaries/
- https://download.docker.com/linux/
- https://mirrors.aliyun.com/docker-ce/linux/static/stable/x86_64/
压缩包命名形如 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 命令环境

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 输入查询站点,按页面提供的替代仓库命令获取镜像。




原稿直接拉取时的超时示例:
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、服务调整或上游访问问题失效,页面给出的命令也需要核对。

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。

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。

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/,选择语言并填写站点信息、管理员账号和密码,再安装并登录。数据库尚未就绪或配置错误时,应先处理数据库连接问题。


通过变量文件部署
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。

其技术体系包括 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 登录。原稿登录后显示一个提示框,点击“知道了”进入管理界面。


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,但没有在末尾同时给出单阶段大小。该数值是原始环境观察,不能当作本文修正模板重新构建后的保证。实际比较时应同时查看两个不同标签、基础镜像、平台和共享层占用。
评论