第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 包含三个核心概念:

  1. 镜像(Image):类似于面向对象编程中的"类",是一个只读的模板,包含了运行应用所需的所有内容(代码、库、环境变量、配置文件)。你可以把它想象成一张光盘(CD),里面刻录好了程序,但是不能修改。

  2. 容器(Container):类似于面向对象编程中的"对象",是镜像运行起来的实例。你可以把它想象成把光盘里的内容加载到内存中运行的程序。一个镜像可以同时创建多个容器,就像一个类可以创建多个对象。

  3. 仓库(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 容器技术的应用场景

容器能用来做什么?这里列举一些常见场景:

  1. 开发环境统一:团队所有成员使用相同的开发环境镜像,告别"在我电脑上能跑"的问题。
  2. 微服务架构:每个微服务打包成一个容器,独立部署、独立扩展。
  3. CI/CD 持续集成:在容器中编译、测试、打包代码,保证环境一致性。
  4. 快速部署和回滚:容器启动只需几秒,出问题了秒级回滚到上一个版本。
  5. 混合云部署:同一个镜像可以部署在阿里云、腾讯云、AWS 任何云上。
  6. 一次性任务:用容器跑一些一次性的数据处理任务,跑完就删。

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 命令手动管理就够了。但当容器数量达到几十、几百甚至上千个的时候,手动管理就变成了噩梦:

  • 某台机器宕机了,上面的容器怎么办?
  • 流量突然增大,怎么自动扩容?
  • 怎么做到滚动更新而不中断服务?
  • 怎么确保总是有一定数量的容器在运行?

这就是容器编排工具要解决的问题。目前主流的容器编排工具有:

  1. Kubernetes(K8s):Google 开源,目前是容器编排领域的事实标准,功能最强大,社区最活跃。
  2. Docker Swarm:Docker 自带的编排工具,简单易用但功能有限。
  3. Apache Mesos:更通用的集群管理工具,也可以用来编排容器。

Docker Compose 主要用于单机上的多容器编排,而 Kubernetes 和 Docker Swarm 则用于跨多台机器的集群编排。在后面的章节中我们会学习 Docker Compose,Kubernetes 则留到下一章。


16.2 Docker 安装与配置

16.2.1 安装前的准备

在安装 Docker 之前,我们需要了解一些前提条件:

  1. Docker 原生运行在 Linux 上(因为它依赖 Linux 内核的 Namespace 和 Cgroup),所以推荐在 Linux 系统上安装。
  2. 如果你用的是 Windows 或 macOS,Docker 提供了 Docker Desktop,它会在后台启动一个轻量级的 Linux 虚拟机来运行 Docker。
  3. 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.
...

这条命令的执行过程是这样的:

  1. Docker 在本地查找 hello-world 镜像,没找到
  2. Docker 自动去 Docker Hub 仓库拉取 hello-world 镜像
  3. 用这个镜像创建并运行一个容器
  4. 容器输出一段欢迎信息后自动退出

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:

  1. 去 Docker 官网下载 Docker Desktop 安装包
  2. Windows 版本要求 Windows 10 64位专业版/企业版/教育版(Build 19041 以上),并且需要开启 WSL 2(Windows Subsystem for Linux 2)
  3. 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)

这样做的好处是:

  1. 节省空间:多个镜像如果共享相同的底层,磁盘上只存一份
  2. 构建速度快:每一层都有缓存,如果某层没变,构建时直接用缓存
  3. 方便分发:拉取镜像时,已有的层不会重复下载

16.3.2 Docker Hub:镜像的应用商店

Docker Hub 是 Docker 官方的公共镜像仓库,地址是 hub.docker.com。它就像手机上的应用商店,你可以在上面搜索、下载各种镜像。

Docker Hub 上的镜像分为两类:

  • 官方镜像(Official Images):由 Docker 官方维护,名字不带斜杠,如 nginxmysqlredispython。这些镜像质量高、更新及时,推荐使用。
  • 个人/组织镜像:由个人或组织上传,名字带斜杠,如 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 savedocker load 保存的是完整的镜像(包含所有层和元数据),适合离线传输。另外还有 docker exportdocker 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 类比成一样东西,那它就是一份菜谱

