第十六章 Docker 容器技术
第16章 Docker 容器技术
学习目标:通过本章的学习,零基础的小白也能从"什么是容器"开始,一步步掌握 Docker 的安装、镜像管理、Dockerfile 编写、容器管理、网络配置、数据持久化、多容器编排、实战部署和容器安全等核心知识。本章会用大量生活中的类比来帮助你理解那些看起来很专业的概念,让你学起来轻松不枯燥。
16.1 容器技术概述
16.1.1 从一个生活场景说起:搬家那些事
想象一下你要搬家。你有两种选择:
选择一:传统搬家法(类似虚拟机)
你雇了一辆大卡车,然后把整个房间的家具、电器、甚至连地板和墙壁都拆下来装上车。到了新家,你得先重新铺地板、砌墙壁,再把家具一件件摆好。这辆大卡车就像一台虚拟机——它要带上整个操作系统(地板和墙壁),沉重、缓慢、浪费空间。
选择二:集装箱搬家法(类似容器)
你把所有的东西分门别类装进标准化的集装箱里。每个集装箱大小一致,上面有标签写着"厨房用品"“卧室用品”“书房用品”。到了新家,你只需要把集装箱整体搬到对应的房间,打开就能用。不需要重新铺地板、砌墙壁,因为新家已经有地板和墙壁了(这就是宿主机的操作系统)。这些集装箱就是容器。
这个类比其实非常贴近现实。现实中,集装箱(Container) 的发明彻底改变了全球航运业。在集装箱出现之前,货物装卸全靠人工搬运,大小不一、形状各异,效率极低。1956 年,美国人马尔科姆·麦克莱恩发明了标准化的集装箱,从此船舶、卡车、火车都可以统一运输这些标准箱子,全球贸易效率提升了上百倍。
而 Docker 里的"容器",就是软件世界的集装箱。它把应用程序和所有依赖的东西打包成一个标准化的"箱子",不管你的服务器是 Ubuntu、CentOS 还是 Windows,只要装了 Docker,就能直接运行这个"箱子"里的程序。
16.1.2 为什么需要容器:一个程序员的心酸故事
让我们来看一个真实场景。小王是一个 Java 程序员,他开发了一个网站。在他的电脑上运行得好好的,但是部署到服务器上就出问题了:
- 他的电脑用的是 JDK 11,服务器上是 JDK 8,运行报错
- 他的电脑装了 Redis 6.0,服务器上是 Redis 3.0,功能不兼容
- 他的电脑是 Windows,服务器是 Linux,一些路径配置不生效
- 服务器的某个系统库版本太低,导致程序崩溃
小王折腾了三天三夜,终于把程序跑起来了。然后他换了台服务器,又得重来一遍。
这时候如果用了 Docker,小王只需要说一句:“把我的程序和它需要的所有东西打包成一个镜像”,然后把这个镜像扔到任何一台装了 Docker 的服务器上,一句 docker run 就能跑起来。就像你把一个集装箱从船上搬到卡车上,从卡车搬到火车上,里面的货物完全不用动。
容器解决了"在我机器上能跑,在你机器上不能跑"这个困扰程序员几十年的老大难问题。
16.1.3 虚拟机 vs 容器:一场重量级的对比
这是理解容器最关键的一个对比。我们用一张表来说明:
| 对比项 | 虚拟机(VM) | 容器(Container) |
|---|---|---|
| 类比 | 独栋别墅,自带地基和水电 | 公寓楼里的一套房,共享水电 |
| 包含内容 | 完整操作系统 + 应用 | 只含应用和依赖,共享宿主机内核 |
| 启动速度 | 几十秒到几分钟 | 几毫秒到几秒 |
| 占用空间 | 几个GB到几十个GB | 几MB到几百MB |
| 资源占用 | 高,每个VM都要分配固定内存 | 低,按需使用,多个容器共享内存 |
| 隔离性 | 强,硬件级隔离 | 较弱,进程级隔离 |
| 数量 | 一台机器跑几个到十几个 | 一台机器可以跑几百个 |
再打个比方:虚拟机就像在一块空地上盖了一栋栋独立的别墅,每栋别墅都有自己的地基、水电系统,互不干扰,但是占地大、成本高。容器就像一栋公寓楼里的一间间房,共享整栋楼的地基和水电,但是每间房有自己的门锁,互不干扰,占地小、成本低。
从技术层面来看:
- 虚拟机通过 Hypervisor(虚拟机监控器)在硬件层面虚拟出完整的计算机,每个虚拟机都有自己的操作系统内核。代表产品:VMware、VirtualBox、Hyper-V。
- 容器通过 Linux 内核的命名空间(Namespace)和控制组(Cgroup)技术,在操作系统层面实现隔离。所有容器共享宿主机的内核,不需要额外的操作系统。代表产品:Docker、Podman、LXC。
16.1.4 Docker 是什么
Docker 是一个开源的应用容器引擎,它让开发者可以把应用和依赖打包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上。Docker 的标志是一只背着集装箱的鲸鱼——鲸鱼代表 Docker 在海洋(云环境)中畅游,集装箱代表标准化的容器。
Docker 最初由 dotCloud 公司(后改名 Docker Inc.)的工程师 Solomon Hykes 在 2013 年发布。它基于 Linux 的 LXC(Linux Containers)技术发展而来,后来改用自己开发的 libcontainer(后改名 runc)作为默认运行时。Docker 的出现让容器技术从"只有大公司才能用的复杂技术"变成了"每个程序员都能上手的工具",可以说是容器技术普及的最大功臣。
Docker 包含三个核心概念:
-
镜像(Image):类似于面向对象编程中的"类",是一个只读的模板,包含了运行应用所需的所有内容(代码、库、环境变量、配置文件)。你可以把它想象成一张光盘(CD),里面刻录好了程序,但是不能修改。
-
容器(Container):类似于面向对象编程中的"对象",是镜像运行起来的实例。你可以把它想象成把光盘里的内容加载到内存中运行的程序。一个镜像可以同时创建多个容器,就像一个类可以创建多个对象。
-
仓库(Registry):存放镜像的地方,类似于 GitHub 存放代码,或者应用商店存放 App。最知名的是 Docker Hub(hub.docker.com),你可以从上面拉取别人做好的镜像,也可以把自己的镜像推上去分享给全世界。
它们三者的关系可以用一句话概括:**从仓库拉取镜像,用镜像创建容器,容器里运行着你的应用。**就像从应用商店(仓库)下载一个安装包(镜像),安装后运行起来就是一个程序(容器)。
16.1.5 容器技术的发展历程
了解一点历史,能帮你更好地理解为什么 Docker 会这么火:
- 1979年:Unix 引入 chroot 系统调用,这是最早的隔离技术,能改变进程的根目录,算得上容器的"老祖宗"。
- 2000年:FreeBSD Jail 出现,第一个真正意义上的容器技术,用于隔离用户和进程。
- 2008年:LXC(Linux Containers)发布,基于 Linux 内核的 Cgroup 和 Namespace,是 Linux 上第一个广泛使用的容器工具。
- 2013年:Docker 发布,把容器技术变得简单易用,一炮而红。
- 2015年:Docker 成立 OCI(Open Container Initiative)组织,制定容器标准,让容器不再绑定 Docker 一家。
- 2017年:Kubernetes 成为容器编排领域的事实标准,Docker 转向开发者工具。
- 至今:容器技术已经成为云计算的基础设施,几乎所有云原生应用都基于容器运行。
16.1.6 容器技术的应用场景
容器能用来做什么?这里列举一些常见场景:
- 开发环境统一:团队所有成员使用相同的开发环境镜像,告别"在我电脑上能跑"的问题。
- 微服务架构:每个微服务打包成一个容器,独立部署、独立扩展。
- CI/CD 持续集成:在容器中编译、测试、打包代码,保证环境一致性。
- 快速部署和回滚:容器启动只需几秒,出问题了秒级回滚到上一个版本。
- 混合云部署:同一个镜像可以部署在阿里云、腾讯云、AWS 任何云上。
- 一次性任务:用容器跑一些一次性的数据处理任务,跑完就删。
16.1.7 容器背后的核心技术:Namespace 和 Cgroup
前面我们说了很多容器的概念和比喻,但你可能会好奇:容器到底是怎么实现隔离的?为什么容器里的程序以为自己在独占一台机器?这背后靠的是 Linux 内核的两大核心技术:命名空间(Namespace)和控制组(Cgroup)。
命名空间(Namespace)—— 实现"你看不见我,我看不见你"
Namespace 是 Linux 内核提供的一种隔离机制。它让进程只能看到与自己相关的系统资源,感觉就像独占了整台机器。打个比方,Namespace 就像是给每个容器戴上了"VR 眼镜",让容器里的程序以为自己在一个完整的系统中运行,实际上它看到的只是虚拟出来的环境。
Linux 提供了以下几种 Namespace:
- PID Namespace:进程隔离。容器里的第一个进程编号是 1,它看不到宿主机上的其他进程。就像每个容器里有自己的"老大哥"(PID为1的进程),但它看不到隔壁容器里的进程。
- NET Namespace:网络隔离。每个容器有自己的网卡、IP 地址、路由表、端口号。就像每个容器有自己的"电话号码",彼此独立。
- IPC Namespace:进程间通信隔离。容器之间的信号量、消息队列、共享内存互不影响。
- MNT Namespace:文件系统挂载点隔离。每个容器看到不同的文件系统视图。就像每个容器有自己的"书架",看不到其他容器的书。
- UTS Namespace:主机名和域名隔离。每个容器可以有自己的主机名。就像每个容器有自己的"门牌号"。
- USER Namespace:用户和用户组隔离。容器内的 root 用户可以映射为宿主机的普通用户,提高安全性。
控制组(Cgroup)—— 实现"给你多少就用多少"
如果说 Namespace 解决的是"隔离"问题(你能看到什么),那 Cgroup 解决的就是"限制"问题(你能用多少)。Cgroup 可以限制容器使用的 CPU、内存、磁盘 I/O 等资源。
打个比方,Namespace 就像给每个租户分配了独立的房间(隔离),而 Cgroup 就像给每个房间装了电表和水表(限制),防止某个租户用掉整栋楼的电和水。
Cgroup 可以限制的资源包括:
- CPU:限制容器能使用的 CPU 核心数和 CPU 时间片
- 内存:限制容器最多能使用多少内存,超过就会被杀掉
- 磁盘 I/O:限制容器的读写速度
- 网络带宽:限制容器的网络流量
- 进程数:限制容器内能创建的进程数量
联合文件系统(UnionFS)—— 实现"一层一层叠起来"
除了 Namespace 和 Cgroup,Docker 还用到了联合文件系统技术来实现镜像的分层存储。UnionFS 允许多个目录"叠加"在一起,对外表现为一个统一的文件系统。
Docker 目前默认使用的存储驱动是 overlay2,它将镜像的各层(只读层)和容器的可写层叠加在一起。当你读取文件时,从上往下查找;当你修改文件时,会先把文件复制到可写层再修改(这种机制叫"写时复制" Copy-On-Write)。
这三大技术合在一起,就构成了容器技术的基石。Docker 做的事情,就是把这些复杂的内核技术封装成简单易用的命令行工具,让普通开发者也能轻松使用容器。
16.1.8 容器编排:从单机到集群
当你只有几个容器的时候,用 Docker 命令手动管理就够了。但当容器数量达到几十、几百甚至上千个的时候,手动管理就变成了噩梦:
- 某台机器宕机了,上面的容器怎么办?
- 流量突然增大,怎么自动扩容?
- 怎么做到滚动更新而不中断服务?
- 怎么确保总是有一定数量的容器在运行?
这就是容器编排工具要解决的问题。目前主流的容器编排工具有:
- Kubernetes(K8s):Google 开源,目前是容器编排领域的事实标准,功能最强大,社区最活跃。
- Docker Swarm:Docker 自带的编排工具,简单易用但功能有限。
- Apache Mesos:更通用的集群管理工具,也可以用来编排容器。
Docker Compose 主要用于单机上的多容器编排,而 Kubernetes 和 Docker Swarm 则用于跨多台机器的集群编排。在后面的章节中我们会学习 Docker Compose,Kubernetes 则留到下一章。
16.2 Docker 安装与配置
16.2.1 安装前的准备
在安装 Docker 之前,我们需要了解一些前提条件:
- Docker 原生运行在 Linux 上(因为它依赖 Linux 内核的 Namespace 和 Cgroup),所以推荐在 Linux 系统上安装。
- 如果你用的是 Windows 或 macOS,Docker 提供了 Docker Desktop,它会在后台启动一个轻量级的 Linux 虚拟机来运行 Docker。
- Docker 要求 Linux 内核版本 3.10 以上,可以用
uname -r命令查看你的内核版本。
# 查看内核版本
uname -r
# 输出示例:5.4.0-80-generic
# 查看系统版本(Ubuntu)
cat /etc/os-release
16.2.2 在 Ubuntu 上安装 Docker
Ubuntu 是最常用的 Docker 宿主系统之一。下面是详细的安装步骤:
方法一:使用官方脚本安装(最简单,推荐新手)
# 1. 更新 apt 包索引
sudo apt update
# 2. 安装必要的依赖包
sudo apt install -y ca-certificates curl gnupg lsb-release
# 3. 添加 Docker 官方 GPG 密钥
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# 4. 添加 Docker 软件源
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 5. 再次更新 apt 包索引
sudo apt update
# 6. 安装 Docker 引擎、命令行工具、containerd 和 Docker Compose
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
方法二:使用一键脚本(最快)
# 使用 Docker 官方一键安装脚本
curl -fsSL https://get.docker.com | sudo bash
这一条命令会自动检测你的系统类型,然后安装最新版本的 Docker。对于新手来说,这是最省心的方式。
16.2.3 在 CentOS 上安装 Docker
CentOS 也是非常常用的服务器系统:
# 1. 卸载旧版本(如果有的话)
sudo yum remove -y docker docker-client docker-client-latest docker-common docker-latest \
docker-latest-logrotate docker-logrotate docker-engine
# 2. 安装 yum-utils 工具
sudo yum install -y yum-utils
# 3. 添加 Docker 官方软件源
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# 4. 安装 Docker
sudo yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
16.2.4 启动 Docker 服务
安装完成后,需要启动 Docker 服务,并设置为开机自启:
# 启动 Docker 服务
sudo systemctl start docker
# 设置 Docker 开机自启
sudo systemctl enable docker
# 查看 Docker 服务状态
sudo systemctl status docker
如果看到绿色的 active (running),说明 Docker 已经成功启动了。
16.2.5 验证安装
运行一个测试容器来验证 Docker 是否正常工作:
# 运行 hello-world 容器
sudo docker run hello-world
如果你看到类似下面的输出,恭喜你,Docker 安装成功了:
Hello from Docker!
This message shows that your installation appears to be working correctly.
...
这条命令的执行过程是这样的:
- Docker 在本地查找 hello-world 镜像,没找到
- Docker 自动去 Docker Hub 仓库拉取 hello-world 镜像
- 用这个镜像创建并运行一个容器
- 容器输出一段欢迎信息后自动退出
16.2.6 配置非 root 用户使用 Docker
默认情况下,Docker 命令需要 sudo 权限才能运行。每次都打 sudo 很麻烦,我们可以把当前用户加入 docker 组:
# 将当前用户加入 docker 组
sudo usermod -aG docker $USER
# 刷新用户组权限(或者重新登录)
newgrp docker
# 现在可以直接用 docker 命令了,不需要 sudo
docker run hello-world
这里要注意一个安全问题:加入 docker 组的用户相当于拥有了 root 权限,因为用户可以通过 docker 挂载宿主机文件系统来获取 root 权限。在生产环境中要谨慎操作。
16.2.7 配置国内镜像加速器
由于 Docker Hub 的服务器在国外,国内拉取镜像可能很慢甚至超时。我们需要配置镜像加速器。国内常用的加速器有:
- 阿里云:
https://<你的ID>.mirror.aliyuncs.com - 腾讯云:
https://mirror.ccs.tencentyun.com - 中科大:
https://docker.mirrors.ustc.edu.cn
配置方法如下:
# 1. 创建 Docker 配置目录(如果没有的话)
sudo mkdir -p /etc/docker
# 2. 写入镜像加速器配置
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://docker.mirrors.ustc.edu.cn",
"https://mirror.ccs.tencentyun.com"
]
}
EOF
# 3. 重新加载 daemon 配置
sudo systemctl daemon-reload
# 4. 重启 Docker 服务
sudo systemctl restart docker
16.2.8 查看 Docker 信息
安装和配置完成后,可以用以下命令查看 Docker 的详细信息:
# 查看 Docker 版本信息
docker version
# 查看 Docker 系统信息(包括容器数、镜像数、存储驱动等)
docker info
# 查看 Docker 配置
docker info | grep -A 5 "Registry Mirrors"
docker info 是一个非常有用的命令,它会显示 Docker 的运行状态、容器数量、镜像数量、存储驱动、网络配置等大量信息。在排查问题的时候经常会用到。
16.2.9 Windows 和 macOS 安装 Docker Desktop
如果你使用的是 Windows 或 macOS,可以安装 Docker Desktop:
- 去 Docker 官网下载 Docker Desktop 安装包
- Windows 版本要求 Windows 10 64位专业版/企业版/教育版(Build 19041 以上),并且需要开启 WSL 2(Windows Subsystem for Linux 2)
- macOS 版本要求 macOS 11(Big Sur)以上
安装步骤很简单,双击安装包,一路下一步就行。安装完成后打开 Docker Desktop,等鲸鱼图标变成稳定状态就可以使用了。
开启 WSL 2 的方法(Windows):
# 在 PowerShell(管理员)中运行
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
# 重启电脑后,设置 WSL 2 为默认版本
wsl --set-default-version 2
16.2.10 常见安装问题与故障排除
在安装 Docker 的过程中,你可能会遇到各种问题。这里列举一些常见的坑和解决方法,帮你少走弯路。
问题一:无法连接到 Docker 守护进程
# 报错信息:
# Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
# 原因:Docker 服务没有启动
# 解决方法:启动 Docker 服务
sudo systemctl start docker
sudo systemctl enable docker
问题二:权限不足,无法执行 docker 命令
# 报错信息:
# Got permission denied while trying to connect to the Docker daemon socket
# 原因:当前用户没有 Docker 的操作权限
# 解决方法:将用户加入 docker 组
sudo usermod -aG docker $USER
newgrp docker
# 如果还是不行,退出重新登录
问题三:拉取镜像超时或速度很慢
# 原因:Docker Hub 服务器在国外,网络不通畅
# 解决方法:配置国内镜像加速器(见前面 16.2.7 节)
# 如果配置了加速器还是慢,可以尝试其他加速源
# 或者手动下载镜像后导入
问题四:端口被占用
# 报错信息:
# Bind for 0.0.0.0:80 failed: port is already allocated
# 原因:宿主机的 80 端口已经被其他程序占用
# 解决方法一:换一个端口
docker run -d -p 8080:80 nginx
# 解决方法二:找到并停止占用端口的程序
sudo lsof -i :80 # 查看谁占用了 80 端口
sudo kill -9 <PID> # 杀掉对应的进程
问题五:磁盘空间不足
# 原因:Docker 镜像和容器占用太多磁盘空间
# 解决方法:清理不需要的镜像、容器、数据卷
# 查看磁盘使用情况
docker system df
# 清理所有未使用的资源(悬空镜像、停止的容器、未使用的网络)
docker system prune
# 彻底清理(包括未使用的镜像和卷)
docker system prune -a --volumes
问题六:Docker 服务启动失败
# 查看 Docker 服务的详细错误日志
sudo journalctl -u docker.service --no-pager | tail -50
# 常见原因:
# 1. daemon.json 配置文件格式错误(检查 JSON 语法)
# 2. 防火墙或 SELinux 阻止了 Docker
# 3. 内核版本太低
# 4. 存储驱动不兼容
# 临时关闭 SELinux 测试
sudo setenforce 0
16.2.11 Docker 的目录结构
了解 Docker 的目录结构,有助于你更好地管理和排查问题:
# Docker 的主要数据目录
sudo ls -la /var/lib/docker/
# 主要子目录说明:
# containers/ —— 所有容器的数据(日志、配置等)
# image/ —— 镜像的元数据
# volumes/ —— 数据卷的实际存储位置
# overlay2/ —— 容器和镜像的文件层(默认存储驱动)
# network/ —— 网络配置文件
# 修改 Docker 数据目录(当 /var 空间不足时很有用)
# 编辑 /etc/docker/daemon.json
# {
# "data-root": "/data/docker"
# }
# 然后重启 Docker
sudo systemctl restart docker
16.3 Docker 镜像与仓库
16.3.1 镜像是什么:再打个比方
前面我们说过,镜像就像一张光盘。但更准确地说,镜像是一个分层的文件系统。
想象一下你在搭乐高积木。Docker 镜像就是一层层叠起来的乐高积木:
- 最底层是基础镜像,比如
ubuntu:20.04,这是地基 - 第二层可能装了一些系统工具,比如
apt install vim - 第三层装了运行环境,比如
apt install python3 - 第四层拷贝了你的代码文件
- 第五层设置了启动命令
每一层都是只读的,多层叠加在一起就构成了完整的镜像。当容器运行时,Docker 会在最上面再加一层可写层,容器运行时的所有修改都发生在这层。这种设计叫联合文件系统(UnionFS)。
这样做的好处是:
- 节省空间:多个镜像如果共享相同的底层,磁盘上只存一份
- 构建速度快:每一层都有缓存,如果某层没变,构建时直接用缓存
- 方便分发:拉取镜像时,已有的层不会重复下载
16.3.2 Docker Hub:镜像的应用商店
Docker Hub 是 Docker 官方的公共镜像仓库,地址是 hub.docker.com。它就像手机上的应用商店,你可以在上面搜索、下载各种镜像。
Docker Hub 上的镜像分为两类:
- 官方镜像(Official Images):由 Docker 官方维护,名字不带斜杠,如
nginx、mysql、redis、python。这些镜像质量高、更新及时,推荐使用。 - 个人/组织镜像:由个人或组织上传,名字带斜杠,如
user/myapp。使用时要谨慎,因为质量参差不齐。
在 Docker Hub 上搜索镜像,你也可以用命令行:
# 搜索 nginx 相关镜像
docker search nginx
# 输出示例:
# NAME DESCRIPTION STARS OFFICIAL
# nginx Official build of Nginx. 16000 [OK]
# jonasal/nginx-certbot Certificate manager for Nginx using Let's Enc… 500
# ...
NAME:镜像名称DESCRIPTION:描述STARS:收藏数,越多说明越受欢迎OFFICIAL:是否是官方镜像
16.3.3 拉取镜像:docker pull
从仓库下载镜像到本地,使用 docker pull 命令:
# 拉取最新版本的 nginx 镜像(默认标签 latest)
docker pull nginx
# 拉取指定版本的 nginx
docker pull nginx:1.25
# 拉取指定版本的 Ubuntu
docker pull ubuntu:22.04
# 从 Docker Hub 拉取个人用户的镜像
docker pull username/myapp:v1.0
# 从其他仓库拉取镜像(例如阿里云容器镜像服务)
docker pull registry.cn-hangzhou.aliyuncs.com/myname/myapp:v1.0
关于镜像标签(Tag)的说明:
nginx:latest中的latest是标签,表示最新版本- 如果不写标签,默认就是
latest - 建议在生产环境中使用明确的版本号,不要用
latest,因为 latest 指向的版本会变化,可能导致不可预期的行为
16.3.4 查看本地镜像:docker images
下载完镜像后,可以查看本地有哪些镜像:
# 列出所有本地镜像
docker images
# 输出示例:
# REPOSITORY TAG IMAGE ID CREATED SIZE
# nginx latest 605c77e624dd 2 weeks ago 141MB
# ubuntu 22.04 216c552ea5ba 3 weeks ago 77.8MB
# mysql 8.0 3218b38490ce 4 weeks ago 521MB
# 只列出镜像 ID
docker images -q
# 过滤显示悬空镜像(没有标签的镜像)
docker images -f dangling=true
# 按仓库名过滤
docker images nginx
各字段含义:
REPOSITORY:仓库名,即镜像名TAG:标签,即版本号IMAGE ID:镜像唯一标识(一个哈希值的前12位)CREATED:创建时间SIZE:镜像大小
16.3.5 删除镜像:docker rmi
当镜像不再需要时,可以删除以释放磁盘空间:
# 通过名称和标签删除镜像
docker rmi nginx:latest
# 通过镜像 ID 删除镜像
docker rmi 605c77e624dd
# 强制删除镜像(即使有容器在用)
docker rmi -f nginx:latest
# 删除所有未被使用的镜像(清理空间的好帮手)
docker image prune
# 删除所有本地镜像(慎用!)
docker rmi $(docker images -q)
注意:如果有容器正在使用某个镜像,直接删除会报错。需要先删除相关的容器,或者使用 -f 强制删除(不推荐)。
16.3.6 镜像标签管理:docker tag
给镜像打标签,类似于给文件起别名:
# 给本地镜像打一个新标签
docker tag nginx:latest mynginx:v1.0
# 这样本地就有了两个"镜像",但它们指向同一个镜像 ID
docker images
# REPOSITORY TAG IMAGE ID SIZE
# nginx latest 605c77e624dd 141MB
# mynginx v1.0 605c77e624dd 141MB
16.3.7 查看镜像详情:docker inspect
查看镜像的详细配置信息,包括创建时间、环境变量、暴露端口、入口命令等:
# 查看 nginx 镜像的详细信息
docker inspect nginx:latest
# 只查看某个字段,例如查看镜像的架构
docker inspect nginx:latest --format '{{.Architecture}}'
# 输出:amd64
16.3.8 查看镜像历史:docker history
查看镜像是怎么一层一层构建出来的,这在分析镜像大小、优化 Dockerfile 时很有用:
# 查看 nginx 镜像的构建历史
docker history nginx:latest
# 输出示例:
# IMAGE CREATED CREATED BY SIZE
# 605c77e624dd 2 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
# ...
16.3.9 导出和导入镜像
在没有网络的环境下,你可以把镜像导出成文件,拷贝到另一台机器上导入:
# 导出镜像为 tar 文件
docker save -o nginx.tar nginx:latest
# 导出多个镜像到一个文件
docker save -o all.tar nginx:latest mysql:8.0 redis:7
# 导入镜像
docker load -i nginx.tar
# 另一种方式:用管道
docker save nginx:latest | gzip > nginx.tar.gz
gunzip -c nginx.tar.gz | docker load
docker save 和 docker load 保存的是完整的镜像(包含所有层和元数据),适合离线传输。另外还有 docker export 和 docker import,它们操作的是容器,导出的是容器的文件系统快照(不含分层信息),体积更小但丢失了历史信息。
16.3.10 推送镜像到仓库:docker push
如果你想把自己的镜像分享给别人,或者备份到云端,可以推送到镜像仓库:
# 1. 先在 Docker Hub 注册账号
# 2. 命令行登录
docker login
# 输入用户名和密码
# 3. 给镜像打标签(格式必须是 用户名/镜像名:标签)
docker tag myapp:v1.0 yourusername/myapp:v1.0
# 4. 推送镜像
docker push yourusername/myapp:v1.0
# 5. 退出登录
docker logout
16.3.11 搭建私有仓库
在企业环境中,出于安全和速度考虑,通常会搭建私有镜像仓库。Docker 官方提供了一个 registry 镜像,可以快速搭建私有仓库:
# 拉取 registry 镜像并启动私有仓库
docker run -d -p 5000:5000 --name registry --restart=always \
-v /opt/registry:/var/lib/registry \
registry:2
# 给镜像打标签(指向私有仓库)
docker tag nginx:latest localhost:5000/mynginx:v1.0
# 推送到私有仓库
docker push localhost:5000/mynginx:v1.0
# 从私有仓库拉取
docker pull localhost:5000/mynginx:v1.0
如果私有仓库使用了 HTTPS,还需要在客户端配置信任。但对于本地测试,上面的配置就够用了。
16.3.12 镜像优化:让镜像更小更快
镜像大小直接影响拉取速度、部署时间和磁盘占用。优化镜像是 Docker 使用中非常重要的技能。下面是一些常用的优化技巧:
技巧一:选择更小的基础镜像
# 同样是 Python 镜像,大小差异巨大
python:3.11 # 约 900MB(完整版)
python:3.11-slim # 约 150MB(精简版)
python:3.11-alpine # 约 50MB(Alpine 版)
技巧二:合并 RUN 指令
每条 RUN 指令都会创建一个新的镜像层。把多条命令合并成一条可以减少层数:
# 不推荐:3个层
RUN apt-get update
RUN apt-get install -y vim
RUN rm -rf /var/lib/apt/lists/*
# 推荐:1个层
RUN apt-get update && \
apt-get install -y vim && \
rm -rf /var/lib/apt/lists/*
技巧三:使用多阶段构建
多阶段构建可以把编译环境和运行环境分离,只把最终产物放到运行镜像中(详见 16.4.7 节)。
技巧四:使用 .dockerignore
避免把不需要的文件打包进构建上下文,加快构建速度。
技巧五:利用构建缓存
把不常变化的步骤放在前面(如安装依赖),常变化的步骤放在后面(如拷贝代码)。这样修改代码时,前面的步骤可以复用缓存,不需要重新执行。
# 先拷贝依赖文件(不常变化)
COPY requirements.txt .
RUN pip install -r requirements.txt
# 再拷贝代码(经常变化)
COPY . .
技巧六:使用 docker squash 压缩层
# 实验性功能,把所有层压缩成一层
docker build --squash -t myapp:v1.0 .
16.3.13 镜像清理:释放磁盘空间
随着时间推移,本地会积累大量镜像,占用磁盘空间。定期清理很有必要:
# 查看 Docker 磁盘使用情况
docker system df
# 清理悬空镜像(没有标签的镜像,通常是被新版本覆盖的旧镜像)
docker image prune
# 清理所有未被使用的镜像
docker image prune -a
# 一键清理所有未使用的资源(镜像、容器、网络、缓存)
docker system prune
# 彻底清理(包括数据卷,慎用!)
docker system prune -a --volumes
16.4 Dockerfile 编写
16.4.1 Dockerfile 是什么:一份菜谱
如果要把 Dockerfile 类比成一样东西,那它就是一份菜谱。
想象你要做一道菜——红烧肉。菜谱上写着:
- 准备五花肉 500 克
- 切成 3 厘米见方的块
- 冷水下锅焯水
- 热锅放冰糖炒糖色
- 放入肉块翻炒上色
- 加入酱油、料酒、八角
- 小火炖 40 分钟
- 大火收汁
Dockerfile 就是这样一份"菜谱",它告诉 Docker 怎么一步步地制作一个镜像。每一行指令就是一步操作,Docker 按照顺序执行这些指令,最终"做出来"一个镜像。
Dockerfile 是一个纯文本文件,名字就叫 Dockerfile(没有扩展名)。用 docker build 命令根据 Dockerfile 构建镜像。
16.4.2 Dockerfile 基本语法
一个最简单的 Dockerfile 长这样:
# 使用 nginx 作为基础镜像
FROM nginx:latest
# 复制自定义的首页到容器中
COPY index.html /usr/share/nginx/html/
# 暴露 80 端口
EXPOSE 80
Dockerfile 的语法规则:
- 每一行是一条指令
- 指令不区分大小写,但约定俗成都用大写
#开头的是注释- 每条指令都会创建一个新的镜像层
16.4.3 常用指令详解
1. FROM:指定基础镜像
FROM 是 Dockerfile 的第一条指令(除了 ARG),表示你要在哪个镜像基础上构建。就像做菜先要准备食材一样,FROM 就是准备基础食材。
# 使用官方 nginx 镜像作为基础
FROM nginx:latest
# 使用 Ubuntu 作为基础
FROM ubuntu:22.04
# 使用 Python 运行环境作为基础
FROM python:3.11-slim
# 使用 Alpine Linux(一个非常小的 Linux 发行版,只有 5MB)
FROM alpine:3.18
# 使用多阶段构建(后面会讲)
FROM golang:1.21 AS builder
选择基础镜像的建议:
- 追求最小体积:用
alpine(约5MB) - 追求兼容性和稳定性:用
ubuntu或debian - 运行特定语言:用官方语言镜像如
python、node、golang - 生产环境:用
-slim版本,比完整版小很多,但比 alpine 兼容性好
2. RUN:执行命令
RUN 指令在构建镜像时执行命令,比如安装软件包、创建目录等。就像菜谱里"切菜""焯水"这些具体操作。
# 安装单个软件包
RUN apt update && apt install -y vim
# 安装多个软件包(推荐写在一起,减少层数)
RUN apt update && apt install -y \
vim \
curl \
wget \
git \
&& rm -rf /var/lib/apt/lists/*
关于 RUN 的两种写法:
- shell 形式:
RUN command,默认通过/bin/sh -c执行 - exec 形式:
RUN ["executable", "param1", "param2"],直接执行,不经过 shell
# shell 形式(推荐大多数情况)
RUN echo "hello" > /tmp/hello.txt
# exec 形式(需要精确控制时使用)
RUN ["/bin/bash", "-c", "echo hello > /tmp/hello.txt"]
最佳实践:把多条相关的命令用 && 连接在一起写成一条 RUN,这样可以减少镜像层数。同时记得清理缓存(如 rm -rf /var/lib/apt/lists/*),减小镜像体积。
3. COPY 和 ADD:复制文件
把宿主机的文件复制到镜像中。就像把食材从冰箱里拿出来放到案板上。
# 复制单个文件
COPY app.py /app/app.py
# 复制整个目录
COPY src/ /app/src/
# 复制并重命名
COPY config.yaml /app/config.yaml
# 使用通配符
COPY *.py /app/
# ADD 也可以复制文件,但多了解压和下载功能
ADD https://example.com/file.tar.gz /app/ # 下载并解压
ADD file.tar.gz /app/ # 自动解压 tar.gz
COPY 和 ADD 的区别:
- COPY 只能复制本地文件,简单直接,推荐使用
- ADD 额外支持:自动解压 tar 包、从 URL 下载文件
- 官方建议:除非需要自动解压功能,否则一律用 COPY
4. WORKDIR:设置工作目录
设置后续指令的工作目录,类似于 cd 命令。如果目录不存在会自动创建。
WORKDIR /app
# 之后的指令都在 /app 目录下执行
COPY . .
RUN pip install -r requirements.txt
推荐使用 WORKDIR 而不是 RUN cd /app,因为 WORKDIR 更清晰,而且如果中间有 cd 失败不容易出问题。
5. ENV:设置环境变量
# 设置环境变量
ENV APP_HOME=/app
ENV PYTHON_VERSION=3.11
ENV PATH=$PATH:/app/bin
# 在后续指令中使用
WORKDIR $APP_HOME
6. ARG:构建参数
和 ENV 类似,但 ARG 只在构建时有效,运行容器时不存在。
# 定义构建参数
ARG VERSION=1.0
ARG BUILD_DATE
# 使用参数
LABEL version=$VERSION
LABEL build-date=$BUILD_DATE
构建时可以传入参数值:
docker build --build-arg VERSION=2.0 --build-arg BUILD_DATE=2024-01-01 -t myapp .
7. EXPOSE:声明端口
告诉 Docker 容器会监听哪些端口。但注意,EXPOSE 只是声明,不会真正映射端口,还需要在 docker run 时用 -p 参数映射。
EXPOSE 80
EXPOSE 443
EXPOSE 8080/tcp 8443/udp
8. CMD:容器启动命令
容器启动时默认执行的命令。一个 Dockerfile 只能有一条 CMD,如果有多条,只有最后一条生效。
# shell 形式
CMD nginx -g "daemon off;"
# exec 形式(推荐)
CMD ["nginx", "-g", "daemon off;"]
# 带参数的形式
CMD ["python", "app.py"]
CMD 可以在 docker run 时被覆盖:
# 覆盖 CMD
docker run myimage echo "hello"
9. ENTRYPOINT:入口点
和 CMD 类似,但 ENTRYPOINT 不会被 docker run 的参数覆盖,而是把参数追加到 ENTRYPOINT 后面。
ENTRYPOINT ["python", "app.py"]
CMD ["--help"]
# 这会执行:python app.py --port 8080
docker run myimage --port 8080
# 这会执行:python app.py --help(使用默认 CMD 参数)
docker run myimage
ENTRYPOINT 和 CMD 配合使用的最佳实践:ENTRYPOINT 固定主命令,CMD 提供默认参数。
10. VOLUME:声明数据卷
VOLUME /data
VOLUME ["/data", "/logs"]
声明容器中某些目录需要持久化存储。运行容器时如果不手动挂载,Docker 会自动分配一个匿名卷。
11. USER:切换用户
出于安全考虑,不建议以 root 运行应用:
# 创建一个普通用户
RUN groupadd -r appuser && useradd -r -g appuser appuser
# 切换到普通用户
USER appuser
# 之后的命令都以 appuser 身份执行
CMD ["python", "app.py"]
12. LABEL:添加标签
给镜像添加元数据:
LABEL maintainer="zhangsan@example.com"
LABEL version="1.0"
LABEL description="This is my first Docker image"
13. HEALTHCHECK:健康检查
告诉 Docker 怎么检查容器是否健康运行:
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost/ || exit 1
16.4.4 第一个完整的 Dockerfile 示例
让我们写一个完整的 Dockerfile,构建一个 Python Flask 网站镜像:
# ===== 基础镜像 =====
FROM python:3.11-slim
# ===== 维护者信息 =====
LABEL maintainer="linuxlearner@example.com"
LABEL version="1.0"
LABEL description="A simple Flask web application"
# ===== 设置环境变量 =====
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV APP_HOME=/app
# ===== 设置工作目录 =====
WORKDIR $APP_HOME
# ===== 安装系统依赖 =====
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*
# ===== 复制依赖文件并安装 =====
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ===== 复制项目代码 =====
COPY . .
# ===== 创建非 root 用户 =====
RUN useradd -r -s /bin/false appuser && chown -R appuser:appuser $APP_HOME
USER appuser
# ===== 暴露端口 =====
EXPOSE 5000
# ===== 健康检查 =====
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:5000/health')" || exit 1
# ===== 启动命令 =====
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
16.4.5 构建镜像:docker build
写好 Dockerfile 后,用 docker build 命令构建镜像:
# 在 Dockerfile 所在目录构建镜像
docker build -t myflaskapp:v1.0 .
# -t 指定镜像名和标签,最后的 . 表示构建上下文目录(当前目录)
# 构建上下文目录中的所有文件都会被发送给 Docker 引擎
# 从指定路径的 Dockerfile 构建
docker build -f /path/to/Dockerfile -t myflaskapp:v1.0 /path/to/context
# 不使用缓存构建(从头开始)
docker build --no-cache -t myflaskapp:v1.0 .
# 构建时传入构建参数
docker build --build-arg VERSION=2.0 -t myflaskapp:v2.0 .
16.4.6 .dockerignore 文件
和 .gitignore 类似,.dockerignore 告诉 Docker 在构建时忽略哪些文件,避免把不需要的文件打包进镜像,同时也能加快构建速度:
# .dockerignore 文件内容
.git
.gitignore
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.venv/
*.egg-info/
dist/
build/
node_modules/
npm-debug.log
Dockerfile
docker-compose.yml
.env
*.md
.idea/
.vscode/
16.4.7 多阶段构建
多阶段构建是 Dockerfile 的一个高级特性,它能大幅减小最终镜像的体积。原理是:用一个大镜像来编译代码,然后把编译结果拷贝到一个小的运行镜像中,丢弃编译环境。
打个比方:你用一个大厨房来做菜(切菜、炒菜、炖汤),做完之后只把做好的菜端上桌,大厨房里的锅碗瓢盆都不需要了。
# ===== 第一阶段:编译阶段 =====
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp .
# ===== 第二阶段:运行阶段 =====
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
WORKDIR /app
# 只把编译好的二进制文件从第一阶段拷贝过来
COPY --from=builder /app/myapp .
EXPOSE 8080
CMD ["./myapp"]
这样最终镜像只有 alpine 的大小(约5MB)加上你的二进制文件,而不会带上整个 Go 编译器(几百MB)。
16.4.8 Dockerfile 最佳实践
- 选择小的基础镜像:优先用 alpine 或 slim 版本
- 合并 RUN 指令:减少镜像层数
- 清理缓存:安装软件后清理包管理器缓存
- 合理利用缓存:把不常变化的指令放前面,常变化的放后面
- 使用 .dockerignore:避免不必要的文件进入构建上下文
- 不要存敏感信息:密码、密钥等不要写在 Dockerfile 里
- 使用非 root 用户:提高安全性
- 使用多阶段构建:减小最终镜像体积
16.4.9 Dockerfile 常见错误和调试技巧
编写 Dockerfile 时难免会遇到各种问题。这里列举一些常见错误和调试方法,帮助你少走弯路:
常见错误一:构建上下文过大
# 错误现象:构建时发送上下文非常慢,输出 "Sending build context to Docker daemon 2.1GB"
# 原因:构建上下文目录中有大量不需要的文件(如 node_modules、.git 目录等)
# 解决方法:创建 .dockerignore 文件排除不需要的文件
# .dockerignore 示例
# .git
# node_modules
# *.log
# __pycache__
常见错误二:指令顺序错误导致缓存失效
# 错误写法:每次代码变化都会重新安装依赖
COPY . .
RUN pip install -r requirements.txt
# 正确写法:先拷贝依赖文件,再拷贝代码
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
常见错误三:容器启动后立即退出
# 错误:CMD 执行完就退出了
CMD ["echo", "hello"]
# 正确:前台运行一个持续服务
CMD ["nginx", "-g", "daemon off;"]
# Docker 容器的主进程(PID 1)必须在前台运行
# 如果主进程退出,容器就会停止
常见错误四:使用了不存在的构建参数
# 错误:使用了未定义的 ARG
RUN echo $UNDEFINED_ARG
# 正确:先定义再使用
ARG MY_ARG=default_value
RUN echo $MY_ARG
调试技巧:
# 1. 构建时查看详细输出
docker build --progress=plain -t myapp:v1 .
# 2. 构建到某一步停止(用于调试中间状态)
docker build --target builder -t myapp:debug .
# 3. 用中间镜像调试
# 构建失败时,Docker 会保留中间层镜像
# 用 docker images -a 查看所有镜像(包括中间层)
docker images -a
# 然后用中间层镜像启动容器进行调试
docker run -it <中间层镜像ID> /bin/bash
# 4. 查看 build 缓存
docker buildx du
# 5. 使用 docker history 分析镜像
docker history myapp:v1 --no-trunc
16.4.10 构建不同语言应用的 Dockerfile 模板
为了方便你快速上手,这里提供几种常用编程语言的 Dockerfile 模板:
Java Spring Boot 应用:
# 多阶段构建:先构建再运行
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:17-jre-alpine
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
USER app
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]
Go 应用:
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]
注意 Go 可以用 scratch(空镜像)作为基础镜像,最终镜像只包含你的二进制文件,大小通常只有十几MB。
静态网站(前端):
# 阶段1:构建前端
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 阶段2:用 Nginx 提供静态文件
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
16.5 Docker 容器生命周期管理
16.5.1 容器的生命周期:从出生到死亡
容器和人一样,有完整的生命周期。让我们用一个比喻来理解:
- 创建(create):就像婴儿出生,容器被创建出来,但还没开始运行
- 启动(start):就像人开始工作,容器开始运行里面的程序
- 运行(running):容器正在正常工作
- 暂停(pause):就像人午休,容器暂停运行,但内存中的内容还在
- 恢复(unpause):午休结束,恢复工作
- 停止(stop):就像人下班回家,容器优雅地停止运行
- 杀死(kill):强制终止,就像被人一拳打晕
- 删除(rm):容器彻底消失,就像人离世
16.5.2 创建并启动容器:docker run
docker run 是最常用的命令,它一次性完成"创建 + 启动"两个动作:
# 最简单的用法:运行一个容器
docker run nginx
# 后台运行容器(-d 表示 detach,在后台运行)
docker run -d nginx
# 交互式运行容器(-i 保持标准输入打开,-t 分配一个伪终端)
docker run -it ubuntu:22.04 /bin/bash
# 给容器起个名字(--name)
docker run -d --name mynginx nginx
# 端口映射(-p 宿主机端口:容器端口)
docker run -d -p 8080:80 --name mynginx nginx
# 访问 http://localhost:8080 就能访问容器的 80 端口
# 挂载数据卷(-v 宿主机路径:容器路径)
docker run -d -v /data/nginx/html:/usr/share/nginx/html -p 8080:80 nginx
# 设置环境变量(-e)
docker run -d -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0
# 设置容器重启策略(--restart)
docker run -d --restart=always nginx # 总是自动重启
docker run -d --restart=on-failure:3 nginx # 失败时重启,最多3次
docker run -d --restart=unless-stopped nginx # 除非手动停止,否则总是重启
# 限制资源(--memory 限制内存,--cpus 限制CPU)
docker run -d --memory=512m --cpus=1.0 nginx
# 运行后自动删除容器(--rm)
docker run --rm -it ubuntu:22.04 echo "hello world"
一个完整的实战例子:
docker run -d \
--name web-server \
-p 8080:80 \
-v /data/web:/usr/share/nginx/html \
-e NGINX_HOST=example.com \
--restart=unless-stopped \
--memory=256m \
--cpus=0.5 \
nginx:1.25
这条命令做了这些事:
-d:后台运行--name web-server:起名叫 web-server-p 8080:80:把宿主机的 8080 端口映射到容器的 80 端口-v /data/web:/usr/share/nginx/html:把宿主机的 /data/web 目录挂载到容器的网站目录-e NGINX_HOST=example.com:设置环境变量--restart=unless-stopped:自动重启策略--memory=256m:限制最多用 256MB 内存--cpus=0.5:限制最多用半个 CPUnginx:1.25:使用的镜像
16.5.3 查看容器:docker ps
# 查看正在运行的容器
docker ps
# 输出示例:
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# a1b2c3d4e5f6 nginx "/docker-entrypoint.…" 5 seconds ago Up 3 seconds 0.0.0.0:8080->80/tcp web-server
# 查看所有容器(包括已停止的)
docker ps -a
# 只显示容器 ID
docker ps -q
# 显示最新创建的容器
docker ps -l
# 过滤显示(例如只显示正在运行的)
docker ps -f status=running
# 显示文件大小
docker ps -s
# 自定义输出格式
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}"
各字段含义:
CONTAINER ID:容器唯一标识IMAGE:使用的镜像COMMAND:启动命令CREATED:创建时间STATUS:状态(Up = 运行中,Exited = 已停止)PORTS:端口映射NAMES:容器名称
16.5.4 启动/停止/重启容器
# 启动已停止的容器
docker start mynginx
docker start a1b2c3d4e5f6
# 停止运行中的容器(发送 SIGTERM 信号,优雅停止)
docker stop mynginx
# 等待10秒后强制停止
docker stop -t 10 mynginx
# 重启容器
docker restart mynginx
# 强制停止容器(发送 SIGKILL 信号,相当于拔电源)
docker kill mynginx
stop 和 kill 的区别:stop 会先发 SIGTERM 信号让容器里的程序优雅退出(保存数据、关闭连接),等待一段时间(默认10秒)后才发 SIGKILL 强制终止。而 kill 直接发 SIGKILL,程序来不及做任何清理工作。
16.5.5 暂停和恢复容器
# 暂停容器(容器还在内存中,但不执行任何操作)
docker pause mynginx
# 恢复暂停的容器
docker unpause mynginx
pause 和 stop 的区别:pause 不释放内存,容器状态被冻结,恢复时瞬间继续运行。stop 会释放内存,重新启动需要一定时间。
16.5.6 删除容器:docker rm
# 删除已停止的容器
docker rm mynginx
# 强制删除运行中的容器(先kill再rm)
docker rm -f mynginx
# 删除所有已停止的容器
docker container prune
# 删除所有容器(慎用)
docker rm -f $(docker ps -aq)
16.5.7 进入容器:docker exec
当容器在后台运行时,你可能需要进去看看情况,就像打开微波炉看看里面的饭热了没有:
# 进入容器并打开 bash 终端
docker exec -it mynginx /bin/bash
# 进入容器并打开 sh 终端(alpine 等精简系统用 sh)
docker exec -it myalpine /bin/sh
# 在容器中执行单条命令
docker exec mynginx ls /etc/nginx
docker exec mynginx cat /etc/nginx/nginx.conf
# 以 root 用户进入容器
docker exec -u 0 -it mynginx /bin/bash
-it 参数的含义:
-i:保持标准输入打开,即使没有附加-t:分配一个伪终端
这两个参数一起用,你就能获得一个交互式终端,可以在里面输入命令。
16.5.8 查看容器日志:docker logs
# 查看容器所有日志
docker logs mynginx
# 实时跟踪日志输出(类似 tail -f)
docker logs -f mynginx
# 显示最后 100 行日志
docker logs --tail 100 mynginx
# 显示最近 10 分钟的日志
docker logs --since 10m mynginx
# 显示指定时间之后的日志
docker logs --since "2024-01-01T00:00:00" mynginx
# 显示时间戳
docker logs -t mynginx
# 组合使用:实时显示最后 50 行日志,带时间戳
docker logs -f -t --tail 50 mynginx
16.5.9 查看容器进程:docker top
# 查看容器中运行的进程
docker top mynginx
# 输出示例:
# UID PID PPID C STIME TTY TIME CMD
# root 1234 1230 0 10:00 ? 00:00:00 nginx: master process nginx
# nginx 1235 1234 0 10:00 ? 00:00:00 nginx: worker process
16.5.10 查看容器资源使用:docker stats
# 实时查看所有容器的资源使用情况
docker stats
# 查看指定容器的资源使用
docker stats mynginx
# 只输出一次结果(不持续刷新)
docker stats --no-stream
# 输出示例:
# CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
# a1b2c3d4e5f6 mynginx 0.50% 5.5MiB / 256MiB 2.15% 1.2kB / 648B 0B / 0B 2
这个命令就像 Windows 的任务管理器,可以看到每个容器占用了多少 CPU、内存、网络和磁盘 IO。
16.5.11 查看容器详情:docker inspect
# 查看容器所有详细信息
docker inspect mynginx
# 只查看 IP 地址
docker inspect mynginx --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
# 只查看挂载的卷
docker inspect mynginx --format '{{json .Mounts}}'
# 只查看环境变量
docker inspect mynginx --format '{{json .Config.Env}}'
# 只查看启动命令
docker inspect mynginx --format '{{.Config.Cmd}}'
16.5.12 容器与宿主机之间的文件拷贝:docker cp
# 从容器拷贝文件到宿主机
docker cp mynginx:/etc/nginx/nginx.conf /tmp/nginx.conf
# 从宿主机拷贝文件到容器
docker cp /tmp/myfile.txt mynginx:/tmp/myfile.txt
# 拷贝整个目录
docker cp mynginx:/etc/nginx /tmp/nginx_config
16.5.13 提交容器为镜像:docker commit
如果你在容器里做了修改,想把当前状态保存成新镜像,可以用 commit:
# 提交容器为新镜像
docker commit mynginx mynginx:v2.0
# 带提交信息
docker commit -m "Added custom config" -a "zhangsan" mynginx mynginx:v2.0
但要注意,docker commit 不推荐在生产环境中使用,因为它不透明(别人不知道这个镜像里改了什么)。正确做法是用 Dockerfile 构建镜像,这样每次修改都有记录。
16.5.14 常用容器管理命令汇总
docker create # 创建容器但不启动
docker start # 启动容器
docker run # 创建并启动容器
docker stop # 停止容器
docker restart # 重启容器
docker kill # 强制终止容器
docker pause # 暂停容器
docker unpause # 恢复容器
docker rm # 删除容器
docker exec # 在运行中的容器执行命令
docker attach # 连接到容器(注意:退出会导致容器停止)
docker logs # 查看日志
docker top # 查看进程
docker stats # 查看资源使用
docker inspect # 查看详细信息
docker cp # 拷贝文件
docker commit # 提交为镜像
docker rename # 重命名容器
docker update # 更新容器配置
docker export # 导出容器文件系统
docker port # 查看端口映射
docker diff # 查看容器文件系统变化
16.5.15 容器管理的实用技巧
技巧一:给容器起一个好名字
容器名就像人的名字,起得好能省很多事。Docker 默认会给容器随机起名(如 adoring_minsky、eloquent_liskov),但这些名字毫无意义,管理起来很痛苦。
# 好的命名规范:项目名-服务名-序号
docker run -d --name myblog-nginx-01 nginx
docker run -d --name myblog-mysql-01 mysql:8.0
docker run -d --name myblog-redis-01 redis:7
# 这样一眼就能看出这个容器属于哪个项目、是什么服务、第几台
技巧二:使用标签管理容器
# 通过环境变量给容器打标签
docker run -d \
--name myapp \
-e APP_ENV=production \
-e APP_VERSION=2.1.0 \
-e DEPARTMENT=tech \
myapp:v2.1.0
# 然后可以通过环境变量筛选容器
docker ps --filter "label=com.docker.compose.project=myblog"
技巧三:一键清理所有容器
# 停止所有运行中的容器
docker stop $(docker ps -q)
# 删除所有容器
docker rm $(docker ps -aq)
# 强制删除所有容器(包括运行中的)
docker rm -f $(docker ps -aq)
# 删除所有未运行的容器
docker container prune -f
技巧四:查看容器内部文件变化
# 查看容器相对于镜像改动了哪些文件
docker diff mynginx
# 输出含义:
# A = Added(新增的文件)
# C = Changed(修改的文件)
# D = Deleted(删除的文件)
技巧五:容器资源监控
# 实时监控所有容器的资源使用
docker stats
# 只看某个容器的资源使用,只输出一次
docker stats --no-stream mynginx
# 查看容器内正在运行的进程
docker top mynginx
# 查看容器的退出码(排查容器为什么停止)
docker inspect mynginx --format '{{.State.ExitCode}}'
# 0 = 正常退出
# 1 = 一般错误
# 137 = 被OOM杀死(内存不足)
# 143 = 被SIGTERM终止(正常停止)
技巧六:容器日志管理
# 容器日志默认存放在宿主机的这个位置
# /var/lib/docker/containers/<容器ID>/<容器ID>-json.log
# 查看某个容器的日志文件大小
docker inspect mynginx --format '{{.LogPath}}'
sudo ls -lh $(docker inspect mynginx --format '{{.LogPath}}')
# 限制容器日志大小(在 daemon.json 中全局配置)
# /etc/docker/daemon.json
# {
# "log-driver": "json-file",
# "log-opts": {
# "max-size": "10m",
# "max-file": "3"
# }
# }
# 或者针对单个容器配置
docker run -d \
--log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
nginx
技巧七:更新运行中容器的配置
# 更新容器的重启策略
docker update --restart=always mynginx
# 更新容器的资源限制
docker update --memory=512m --cpus=1.0 mynginx
# 注意:不是所有配置都能动态更新,有些需要重建容器
技巧八:查看 Docker 系统整体状态
# 查看 Docker 整体资源使用情况
docker system df
# 输出示例:
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 15 5 5.2GB 3.1GB (59%)
# Containers 8 3 120MB 80MB (66%)
# Local Volumes 5 3 2.1GB 500MB (23%)
# Build Cache 20 0 800MB 800MB
# 查看实时事件(容器启动、停止、删除等事件)
docker system events
# 查看 Docker 整体信息
docker system info
16.6 Docker 网络配置
16.6.1 Docker 网络基础:为什么要了解网络
想象你住在一个小区里。你的房子就是容器,小区的路网就是 Docker 网络。不同的容器需要互相通信(串门),也需要和外部世界通信(出门),这就需要网络配置。
Docker 网络是容器之间以及容器与外部世界通信的桥梁。理解 Docker 网络,对于部署多容器应用至关重要。
16.6.2 Docker 的四种网络模式
Docker 安装后会自动创建三种网络,加上可以自定义的网络,主要有以下几种模式:
1. bridge 模式(桥接模式,默认)
这是最常用的网络模式。就像小区里建了一座桥,所有容器都连到这座桥上,彼此可以通信,也可以通过桥连接到外部网络。
Docker 在宿主机上创建一个虚拟网桥 docker0,所有容器的网络都连接到这个网桥上。每个容器会分配一个内部 IP 地址(通常是 172.17.0.x)。
# 默认就是 bridge 模式
docker run -d --name mynginx nginx
# 查看容器的网络信息
docker inspect mynginx --format '{{.NetworkSettings.IPAddress}}'
# 输出示例:172.17.0.2
2. host 模式(主机模式)
容器直接使用宿主机的网络,没有网络隔离。就像你不住在小区里,而是直接住在马路边,和外部世界零距离。好处是网络性能好(没有虚拟化开销),坏处是没有隔离,端口可能冲突。
# 使用 host 模式(不需要 -p 端口映射,直接用宿主机端口)
docker run -d --network host nginx
# 此时访问宿主机的 80 端口就直接访问到了容器
3. none 模式(无网络模式)
容器没有网络,完全隔离。就像你住在一个与世隔绝的山洞里,谁也联系不上你。适用于一些不需要网络的计算任务。
docker run -d --network none alpine sleep 3600
4. container 模式(容器模式)
新容器和指定的容器共享网络命名空间。就像两个人合租一套房,共享同一个 WiFi。
# 第一个容器
docker run -d --name container1 nginx
# 第二个容器共享第一个容器的网络
docker run -d --name container2 --network container:container1 alpine sleep 3600
5. overlay 模式(覆盖网络模式)
用于跨多台宿主机的容器通信,通常配合 Docker Swarm 使用。就像不同城市的分公司通过 VPN 连成一个大网络。这个在单机学习阶段用得不多,了解即可。
16.6.3 查看和管理网络
# 查看所有网络
docker network ls
# 输出示例:
# NETWORK ID NAME DRIVER SCOPE
# a1b2c3d4e5f6 bridge bridge local
# b2c3d4e5f6a1 host host local
# c3d4e5f6a1b2 none null local
# 查看网络详情
docker network inspect bridge
# 查看哪些容器连接到了某个网络
docker network inspect bridge --format '{{range .Containers}}{{.Name}} {{end}}'
16.6.4 创建自定义网络
默认的 bridge 网络有一个缺点:容器之间只能通过 IP 地址通信,不能用容器名。而自定义网络支持通过容器名互相访问,就像小区里的住户可以通过名字找到彼此。
# 创建一个自定义 bridge 网络
docker network create mynetwork
# 创建时指定子网和网关
docker network create --subnet=172.20.0.0/16 --gateway=172.20.0.1 mynetwork
# 创建一个 overlay 网络(跨主机通信需要)
docker network create -d overlay myoverlay
# 删除网络
docker network rm mynetwork
# 删除所有未使用的网络
docker network prune
16.6.5 容器连接到网络
# 创建容器时指定网络
docker run -d --name web --network mynetwork nginx
# 让已运行的容器连接到另一个网络
docker network connect mynetwork mynginx
# 断开容器与网络的连接
docker network disconnect mynetwork mynginx
16.6.6 容器之间的通信
使用自定义网络后,容器之间可以通过容器名互相访问,这比记住 IP 地址方便多了:
# 1. 创建自定义网络
docker network create appnet
# 2. 启动一个数据库容器
docker run -d --name db --network appnet -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0
# 3. 启动一个 Web 容器
docker run -d --name web --network appnet -p 8080:80 nginx
# 4. 在 web 容器中可以通过名字访问 db 容器
docker exec -it web ping db
# 可以看到 db 容器的 IP 地址被解析出来了
# 5. 在 web 容器中连接数据库
docker exec -it web apt update && apt install -y mysql-client
docker exec -it web mysql -h db -u root -p123456
这就是容器间通信的核心:在同一个自定义网络中,容器可以通过名字找到彼此,Docker 内置了 DNS 解析功能。
16.6.7 端口映射详解
容器有自己的端口,但外部世界访问不到。需要通过端口映射,把宿主机的端口和容器的端口关联起来。就像你家的门牌号(容器端口)外人不知道,但小区大门的门牌号(宿主机端口)外人能看到,门卫(Docker)会帮你转接。
# 基本端口映射:宿主机 8080 -> 容器 80
docker run -d -p 8080:80 nginx
# 映射到指定 IP
docker run -d -p 127.0.0.1:8080:80 nginx
# 映射多个端口
docker run -d -p 8080:80 -p 8443:443 nginx
# 随机映射端口(Docker 自动分配宿主机端口)
docker run -d -P nginx
# 查看端口映射
docker port mynginx
# 输出:80/tcp -> 0.0.0.0:8080
# 映射 UDP 端口
docker run -d -p 53:53/udp dns-server
关于端口映射的注意事项:
- 宿主机端口不能被其他程序占用,否则会报错
- 如果多个容器都映射到宿主机的同一个端口,会冲突
-P(大写)是随机映射,-p(小写)是指定映射- 生产环境建议指定明确的端口,不要用随机映射
16.6.8 网络实战:搭建 Web + DB 架构
让我们用网络知识搭建一个简单的 Web 应用 + 数据库的架构:
# 1. 创建网络
docker network create webapp-net
# 2. 启动 MySQL 数据库
docker run -d \
--name mysql-db \
--network webapp-net \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=myapp \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=apppass \
-v mysql-data:/var/lib/mysql \
mysql:8.0
# 3. 启动 Redis 缓存
docker run -d \
--name redis-cache \
--network webapp-net \
redis:7-alpine
# 4. 启动 Web 应用
docker run -d \
--name web-app \
--network webapp-net \
-p 8080:5000 \
-e DB_HOST=mysql-db \
-e DB_USER=appuser \
-e DB_PASSWORD=apppass \
-e REDIS_HOST=redis-cache \
myapp:v1.0
# 现在 web-app 容器可以通过容器名 mysql-db 和 redis-cache 访问数据库和缓存
# 外部用户通过 http://localhost:8080 访问 web-app
16.6.9 Docker 网络故障排除
在实际使用中,网络问题是最常见的 Docker 问题之一。这里介绍一些排查网络问题的方法:
排查一:容器无法访问外网
# 1. 进入容器测试网络
docker exec -it mynginx ping 8.8.8.8
# 如果 ping 不通,说明容器没有外网访问权限
# 2. 检查容器的 DNS 配置
docker exec -it mynginx cat /etc/resolv.conf
# 如果 DNS 服务器配置不对,容器无法解析域名
# 3. 检查宿主机网络是否正常
ping 8.8.8.8
# 4. 检查 Docker 网络配置
docker network inspect bridge
# 5. 重启 Docker 网络服务
sudo systemctl restart docker
排查二:容器之间无法通信
# 1. 确认两个容器在同一个网络中
docker network inspect mynetwork
# 检查 Containers 字段中是否包含两个容器
# 2. 如果不在同一网络,把容器加入同一网络
docker network connect mynetwork container2
# 3. 测试容器间连通性
docker exec -it container1 ping container2
# 4. 检查防火墙是否阻止了容器间通信
sudo iptables -L -n
排查三:端口映射不生效
# 1. 检查端口映射配置
docker port mynginx
# 2. 检查宿主机端口是否被占用
sudo netstat -tlnp | grep :8080
# 3. 检查容器内服务是否正常监听
docker exec -it mynginx netstat -tlnp
# 4. 检查防火墙是否放行了该端口
sudo firewall-cmd --list-ports # CentOS
sudo ufw status # Ubuntu
16.6.10 Docker DNS 解析机制
在自定义网络中,Docker 内置了 DNS 服务器,容器之间可以通过容器名互相访问。了解这个机制有助于排查网络问题:
# Docker DNS 的工作原理:
# 1. 当容器加入自定义网络时,Docker 会自动给它分配一个 DNS 名称
# 2. DNS 名称就是容器名(或 --network-alias 指定的别名)
# 3. 当容器A访问容器B的名字时,Docker DNS 会把名字解析为容器B的 IP
# 设置网络别名(一个容器可以有多个别名)
docker run -d --name myapp --network mynet --network-alias api --network-alias backend myapp:v1
# 现在其他容器可以通过 myapp、api、backend 三个名字访问这个容器
# 注意:默认的 bridge 网络没有 DNS 功能,容器之间只能通过 IP 访问
# 所以一定要使用自定义网络来实现容器间通信
16.7 Docker 数据卷与持久化
16.7.1 为什么需要持久化:一个悲伤的故事
假设你运行了一个 MySQL 容器,存了几个月的数据。有一天你不小心执行了 docker rm 删除了这个容器。结果——所有数据全没了!
这是因为容器的文件系统是临时的。容器删除后,里面所有的数据都会消失。就像你在沙滩上盖了一座城堡,涨潮一来就什么都没了。
为了解决这个问题,Docker 提供了数据卷(Volume)机制,让你可以把数据存储在容器之外,就像在沙滩旁边建了一个防水保险箱,涨潮也冲不走。
16.7.2 Docker 的三种数据存储方式
Docker 提供了三种方式来实现数据持久化:
1. Volume(数据卷) —— Docker 管理的存储
由 Docker 创建和管理,存储在宿主机的特定目录下(Linux 上是 /var/lib/docker/volumes/)。这是官方推荐的方式。
# 创建一个数据卷
docker volume create mydata
# 使用数据卷启动容器
docker run -d -v mydata:/app/data nginx
# 查看 mydata 在宿主机上的实际路径
docker volume inspect mydata
# 可以看到 Mountpoint: /var/lib/docker/volumes/mydata/_data
2. Bind Mount(绑定挂载) —— 直接挂载宿主机目录
把宿主机上的一个目录直接挂载到容器中。你可以明确知道数据存在宿主机的哪个位置。
# 把宿主机的 /data/web 挂载到容器的 /usr/share/nginx/html
docker run -d -v /data/web:/usr/share/nginx/html nginx
# 使用只读模式挂载(容器不能修改,只能读取)
docker run -d -v /data/config:/etc/nginx:ro nginx
# 使用 --mount 语法(更明确,推荐)
docker run -d --mount type=bind,source=/data/web,target=/usr/share/nginx/html nginx
3. tmpfs Mount(临时文件系统) —— 存在内存中
数据存储在宿主机的内存中,不会写入磁盘。容器停止后数据消失。适用于存储敏感信息或需要高速访问的临时数据。
docker run -d --tmpfs /app/cache nginx
三种方式的对比:
| 方式 | 存储位置 | 管理者 | 适用场景 |
|---|---|---|---|
| Volume | Docker 管理的区域 | Docker | 大多数场景,推荐使用 |
| Bind Mount | 宿主机任意位置 | 用户 | 开发环境,需要直接访问文件 |
| tmpfs | 宿主机内存 | Docker | 敏感数据,临时数据 |
16.7.3 数据卷的完整操作
# 创建数据卷
docker volume create mydata
# 列出所有数据卷
docker volume ls
# 查看数据卷详情
docker volume inspect mydata
# 删除数据卷
docker volume rm mydata
# 删除所有未使用的数据卷(清理空间)
docker volume prune
# 启动容器时自动创建数据卷(如果不存在的话)
docker run -d -v auto-created:/app/data nginx
16.7.4 数据卷的实战使用
场景一:MySQL 数据持久化
# 创建数据卷
docker volume create mysql-data
# 启动 MySQL 容器,数据存储在数据卷中
docker run -d \
--name mysql \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
-v mysql-data:/var/lib/mysql \
mysql:8.0
# 现在即使删除容器,数据还在
docker stop mysql && docker rm mysql
# 重新创建容器,挂载同一个数据卷,数据就回来了
docker run -d \
--name mysql \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
-v mysql-data:/var/lib/mysql \
mysql:8.0
场景二:Nginx 网站文件持久化
# 在宿主机创建网站目录
mkdir -p /data/nginx/html
echo "<h1>Hello Docker!</h1>" > /data/nginx/html/index.html
# 启动 Nginx,挂载网站目录
docker run -d \
--name nginx \
-p 80:80 \
-v /data/nginx/html:/usr/share/nginx/html:ro \
nginx:latest
# 修改网站内容,立即生效
echo "<h1>Updated!</h1>" > /data/nginx/html/index.html
# 刷新浏览器就能看到变化
场景三:多个容器共享数据
# 创建共享数据卷
docker volume create shared-data
# 容器 A 写入数据
docker run -d --name writer -v shared-data:/data alpine \
sh -c 'while true; do echo "Hello from writer at $(date)" >> /data/log.txt; sleep 5; done'
# 容器 B 读取数据
docker run -d --name reader -v shared-data:/data alpine \
sh -c 'tail -f /data/log.txt'
# 查看 reader 的日志
docker logs reader
16.7.5 数据卷的备份和恢复
备份数据卷:
# 假设有一个名为 mysql-data 的数据卷
# 用一个临时容器把数据卷内容打包成 tar 文件
docker run --rm \
-v mysql-data:/data:ro \
-v /backup:/backup \
alpine tar czf /backup/mysql-data-backup.tar.gz -C /data .
# 解释:
# --rm:容器执行完毕后自动删除
# -v mysql-data:/data:ro:以只读方式挂载数据卷
# -v /backup:/backup:挂载宿主机的备份目录
# alpine tar czf ...:用 tar 命令打包压缩
恢复数据卷:
# 创建一个新的空数据卷
docker volume create mysql-data-restored
# 用临时容器解压备份文件到新数据卷
docker run --rm \
-v mysql-data-restored:/data \
-v /backup:/backup:ro \
alpine sh -c 'tar xzf /backup/mysql-data-backup.tar.gz -C /data'
16.7.6 在 Dockerfile 中使用 VOLUME 指令
在 Dockerfile 中可以用 VOLUME 指令声明需要持久化的目录:
FROM mysql:8.0
VOLUME /var/lib/mysql
这样即使用户运行容器时忘了挂载 -v,Docker 也会自动创建一个匿名卷来存储数据,防止数据丢失。但匿名卷不好管理,所以还是建议运行时显式指定 -v。
16.7.7 数据迁移:把数据搬到新机器
在实际工作中,你经常需要把数据从一台服务器迁移到另一台。使用数据卷后,数据迁移变得非常方便。
方法一:通过数据卷容器迁移
# 1. 在旧机器上,创建一个包含数据卷的容器(数据卷容器)
docker create -v /data --name data-container alpine
# 2. 把旧容器的数据复制到数据卷容器
docker cp /old/data/. data-container:/data/
# 3. 把数据卷容器导出为镜像
docker export data-container | docker import - data-image:latest
# 4. 把镜像保存为文件
docker save -o data-image.tar data-image:latest
# 5. 把文件传到新机器
scp data-image.tar newserver:/tmp/
# 6. 在新机器上导入镜像
docker load -i /tmp/data-image.tar
# 7. 用这个镜像启动容器,数据就迁移过来了
docker run -d --name new-container -v /data:/app/data some-app
方法二:直接拷贝数据卷目录(更简单)
# 1. 找到数据卷在宿主机上的实际路径
docker volume inspect my-data
# Mountpoint: /var/lib/docker/volumes/my-data/_data
# 2. 打包数据
sudo tar czf /tmp/my-data-backup.tar.gz -C /var/lib/docker/volumes/my-data/_data .
# 3. 传到新机器
scp /tmp/my-data-backup.tar.gz newserver:/tmp/
# 4. 在新机器上创建数据卷
docker volume create my-data
# 5. 解压数据到新数据卷
sudo tar xzf /tmp/my-data-backup.tar.gz -C /var/lib/docker/volumes/my-data/_data
16.7.8 数据卷管理的最佳实践
-
给数据卷起有意义的名字:不要用默认的匿名卷,用
docker volume create mysql-prod-data这样明确的名字。 -
定期备份:重要数据一定要定期备份,不要等数据丢了才后悔。
-
不要在容器中存储重要数据:容器是临时的,重要数据一定要放在数据卷或绑定挂载中。
-
使用命名卷而非匿名卷:命名卷更容易管理、备份和识别。
-
生产环境用数据卷,开发环境用绑定挂载:开发时需要直接修改文件,用绑定挂载更方便;生产环境用数据卷更安全、性能更好。
-
清理无用数据卷:定期用
docker volume prune清理不再使用的数据卷,释放空间。但清理前一定要确认数据不再需要。 -
数据卷的权限问题:如果容器内以非 root 用户运行,挂载的数据卷可能存在权限问题。可以在 Dockerfile 中提前修改目录权限,或者使用
--user参数指定正确的用户。
16.8 Docker Compose 多容器编排
16.8.1 为什么需要 Compose:从手动到自动
想象你要部署一个网站,需要 Nginx 做前端、Python 做后端、MySQL 做数据库、Redis 做缓存。如果用 docker run 命令,你需要:
docker network create mynet
docker run -d --name db --network mynet -v ... -e ... mysql
docker run -d --name redis --network mynet redis
docker run -d --name backend --network mynet -e ... -p ... myapp
docker run -d --name frontend --network mynet -p ... nginx
这么多命令,不仅繁琐,还容易出错。万一哪天换台机器部署,又得重新敲一遍。
Docker Compose 就是为了解决这个问题。它让你用一个 YAML 文件描述所有的容器、网络、数据卷配置,然后一条命令就能启动所有服务。就像一份乐谱,指挥家(Compose)照着乐谱一挥手,整个乐队(所有容器)就开始演奏了。
16.8.2 安装 Docker Compose
如果你安装的是新版 Docker(Docker Engine 20.10+),Docker Compose 已经作为插件内置了,直接用 docker compose 命令即可。
# 检查是否已安装
docker compose version
# 如果没有安装,在 Ubuntu 上可以安装插件
sudo apt install docker-compose-plugin
如果是老版本,需要单独安装 Docker Compose V1:
# 下载 Docker Compose 二进制文件
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" \
-o /usr/local/bin/docker-compose
# 添加执行权限
sudo chmod +x /usr/local/bin/docker-compose
# 验证安装
docker-compose --version
注意 V1 用 docker-compose(带横杠),V2 用 docker compose(空格),功能基本一样。
16.8.3 docker-compose.yml 文件结构
Docker Compose 的核心是一个名为 docker-compose.yml 的文件。让我们先看一个完整的例子:
# docker-compose.yml 文件版本
version: '3.8'
# 定义所有服务
services:
# Web 前端服务
web:
image: nginx:1.25
container_name: my-web
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- app
networks:
- frontend
restart: unless-stopped
# 应用后端服务
app:
build: ./app
container_name: my-app
environment:
- DB_HOST=db
- DB_NAME=myapp
- DB_USER=appuser
- DB_PASSWORD=apppass
- REDIS_HOST=redis
ports:
- "5000:5000"
depends_on:
- db
- redis
networks:
- frontend
- backend
restart: unless-stopped
# 数据库服务
db:
image: mysql:8.0
container_name: my-db
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: myapp
MYSQL_USER: appuser
MYSQL_PASSWORD: apppass
volumes:
- db-data:/var/lib/mysql
networks:
- backend
restart: unless-stopped
# 缓存服务
redis:
image: redis:7-alpine
container_name: my-redis
networks:
- backend
restart: unless-stopped
# 定义网络
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # 内部网络,不能访问外网
# 定义数据卷
volumes:
db-data:
driver: local
这个配置文件做了这些事:
- 定义了4个服务:web、app、db、redis
- 创建了2个网络:frontend(前端网络)和 backend(后端网络)
- 创建了1个数据卷:db-data(数据库持久化)
- web 依赖 app,app 依赖 db 和 redis
- 所有服务都设置了自动重启
16.8.4 docker-compose.yml 常用配置项
services:
servicename:
# 镜像
image: nginx:latest # 使用已有镜像
build: ./app # 从目录中的 Dockerfile 构建
build: # 详细构建配置
context: ./app
dockerfile: Dockerfile.prod
args:
VERSION: 1.0
# 容器名
container_name: my-nginx
# 端口映射
ports:
- "8080:80"
- "443:443"
# 环境变量
environment:
- KEY=value
- MYSQL_ROOT_PASSWORD=123456
# 或者用文件
env_file:
- .env
# 数据卷
volumes:
- ./html:/usr/share/nginx/html:ro
- nginx-data:/data
# 网络
networks:
- frontend
- backend
# 依赖关系(决定启动顺序)
depends_on:
- db
- redis
# 详细依赖配置(等待健康检查通过)
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
# 重启策略
restart: always
# 命令覆盖
command: ["python", "app.py", "--port", "5000"]
# 入口点覆盖
entrypoint: ["/app/start.sh"]
# 工作目录
working_dir: /app
# 用户
user: "1000:1000"
# 资源限制
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
memory: 256M
# 健康检查
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/"]
interval: 30s
timeout: 3s
retries: 3
# 日志配置
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
16.8.5 Docker Compose 常用命令
# 在 docker-compose.yml 所在目录启动所有服务(后台运行)
docker compose up -d
# 前台运行(可以看到日志输出,Ctrl+C 停止)
docker compose up
# 启动并重新构建镜像
docker compose up -d --build
# 停止并删除所有容器、网络(保留数据卷)
docker compose down
# 停止并删除所有容器、网络、数据卷(慎用,数据会丢失)
docker compose down -v
# 只停止不删除
docker compose stop
# 重新启动
docker compose start
# 查看服务状态
docker compose ps
# 查看日志
docker compose logs
docker compose logs -f web # 只看 web 服务的日志
# 重新构建镜像
docker compose build
# 拉取最新镜像
docker compose pull
# 重启某个服务
docker compose restart web
# 在运行中的服务里执行命令
docker compose exec web ls /etc/nginx
# 扩展服务实例数(启动多个副本)
docker compose up -d --scale app=3
# 查看配置(不执行)
docker compose config
16.8.6 一个完整的实战案例:WordPress 博客
WordPress 是最流行的博客系统,它需要 MySQL 数据库。用 Docker Compose 部署非常方便:
# docker-compose.yml
version: '3.8'
services:
wordpress:
image: wordpress:6.4-php8.2-apache
container_name: wordpress
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wpuser
WORDPRESS_DB_PASSWORD: wppass
WORDPRESS_DB_NAME: wordpress
volumes:
- wp-data:/var/www/html
depends_on:
db:
condition: service_healthy
networks:
- wp-net
restart: unless-stopped
db:
image: mysql:8.0
container_name: wordpress-db
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wpuser
MYSQL_PASSWORD: wppass
MYSQL_ROOT_PASSWORD: rootpass
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- wp-net
restart: unless-stopped
volumes:
wp-data:
db-data:
networks:
wp-net:
启动:
# 启动 WordPress
docker compose up -d
# 查看状态
docker compose ps
# 打开浏览器访问 http://localhost:8080 就能看到 WordPress 安装界面了
就这么简单,两条命令就能搭建一个完整的博客系统!
16.8.7 环境变量文件 .env
Docker Compose 会自动读取同目录下的 .env 文件,你可以在里面定义变量,然后在 yml 文件中引用:
# .env 文件
MYSQL_ROOT_PASSWORD=secret123
WORDPRESS_PORT=8080
# docker-compose.yml
version: '3.8'
services:
wordpress:
image: wordpress:latest
ports:
- "${WORDPRESS_PORT}:80"
environment:
WORDPRESS_DB_HOST: db
这样把敏感信息放在 .env 文件中(记得加入 .gitignore 不提交到代码仓库),配置文件里只引用变量名,更安全也更灵活。
16.8.8 Docker Compose 高级用法
1. 使用多个 Compose 文件覆盖配置
在开发和生产环境中,配置往往不同。Docker Compose 支持多个配置文件叠加:
# 基础配置文件 docker-compose.yml(通用配置)
# 开发覆盖文件 docker-compose.override.yml(开发环境特有配置)
# 生产覆盖文件 docker-compose.prod.yml(生产环境特有配置)
# 默认情况下,Compose 会自动加载 docker-compose.yml 和 docker-compose.override.yml
docker compose up -d
# 指定使用生产环境配置
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
示例 - 生产环境覆盖文件 docker-compose.prod.yml:
version: '3.8'
services:
web:
image: myapp:prod-v2.0 # 使用生产镜像
environment:
- FLASK_ENV=production
- DEBUG=False
deploy:
replicas: 3 # 生产环境启动3个副本
resources:
limits:
cpus: '2.0'
memory: 1G
restart: always # 生产环境总是重启
db:
environment:
MYSQL_ROOT_PASSWORD: ${PROD_DB_PASSWORD} # 使用生产密码
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
2. 使用 profiles 按需启动服务
Docker Compose 2.0+ 支持 profiles,可以按需启动不同的服务组合:
version: '3.8'
services:
web:
image: nginx
profiles: ["default", "prod"] # 默认和生产环境都启动
db:
image: mysql
profiles: ["prod"] # 只在生产环境启动
debug-tools:
image: nicolaka/netshoot
profiles: ["debug"] # 只在调试时启动
monitoring:
image: grafana/grafana
profiles: ["monitoring"] # 只在需要监控时启动
# 默认启动(只启动有 default profile 的服务)
docker compose up -d
# 启动生产环境
docker compose --profile prod up -d
# 启动调试环境
docker compose --profile debug up -d
# 启动包含监控的环境
docker compose --profile monitoring up -d
3. 扩展服务和模板复用
version: '3.8'
# 定义可复用的配置块
x-common-env: &common-env
TZ: Asia/Shanghai
LANG: C.UTF-8
x-common-healthcheck: &common-healthcheck
interval: 30s
timeout: 3s
retries: 3
services:
app1:
image: myapp:v1
environment:
<<: *common-env # 引用公共环境变量
APP_NAME: app1
healthcheck:
<<: *common-healthcheck # 引用公共健康检查配置
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
app2:
image: myapp:v1
environment:
<<: *common-env
APP_NAME: app2
healthcheck:
<<: *common-healthcheck
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
4. 使用 Compose 进行容器编排的最佳实践
version: '3.8'
services:
web:
image: nginx:1.25
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
app:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
interval: 30s
timeout: 3s
retries: 3
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
restart: unless-stopped
networks:
- frontend
app:
build:
context: ./app
dockerfile: Dockerfile
environment:
- DB_HOST=db
- REDIS_HOST=redis
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s # 给应用启动留出时间
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
restart: unless-stopped
networks:
- frontend
- backend
这个配置体现了生产级别的最佳实践:
- 使用明确的版本号而非 latest
- 配置健康检查和启动延迟
- 设置资源限制防止资源耗尽
- 配置日志轮转防止日志撑满磁盘
- 使用 depends_on 的条件等待确保启动顺序
- 前后端网络分离提高安全性
16.9 Docker 实战部署案例
16.9.1 案例一:部署 Nginx 静态网站
这是最简单的 Docker 部署案例,适合入门:
# 1. 准备网站文件
mkdir -p /data/website
cd /data/website
cat > index.html << 'EOF'
<!DOCTYPE html>
<html>
<head><title>My Docker Website</title></head>
<body>
<h1>Hello from Docker!</h1>
<p>This website is served by Nginx running in a Docker container.</p>
</body>
</html>
EOF
# 2. 启动 Nginx 容器
docker run -d \
--name my-website \
-p 80:80 \
-v /data/website:/usr/share/nginx/html:ro \
--restart unless-stopped \
nginx:1.25
# 3. 验证
curl http://localhost
# 或者用浏览器访问 http://你的服务器IP
进阶:自定义 Nginx 配置
# 创建自定义配置文件
cat > /data/website/nginx.conf << 'EOF'
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ =404;
}
location /health {
return 200 'OK';
add_header Content-Type text/plain;
}
}
EOF
# 启动容器,挂载配置文件
docker run -d \
--name my-website \
-p 80:80 \
-v /data/website:/usr/share/nginx/html:ro \
-v /data/website/nginx.conf:/etc/nginx/conf.d/default.conf:ro \
--restart unless-stopped \
nginx:1.25
16.9.2 案例二:部署 MySQL 数据库
# 1. 创建数据和配置目录
mkdir -p /data/mysql/data /data/mysql/conf /data/mysql/init
# 2. 创建自定义配置文件
cat > /data/mysql/conf/my.cnf << 'EOF'
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
max_connections=200
innodb_buffer_pool_size=256M
slow_query_log=1
slow_query_log_file=/var/lib/mysql/slow.log
long_query_time=2
EOF
# 3. 创建初始化 SQL 脚本(容器首次启动时自动执行)
cat > /data/mysql/init/init.sql << 'EOF'
CREATE DATABASE IF NOT EXISTS myapp DEFAULT CHARACTER SET utf8mb4;
CREATE USER IF NOT EXISTS 'appuser'@'%' IDENTIFIED BY 'apppass';
GRANT ALL PRIVILEGES ON myapp.* TO 'appuser'@'%';
FLUSH PRIVILEGES;
EOF
# 4. 启动 MySQL 容器
docker run -d \
--name mysql \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=Root@123456 \
-v /data/mysql/data:/var/lib/mysql \
-v /data/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro \
-v /data/mysql/init:/docker-entrypoint-initdb.d:ro \
--restart unless-stopped \
mysql:8.0
# 5. 验证连接
docker exec -it mysql mysql -uroot -p'Root@123456' -e "SHOW DATABASES;"
关于 MySQL 容器的几个要点:
/docker-entrypoint-initdb.d目录中的 SQL 脚本只在容器第一次启动(数据目录为空)时执行- 如果要修改密码或重新初始化,需要删除数据目录
- 生产环境要设置复杂的密码,并限制访问 IP
16.9.3 案例三:部署 Python Flask 应用
项目结构:
myflask/
├── app.py
├── requirements.txt
├── Dockerfile
└── docker-compose.yml
app.py:
from flask import Flask, jsonify
import os
import socket
import redis
app = Flask(__name__)
# 从环境变量读取 Redis 主机名
redis_host = os.getenv('REDIS_HOST', 'localhost')
redis_port = int(os.getenv('REDIS_PORT', 6379))
# 连接 Redis
try:
r = redis.Redis(host=redis_host, port=redis_port, decode_responses=True)
r.ping()
redis_connected = True
except:
redis_connected = False
@app.route('/')
def index():
hostname = socket.gethostname()
return jsonify({
'message': 'Hello from Flask!',
'hostname': hostname,
'redis_connected': redis_connected
})
@app.route('/visit')
def visit():
if redis_connected:
count = r.incr('visit_count')
return jsonify({'visit_count': count})
return jsonify({'error': 'Redis not connected'}), 500
@app.route('/health')
def health():
return jsonify({'status': 'healthy'}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
requirements.txt:
flask==3.0.0
redis==5.0.1
gunicorn==21.2.0
Dockerfile:
FROM python:3.11-slim
LABEL maintainer="linuxlearner@example.com"
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd -r -s /bin/false appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')" || exit 1
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "3", "app:app"]
docker-compose.yml:
version: '3.8'
services:
web:
build: .
ports:
- "5000:5000"
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
- redis
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
restart: unless-stopped
volumes:
redis-data:
部署:
# 构建并启动
docker compose up -d --build
# 查看状态
docker compose ps
# 测试
curl http://localhost:5000/
curl http://localhost:5000/visit
# 查看日志
docker compose logs -f web
# 停止
docker compose down
16.9.4 案例四:部署 Node.js Express 应用
项目结构:
mynode/
├── app.js
├── package.json
├── Dockerfile
└── .dockerignore
package.json:
{
"name": "my-node-app",
"version": "1.0.0",
"main": "app.js",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
app.js:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({ message: 'Hello from Node.js Docker!', timestamp: new Date() });
});
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Dockerfile:
# 使用多阶段构建
# 阶段1:安装依赖
FROM node:18-alpine AS dependencies
WORKDIR /app
COPY package.json ./
RUN npm install --production
# 阶段2:运行应用
FROM node:18-alpine
WORKDIR /app
# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# 从依赖阶段拷贝 node_modules
COPY --from=dependencies /app/node_modules ./node_modules
# 拷贝应用代码
COPY --chown=nodejs:nodejs . .
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "app.js"]
.dockerignore:
node_modules
npm-debug.log
Dockerfile
.git
构建和运行:
# 构建镜像
docker build -t mynodeapp:v1.0 .
# 运行容器
docker run -d --name node-app -p 3000:3000 mynodeapp:v1.0
# 测试
curl http://localhost:3000/
16.9.5 案例五:用 Compose 部署完整 LAMP 架构
LAMP = Linux + Apache + MySQL + PHP,是经典的 Web 开发架构:
version: '3.8'
services:
# Apache + PHP
web:
image: php:8.2-apache
container_name: lamp-web
ports:
- "8080:80"
volumes:
- ./www:/var/www/html
- ./php.ini:/usr/local/etc/php/php.ini:ro
environment:
- MYSQL_HOST=db
- MYSQL_DATABASE=lampdb
- MYSQL_USER=lampuser
- MYSQL_PASSWORD=lamppass
depends_on:
db:
condition: service_healthy
networks:
- lamp-net
restart: unless-stopped
# MySQL 数据库
db:
image: mysql:8.0
container_name: lamp-db
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: lampdb
MYSQL_USER: lampuser
MYSQL_PASSWORD: lamppass
volumes:
- db-data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-prootpass"]
interval: 10s
timeout: 5s
retries: 5
networks:
- lamp-net
restart: unless-stopped
# phpMyAdmin 数据库管理工具
phpmyadmin:
image: phpmyadmin:5.2
container_name: lamp-phpmyadmin
ports:
- "8081:80"
environment:
PMA_HOST: db
PMA_USER: root
PMA_PASSWORD: rootpass
depends_on:
- db
networks:
- lamp-net
restart: unless-stopped
volumes:
db-data:
networks:
lamp-net:
创建测试 PHP 页面 www/index.php:
<?php
$host = getenv('MYSQL_HOST');
$dbname = getenv('MYSQL_DATABASE');
$user = getenv('MYSQL_USER');
$pass = getenv('MYSQL_PASSWORD');
try {
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "<h1>LAMP 环境搭建成功!</h1>";
echo "<p>MySQL 连接成功</p>";
echo "<p>PHP 版本:" . phpversion() . "</p>";
// 创建测试表
$pdo->exec("CREATE TABLE IF NOT EXISTS test (id INT AUTO_INCREMENT PRIMARY KEY, msg VARCHAR(255))");
$pdo->exec("INSERT INTO test (msg) VALUES ('Hello LAMP!')");
$stmt = $pdo->query("SELECT * FROM test");
echo "<h2>数据库内容:</h2><ul>";
while ($row = $stmt->fetch()) {
echo "<li>" . $row['id'] . ": " . $row['msg'] . "</li>";
}
echo "</ul>";
} catch (PDOException $e) {
echo "数据库连接失败:" . $e->getMessage();
}
?>
启动:
# 启动所有服务
docker compose up -d
# 访问网站:http://localhost:8080
# 访问 phpMyAdmin:http://localhost:8081
16.9.6 部署最佳实践
- 使用明确的版本号:不要用
latest,用nginx:1.25.3这样明确的版本 - 使用健康检查:让 Docker 知道容器是否真正可用
- 设置资源限制:防止某个容器吃光资源
- 配置日志轮转:防止日志文件无限增长
- 使用 .env 文件:管理敏感信息
- 多阶段构建:减小镜像体积
- 使用非 root 用户:提高安全性
16.9.7 Docker 性能优化建议
在生产环境中运行 Docker 时,性能优化非常重要。以下是一些实用的优化建议:
1. 合理分配资源
不要给容器分配过多的资源,也不要分配太少。根据应用的实际需求来设定资源限制。可以通过 docker stats 观察容器的资源使用情况,逐步调整到最佳值。
2. 使用合适的存储驱动
Docker 默认使用 overlay2 存储驱动,它在大多数场景下性能最好。如果你的系统支持 overlay2,就不要换成其他的。可以通过 docker info | grep Storage 查看当前使用的存储驱动。
3. 优化镜像大小
镜像越小,拉取和启动就越快。使用 alpine 或 slim 基础镜像,合并 RUN 指令,清理缓存文件,使用多阶段构建。这些都能有效减小镜像体积。
4. 合理使用数据卷
频繁读写的数据应该放在数据卷中,而不是容器的可写层。因为容器可写层使用的是联合文件系统,性能不如直接读写数据卷。特别是数据库的数据文件,一定要放在数据卷中。
5. 网络模式选择
对网络性能要求极高的场景,可以考虑使用 host 网络模式,省去虚拟网络的开销。但要注意安全隔离问题。一般场景下 bridge 模式就够了。
6. 开启构建缓存
在 CI/CD 流水线中,利用 Docker 的构建缓存可以大幅加快构建速度。把不常变化的步骤放前面,常变化的放后面。
7. 日志配置优化
容器日志如果配置不当,会占用大量磁盘空间和 I/O。建议配置日志轮转,限制单个日志文件大小和文件数量。
16.9.8 Docker 常见面试题精选
学习和面试是分不开的,这里整理了一些 Docker 相关的常见面试题,帮助你巩固知识:
问题一:Docker 和虚拟机的区别是什么?
答:主要区别在于隔离级别和资源占用。虚拟机是硬件级隔离,每个虚拟机都有完整的操作系统,占用资源多、启动慢。Docker 容器是进程级隔离,共享宿主机内核,占用资源少、启动快。虚拟机适合运行不同操作系统的场景,容器适合快速部署和弹性伸缩的场景。
问题二:Docker 镜像是怎么实现分层的?
答:Docker 镜像使用联合文件系统(UnionFS)实现分层存储。每条 Dockerfile 指令都会创建一个新的只读层,多层叠加构成完整镜像。当容器运行时,在最上层添加一个可写层。修改文件时使用写时复制机制,只有被修改的文件才会被复制到可写层。这种设计节省存储空间,加快构建速度。
问题三:CMD 和 ENTRYPOINT 有什么区别?
答:CMD 设置容器启动的默认命令,可以被 docker run 的参数覆盖。ENTRYPOINT 设置容器的入口程序,不会被 docker run 的参数覆盖,而是把参数追加到 ENTRYPOINT 后面。最佳实践是两者配合使用:ENTRYPOINT 固定主命令,CMD 提供默认参数。
问题四:如何减小 Docker 镜像体积?
答:方法包括:使用 alpine 或 slim 基础镜像;合并多条 RUN 指令减少层数;安装软件后清理缓存;使用多阶段构建只保留最终产物;使用 .dockerignore 排除不必要文件;选择静态编译的语言如 Go 可以用 scratch 作为基础镜像。
问题五:Docker 数据持久化有哪些方式?
答:主要有三种方式。Volume(数据卷)由 Docker 管理,推荐用于生产环境。Bind Mount(绑定挂载)直接挂载宿主机目录,适合开发环境。tmpfs 存储在内存中,适合临时和敏感数据。生产环境的数据一定要持久化到数据卷或绑定挂载中,不能存在容器的可写层里。
问题六:Docker 容器之间如何通信?
答:有多种方式。同一主机上的容器可以通过自定义网络(bridge 模式)用容器名互相访问,Docker 内置了 DNS 解析。跨主机的容器可以通过 overlay 网络通信。也可以通过端口映射让容器通过宿主机端口通信。生产环境推荐使用自定义网络并做好网络隔离。
问题七:什么是容器逃逸?如何防范?
答:容器逃逸是指攻击者从容器内部突破隔离,获取宿主机权限。防范措施包括:不以特权模式运行容器;不挂载 Docker Socket 和宿主机敏感目录;以非 root 用户运行容器;移除不必要的 capabilities;启用用户命名空间映射;保持 Docker 和内核更新修复已知漏洞。
16.10 容器安全基础
16.10.1 为什么容器安全很重要
容器虽然提供了隔离,但这种隔离不像虚拟机那样是硬件级的。如果容器被攻破,攻击者有可能逃逸到宿主机,造成更大的破坏。
打个比方:虚拟机的安全就像银行金库,有厚厚的钢板墙;容器的安全就像办公室的隔间,有隔板但不是坚不可摧。所以我们需要额外加固容器的安全性。
16.10.2 容器安全的几个层面
1. 镜像安全
使用不安全的镜像是最大的风险之一。你从 Docker Hub 拉取的镜像可能包含恶意代码或已知漏洞。
# 只使用官方镜像或可信来源的镜像
# 官方镜像有 [OK] 标记
docker search nginx
# 验证镜像的签名(如果有的话)
docker trust inspect nginx:latest
# 使用镜像扫描工具检查漏洞
# Docker 自带了扫描功能(需要 Docker Desktop 或 Docker Scout)
docker scout cves myimage:latest
镜像安全最佳实践:
- 优先使用官方镜像
- 定期更新基础镜像
- 使用镜像扫描工具检查漏洞
- 不使用来历不明的镜像
- 构建时验证下载文件的校验和
2. 最小权限原则
容器应该只拥有它运行所需的最小权限,不多给一点。
# 以非 root 用户运行容器
docker run -d --user 1000:1000 nginx
# 在 Dockerfile 中创建并切换用户
# (见前面 Dockerfile 章节)
# 只读文件系统(容器内文件不可修改)
docker run -d --read-only nginx
# 如果只读模式下需要写临时文件,用 tmpfs
docker run -d --read-only --tmpfs /tmp nginx
3. 限制容器能力
Linux 有一种权限控制机制叫 capabilities。默认情况下,Docker 会给容器一些能力,但很多是不必要的。我们可以移除所有能力,只添加需要的。
# 移除所有能力,然后只添加需要的
docker run -d --cap-drop ALL --cap-add NET_BIND_SERVICE nginx
# 常见的能力:
# NET_BIND_SERVICE:绑定小于1024的端口
# CHOWN:修改文件所有者
# SETUID/SETGID:切换用户身份
# SYS_ADMIN:系统管理(很危险,不要给)
# 禁止容器获取新权限
docker run -d --security-opt no-new-privileges nginx
4. 资源限制
防止容器消耗过多资源导致宿主机崩溃(DoS 攻击):
# 限制内存和 CPU
docker run -d \
--memory=512m \
--memory-swap=512m \
--memory-reservation=256m \
--cpus=1.0 \
--cpu-shares=512 \
--pids-limit=100 \
nginx
# 在 docker-compose.yml 中
services:
web:
image: nginx
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
5. 网络安全
# 不要把不需要外网访问的容器暴露到公网
# 数据库容器只加入内部网络,不映射端口
# 限制容器的网络访问
# 使用内部网络(不能访问外网)
docker network create --internal secure-net
# 把数据库容器放到内部网络
docker run -d --network secure-net mysql
# docker-compose.yml 中的网络安全
services:
db:
image: mysql:8.0
networks:
- internal # 只在内部网络,不暴露端口
# 不写 ports,外部完全访问不到
web:
image: nginx
ports:
- "80:80"
networks:
- internal
- external
networks:
internal:
internal: true # 标记为内部网络
external:
driver: bridge
6. 防止容器逃逸
容器逃逸是指攻击者从容器内部突破到宿主机。以下措施可以降低风险:
# 不要以 --privileged 模式运行容器(这相当于给容器 root 权限)
# 错误做法:
docker run --privileged nginx # 非常危险!
# 不要挂载 Docker Socket(这等于把宿主机的控制权交给容器)
# 错误做法:
docker run -v /var/run/docker.sock:/var/run/docker.sock some-image # 危险!
# 不要挂载宿主机的敏感目录
# 错误做法:
docker run -v /:/host some-image # 极其危险!
# 使用用户命名空间映射(让容器内的 root 映射为宿主机的普通用户)
# 在 /etc/docker/daemon.json 中配置:
# {
# "userns-remap": "default"
# }
16.10.3 使用 Docker Bench 进行安全检查
Docker Bench 是一个开源的安全检查脚本,它会按照 CIS Docker Benchmark 标准检查你的 Docker 配置:
# 运行 Docker Bench Security 检查
docker run --rm --net host --pid host --userns host --cap-add audit_control \
-e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
-v /var/lib:/var/lib:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /etc:/etc:ro \
--label docker_bench_security \
docker/docker-bench-security
它会输出一系列检查项,告诉你哪些配置是安全的(PASS),哪些需要改进(WARN)。
16.10.4 镜像签名与内容信任
Docker Content Trust(DCT)可以验证镜像的完整性和来源,防止使用被篡改的镜像:
# 启用内容信任(全局)
export DOCKER_CONTENT_TRUST=1
# 启用后,docker pull 只会拉取有签名的镜像
docker pull nginx:latest
# 在 daemon.json 中永久启用
# /etc/docker/daemon.json
# {
# "content-trust": true
# }
16.10.5 安全最佳实践总结
-
镜像层面:
- 使用官方镜像或可信镜像
- 定期更新基础镜像修复漏洞
- 使用镜像扫描工具
- 使用多阶段构建减小攻击面
-
容器层面:
- 以非 root 用户运行
- 移除不必要的 capabilities
- 设置资源限制
- 使用只读文件系统
- 禁止获取新权限
-
网络层面:
- 不暴露不必要的端口
- 使用内部网络隔离敏感服务
- 限制容器间的网络访问
-
宿主机层面:
- 保持 Docker 引擎更新
- 不要使用 --privileged
- 不要挂载敏感目录和 Docker Socket
- 启用用户命名空间隔离
-
运维层面:
- 定期安全扫描
- 监控容器行为
- 及时更新修复漏洞
- 使用最小权限原则
- 做好日志收集和审计
16.10.6 一个安全加固的 Dockerfile 示例
# 使用官方 slim 镜像
FROM python:3.11-slim
# 安装安全更新
RUN apt-get update && apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# 创建非 root 用户
RUN groupadd -r appuser && useradd -r -g appuser -s /bin/false appuser
WORKDIR /app
# 安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 拷贝应用代码
COPY --chown=appuser:appuser . .
# 切换用户
USER appuser
# 设置安全相关的环境变量
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PYTHONHASHSEED=random
EXPOSE 5000
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 --start-period=10s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')" || exit 1
# 不以 root 运行
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "3", "--user", "appuser", "app:app"]
运行时的安全加固:
docker run -d \
--name secure-app \
--user 1000:1000 \
--read-only \
--tmpfs /tmp \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges \
--memory=512m \
--cpus=1.0 \
--pids-limit=100 \
--network internal-net \
-p 5000:5000 \
myapp:secure
16.10.7 常见的容器安全误区
在学习容器安全的过程中,很多初学者会有一些误解。下面我们来澄清几个常见的误区:
误区一:“容器和虚拟机一样安全”
这是最危险的误区。虚拟机有硬件级的隔离(Hypervisor 层),而容器只是进程级的隔离(Namespace)。如果容器内的攻击者利用内核漏洞,有可能逃逸到宿主机。所以容器的隔离性不如虚拟机,需要额外的安全加固。
误区二:“用了官方镜像就一定安全”
官方镜像虽然比个人镜像更可靠,但也不是绝对安全。官方镜像也可能包含有漏洞的软件包。你需要定期更新镜像,并使用镜像扫描工具检查漏洞。就像你买的大品牌食品也可能有过期的时候,还是要检查保质期。
误区三:“容器以 root 运行没关系”
很多默认的 Docker 镜像都以 root 用户运行。如果攻击者攻破了容器,就获得了容器内的 root 权限。虽然容器内的 root 不等于宿主机的 root,但如果存在容器逃逸漏洞,root 权限会让攻击者更容易突破隔离。所以一定要以非 root 用户运行应用。
误区四:“内部网络就完全安全了”
Docker 的内部网络(internal: true)只是阻止了容器访问外部网络,但同一网络内的容器之间仍然可以互相访问。如果一个容器被攻破,攻击者可以攻击同一网络内的其他容器。所以还需要在应用层面做好认证和授权。
误区五:“设置了资源限制就不会被 DoS 攻击”
资源限制确实能防止单个容器耗尽宿主机资源,但攻击者仍然可以通过其他方式造成影响,比如大量创建网络连接、占用文件描述符等。资源限制是必要的,但不是充分的防护措施。
16.10.8 Docker 安全审计
在生产环境中,你需要定期对 Docker 环境进行安全审计,确保配置符合安全标准。
1. 审计 Docker 守护进程配置
# 检查 daemon.json 配置
sudo cat /etc/docker/daemon.json
# 确保以下安全配置已启用:
# - "icc": false 禁止容器间默认通信
# - "no-new-privileges": true 禁止提权
# - "userland-proxy": false 禁用用户态代理
# - "live-restore": true Docker 重启时容器不中断
2. 审计容器配置
# 检查所有运行中容器的安全配置
docker ps -q | xargs docker inspect --format \
'Container: {{.Name}}
User: {{.Config.User}}
Privileged: {{.HostConfig.Privileged}}
ReadOnly: {{.HostConfig.ReadonlyRootfs}}
CapAdd: {{.HostConfig.CapAdd}}
CapDrop: {{.HostConfig.CapDrop}}'
3. 定期镜像漏洞扫描
# 使用 Trivy 扫描镜像漏洞(开源工具)
# 安装 Trivy
sudo apt install trivy
# 扫描镜像
trivy image nginx:1.25
# 只显示高危和严重漏洞
trivy image --severity HIGH,CRITICAL nginx:1.25
# 扫描结果会列出:
# - 漏洞编号(CVE)
# - 严重程度
# - 影响的软件包
# - 修复建议
4. 日志审计
# 查看 Docker 守护进程日志
sudo journalctl -u docker.service
# 查看容器操作审计日志
# 需要在 auditd 中配置 Docker 审计规则
# /etc/audit/rules.d/audit.rules
# -w /usr/bin/docker -p wa
# -w /var/lib/docker -p wa
# -w /etc/docker -p wa
# 查看审计日志
sudo ausearch -k docker
16.10.9 生产环境安全检查清单
在将容器部署到生产环境之前,请对照以下清单逐项检查:
- 基础镜像来源可信(官方镜像或经过审查的镜像)
- 镜像已通过漏洞扫描
- 镜像使用了明确的版本号(非 latest)
- Dockerfile 中使用了非 root 用户
- 容器运行时使用了 --user 参数指定用户
- 移除了不必要的 capabilities(–cap-drop ALL)
- 设置了内存和 CPU 资源限制
- 配置了只读文件系统(–read-only)
- 配置了日志轮转(防止日志撑满磁盘)
- 数据库等敏感服务不暴露到公网
- 敏感信息(密码、密钥)不在 Dockerfile 和镜像中
- 使用了 Docker Secret 或环境变量管理敏感信息
- 配置了健康检查
- 设置了合理的重启策略
- Docker 守护进程配置已加固
- 定期更新基础镜像和 Docker 引擎
- 配置了审计日志
附录一:Docker 常用命令速查表
为了方便日常使用,这里整理了 Docker 最常用的命令,按功能分类:
镜像相关命令:
| 命令 | 说明 | 示例 |
|---|---|---|
| docker pull | 拉取镜像 | docker pull nginx:1.25 |
| docker images | 列出本地镜像 | docker images |
| docker rmi | 删除镜像 | docker rmi nginx:1.25 |
| docker build | 构建镜像 | docker build -t myapp:v1 . |
| docker tag | 给镜像打标签 | docker tag myapp:v1 myapp:latest |
| docker push | 推送镜像 | docker push username/myapp:v1 |
| docker save | 导出镜像 | docker save -o app.tar myapp:v1 |
| docker load | 导入镜像 | docker load -i app.tar |
| docker history | 查看镜像历史 | docker history nginx:1.25 |
| docker inspect | 查看镜像详情 | docker inspect nginx:1.25 |
| docker image prune | 清理悬空镜像 | docker image prune -a |
容器相关命令:
| 命令 | 说明 | 示例 |
|---|---|---|
| docker run | 创建并启动容器 | docker run -d --name web -p 80:80 nginx |
| docker ps | 查看运行中的容器 | docker ps -a |
| docker start | 启动容器 | docker start web |
| docker stop | 停止容器 | docker stop web |
| docker restart | 重启容器 | docker restart web |
| docker rm | 删除容器 | docker rm web |
| docker exec | 进入容器执行命令 | docker exec -it web /bin/bash |
| docker logs | 查看容器日志 | docker logs -f --tail 100 web |
| docker stats | 查看资源使用 | docker stats |
| docker top | 查看容器进程 | docker top web |
| docker inspect | 查看容器详情 | docker inspect web |
| docker cp | 拷贝文件 | docker cp web:/etc/nginx.conf . |
| docker commit | 提交为镜像 | docker commit web myapp:v2 |
网络相关命令:
| 命令 | 说明 | 示例 |
|---|---|---|
| docker network ls | 列出网络 | docker network ls |
| docker network create | 创建网络 | docker network create mynet |
| docker network connect | 连接网络 | docker network connect mynet web |
| docker network disconnect | 断开网络 | docker network disconnect mynet web |
| docker network rm | 删除网络 | docker network rm mynet |
| docker network inspect | 查看网络详情 | docker network inspect mynet |
数据卷相关命令:
| 命令 | 说明 | 示例 |
|---|---|---|
| docker volume ls | 列出数据卷 | docker volume ls |
| docker volume create | 创建数据卷 | docker volume create mydata |
| docker volume inspect | 查看数据卷详情 | docker volume inspect mydata |
| docker volume rm | 删除数据卷 | docker volume rm mydata |
| docker volume prune | 清理无用数据卷 | docker volume prune |
系统相关命令:
| 命令 | 说明 | 示例 |
|---|---|---|
| docker info | 查看 Docker 信息 | docker info |
| docker version | 查看 Docker 版本 | docker version |
| docker system df | 查看磁盘使用 | docker system df |
| docker system prune | 清理所有无用资源 | docker system prune -a |
| docker system events | 查看实时事件 | docker system events |
Docker Compose 常用命令:
| 命令 | 说明 | 示例 |
|---|---|---|
| docker compose up | 启动所有服务 | docker compose up -d |
| docker compose down | 停止并删除所有服务 | docker compose down |
| docker compose ps | 查看服务状态 | docker compose ps |
| docker compose logs | 查看日志 | docker compose logs -f web |
| docker compose build | 重新构建镜像 | docker compose build |
| docker compose restart | 重启服务 | docker compose restart web |
| docker compose exec | 执行命令 | docker compose exec web bash |
| docker compose pull | 拉取最新镜像 | docker compose pull |
附录二:Docker 学习建议和进阶路线
给初学者的学习建议:
-
先动手再理论:不要一上来就啃文档,先跟着教程把 Docker 装上,把 hello-world 跑起来,有了感性认识再去学理论。
-
循序渐进:按照本章的顺序学习,先理解概念,再学基本命令,然后学 Dockerfile,最后学 Compose 和安全。不要跳着学。
-
多动手实践:每学一个命令就亲手敲一遍,每学一个概念就找一个例子验证。光看不练等于白学。
-
善用帮助命令:遇到不会的命令,用
docker --help、docker run --help查看帮助文档,比百度还快。 -
养成好习惯:从一开始就遵循最佳实践,比如用明确的版本号、用非 root 用户、写 .dockerignore 等。
-
不要怕报错:报错是最好的老师。遇到错误仔细阅读错误信息,大部分问题错误信息已经告诉你原因了。
进阶学习路线:
- Docker 进阶:学习 Docker Swarm 集群、Docker Secret 管理密钥、Docker 多平台构建等。
- Kubernetes:学习 Pod、Deployment、Service、Ingress 等概念,掌握集群容器编排。
- CI/CD:学习 Jenkins、GitLab CI、GitHub Actions,把 Docker 集成到持续集成流水线中。
- 监控告警:学习 Prometheus + Grafana 监控 Docker 容器,配置告警规则。
- 日志收集:学习 ELK(Elasticsearch + Logstash + Kibana)或 Loki 收集容器日志。
- 服务网格:学习 Istio、Linkerd 等服务网格技术,实现微服务的流量管理和安全。
- 云原生:学习 CNCF(云原生计算基金会)生态中的各种工具和项目。
Docker 只是云原生世界的入口,学好了 Docker,你就拿到了通往云原生世界的钥匙。祝你学习顺利,早日成为容器技术高手!
本章总结
通过本章的学习,我们从一个对容器一无所知的小白,逐步掌握了 Docker 容器技术的方方面面。让我们回顾一下本章的主要内容:
-
容器技术概述:理解了容器就是软件世界的"集装箱",解决了"在我机器上能跑"的问题。容器比虚拟机更轻量、更快速。
-
Docker 安装与配置:学会了在 Ubuntu、CentOS 上安装 Docker,配置镜像加速器,以及验证安装。
-
镜像与仓库:掌握了镜像的拉取、查看、删除、导出导入、打标签等操作,了解了 Docker Hub 和私有仓库的使用。
-
Dockerfile 编写:学会了用 Dockerfile 这份"菜谱"来构建镜像,掌握了所有常用指令,以及多阶段构建等高级技巧。
-
容器生命周期管理:全面掌握了容器的创建、启动、停止、删除、进入、查看日志、查看资源使用等管理操作。
-
网络配置:理解了 Docker 的网络模式,学会了创建自定义网络、端口映射、容器间通信。
-
数据卷与持久化:学会了用 Volume、Bind Mount、tmpfs 三种方式实现数据持久化,以及数据卷的备份恢复。
-
Docker Compose:掌握了用 YAML 文件编排多容器应用,实现了一键启动复杂架构。
-
实战部署案例:通过 Nginx、MySQL、Python Flask、Node.js、LAMP 等案例,将所学知识融会贯通。
-
容器安全基础:了解了容器安全的各个层面,学会了镜像安全、最小权限、资源限制、网络隔离等安全加固措施。
Docker 的学习是一个循序渐进的过程,建议你按照本章的顺序,一边阅读一边动手实践。把每个命令都敲一遍,把每个 Dockerfile 都构建一次,把每个案例都跑起来。只有动手做了,才能真正理解和掌握。祝你学习愉快!
容器技术是现代软件工程中不可或缺的一项技能。从开发到测试,从部署到运维,Docker 都在发挥着越来越重要的作用。掌握了 Docker,你不仅能提高自己的开发效率,还能为后续学习 Kubernetes 和云原生技术打下坚实的基础。
回顾本章,我们首先通过搬家的类比理解了容器的本质——软件世界的标准化集装箱。然后从安装配置开始,一步步学习了镜像管理、Dockerfile 编写、容器管理、网络配置、数据持久化、多容器编排、实战部署和安全加固。每一个知识点都配上了大量的命令示例和配置文件,方便你直接参考和使用。
希望本章的内容能帮助你顺利入门 Docker 容器技术。记住,技术学习最重要的是实践,纸上得来终觉浅,绝知此事要躬行。快去打开你的终端,开始 Docker 之旅吧!
下章预告:在第十七章中,我们将学习 Kubernetes 容器编排,它是管理大规模容器集群的利器。如果说 Docker 是集装箱,那 Kubernetes 就是管理成千上万个集装箱的港口调度系统。敬请期待!
更多推荐
所有评论(0)