想象你要做一道菜——红烧肉。菜谱上写着:

  1. 准备五花肉 500 克
  2. 切成 3 厘米见方的块
  3. 冷水下锅焯水
  4. 热锅放冰糖炒糖色
  5. 放入肉块翻炒上色
  6. 加入酱油、料酒、八角
  7. 小火炖 40 分钟
  8. 大火收汁

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)
  • 追求兼容性和稳定性:用 ubuntudebian
  • 运行特定语言:用官方语言镜像如 pythonnodegolang
  • 生产环境:用 -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 最佳实践

  1. 选择小的基础镜像:优先用 alpine 或 slim 版本
  2. 合并 RUN 指令:减少镜像层数
  3. 清理缓存:安装软件后清理包管理器缓存
  4. 合理利用缓存:把不常变化的指令放前面,常变化的放后面
  5. 使用 .dockerignore:避免不必要的文件进入构建上下文
  6. 不要存敏感信息:密码、密钥等不要写在 Dockerfile 里
  7. 使用非 root 用户:提高安全性
  8. 使用多阶段构建:减小最终镜像体积

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:限制最多用半个 CPU
  • nginx: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

stopkill 的区别: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_minskyeloquent_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 数据卷管理的最佳实践

  1. 给数据卷起有意义的名字:不要用默认的匿名卷,用 docker volume create mysql-prod-data 这样明确的名字。

  2. 定期备份:重要数据一定要定期备份,不要等数据丢了才后悔。

  3. 不要在容器中存储重要数据:容器是临时的,重要数据一定要放在数据卷或绑定挂载中。

  4. 使用命名卷而非匿名卷:命名卷更容易管理、备份和识别。

  5. 生产环境用数据卷,开发环境用绑定挂载:开发时需要直接修改文件,用绑定挂载更方便;生产环境用数据卷更安全、性能更好。

  6. 清理无用数据卷:定期用 docker volume prune 清理不再使用的数据卷,释放空间。但清理前一定要确认数据不再需要。

  7. 数据卷的权限问题:如果容器内以非 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

这个配置文件做了这些事:

  1. 定义了4个服务:web、app、db、redis
  2. 创建了2个网络:frontend(前端网络)和 backend(后端网络)
  3. 创建了1个数据卷:db-data(数据库持久化)
  4. web 依赖 app,app 依赖 db 和 redis
  5. 所有服务都设置了自动重启

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 部署最佳实践

  1. 使用明确的版本号:不要用 latest,用 nginx:1.25.3 这样明确的版本
  2. 使用健康检查:让 Docker 知道容器是否真正可用
  3. 设置资源限制:防止某个容器吃光资源
  4. 配置日志轮转:防止日志文件无限增长
  5. 使用 .env 文件:管理敏感信息
  6. 多阶段构建:减小镜像体积
  7. 使用非 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 安全最佳实践总结

  1. 镜像层面

    • 使用官方镜像或可信镜像
    • 定期更新基础镜像修复漏洞
    • 使用镜像扫描工具
    • 使用多阶段构建减小攻击面
  2. 容器层面

    • 以非 root 用户运行
    • 移除不必要的 capabilities
    • 设置资源限制
    • 使用只读文件系统
    • 禁止获取新权限
  3. 网络层面

    • 不暴露不必要的端口
    • 使用内部网络隔离敏感服务
    • 限制容器间的网络访问
  4. 宿主机层面

    • 保持 Docker 引擎更新
    • 不要使用 --privileged
    • 不要挂载敏感目录和 Docker Socket
    • 启用用户命名空间隔离
  5. 运维层面

    • 定期安全扫描
    • 监控容器行为
    • 及时更新修复漏洞
    • 使用最小权限原则
    • 做好日志收集和审计

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 学习建议和进阶路线

给初学者的学习建议:

  1. 先动手再理论:不要一上来就啃文档,先跟着教程把 Docker 装上,把 hello-world 跑起来,有了感性认识再去学理论。

  2. 循序渐进:按照本章的顺序学习,先理解概念,再学基本命令,然后学 Dockerfile,最后学 Compose 和安全。不要跳着学。

  3. 多动手实践:每学一个命令就亲手敲一遍,每学一个概念就找一个例子验证。光看不练等于白学。

  4. 善用帮助命令:遇到不会的命令,用 docker --helpdocker run --help 查看帮助文档,比百度还快。

  5. 养成好习惯:从一开始就遵循最佳实践,比如用明确的版本号、用非 root 用户、写 .dockerignore 等。

  6. 不要怕报错:报错是最好的老师。遇到错误仔细阅读错误信息,大部分问题错误信息已经告诉你原因了。

进阶学习路线:

  1. Docker 进阶:学习 Docker Swarm 集群、Docker Secret 管理密钥、Docker 多平台构建等。
  2. Kubernetes:学习 Pod、Deployment、Service、Ingress 等概念,掌握集群容器编排。
  3. CI/CD:学习 Jenkins、GitLab CI、GitHub Actions,把 Docker 集成到持续集成流水线中。
  4. 监控告警:学习 Prometheus + Grafana 监控 Docker 容器,配置告警规则。
  5. 日志收集:学习 ELK(Elasticsearch + Logstash + Kibana)或 Loki 收集容器日志。
  6. 服务网格:学习 Istio、Linkerd 等服务网格技术,实现微服务的流量管理和安全。
  7. 云原生:学习 CNCF(云原生计算基金会)生态中的各种工具和项目。

Docker 只是云原生世界的入口,学好了 Docker,你就拿到了通往云原生世界的钥匙。祝你学习顺利,早日成为容器技术高手!


本章总结

通过本章的学习,我们从一个对容器一无所知的小白,逐步掌握了 Docker 容器技术的方方面面。让我们回顾一下本章的主要内容:

  1. 容器技术概述:理解了容器就是软件世界的"集装箱",解决了"在我机器上能跑"的问题。容器比虚拟机更轻量、更快速。

  2. Docker 安装与配置:学会了在 Ubuntu、CentOS 上安装 Docker,配置镜像加速器,以及验证安装。

  3. 镜像与仓库:掌握了镜像的拉取、查看、删除、导出导入、打标签等操作,了解了 Docker Hub 和私有仓库的使用。

  4. Dockerfile 编写:学会了用 Dockerfile 这份"菜谱"来构建镜像,掌握了所有常用指令,以及多阶段构建等高级技巧。

  5. 容器生命周期管理:全面掌握了容器的创建、启动、停止、删除、进入、查看日志、查看资源使用等管理操作。

  6. 网络配置:理解了 Docker 的网络模式,学会了创建自定义网络、端口映射、容器间通信。

  7. 数据卷与持久化:学会了用 Volume、Bind Mount、tmpfs 三种方式实现数据持久化,以及数据卷的备份恢复。

  8. Docker Compose:掌握了用 YAML 文件编排多容器应用,实现了一键启动复杂架构。

  9. 实战部署案例:通过 Nginx、MySQL、Python Flask、Node.js、LAMP 等案例,将所学知识融会贯通。

  10. 容器安全基础:了解了容器安全的各个层面,学会了镜像安全、最小权限、资源限制、网络隔离等安全加固措施。

Docker 的学习是一个循序渐进的过程,建议你按照本章的顺序,一边阅读一边动手实践。把每个命令都敲一遍,把每个 Dockerfile 都构建一次,把每个案例都跑起来。只有动手做了,才能真正理解和掌握。祝你学习愉快!

容器技术是现代软件工程中不可或缺的一项技能。从开发到测试,从部署到运维,Docker 都在发挥着越来越重要的作用。掌握了 Docker,你不仅能提高自己的开发效率,还能为后续学习 Kubernetes 和云原生技术打下坚实的基础。

回顾本章,我们首先通过搬家的类比理解了容器的本质——软件世界的标准化集装箱。然后从安装配置开始,一步步学习了镜像管理、Dockerfile 编写、容器管理、网络配置、数据持久化、多容器编排、实战部署和安全加固。每一个知识点都配上了大量的命令示例和配置文件,方便你直接参考和使用。

希望本章的内容能帮助你顺利入门 Docker 容器技术。记住,技术学习最重要的是实践,纸上得来终觉浅,绝知此事要躬行。快去打开你的终端,开始 Docker 之旅吧!


下章预告:在第十七章中,我们将学习 Kubernetes 容器编排,它是管理大规模容器集群的利器。如果说 Docker 是集装箱,那 Kubernetes 就是管理成千上万个集装箱的港口调度系统。敬请期待!

更多推荐