封面来源:本文封面来源于网络,如有侵权,请联系删除。

参考链接:尚硅谷Docker实战教程

1. 简介

1.1 Docker 是什么

当开发的应用依赖于当前的电脑配置和某些配置文件,为了能够尽可能多地在其他本地模拟这些环境而不产生重新创建服务器环境的开销,此时可以使用 容器技术

Docker 之所以发展如此迅速,就是因为它对此给出了一个标准化的解决方案 —— 系统平滑移植,容器虚拟化技术。

当环境配置相当复杂时,人们会想到能不能从根本上解决这个问题,软件可不可以连带着其需要的环境一起安装?也就是在安装软件的时候,一并把原始环境也一模一样地复制过来,开发人员利用 Docker 消除协作编码时产生的「在我的机器上都能跑,怎么你的不行」的问题。

Docker 的出现打破了「程序即应用」的关联,通过镜像(images)将作业系统核心除外,运作应用程序所需要的系统环境由上而下地打包,达到应用程序跨平台的无缝接轨运作。

Docker 是基于 Golang 实现的云开源项目,它的主要目标是「Build, Ship and Run Any App, Anywhere」,也就是通过对应用组件的封装、分发、部署、运行等生命周期的管理,使用户的应用程序及其运行环境能够做到「一次镜像,处处运行」。

Linux 容器技术的出现就解决了这样一个问题,Docker 是在它的基础上发展过来的。将应用打包成镜像,通过镜像成为运行在 Docker 容器上的实例。Docker 容器在任何操作系统上都是一致的,这就实现了跨平台、跨服务器。只需要一次配置好环境,换到别的机子上还能一键部署好,极大简化了操作。

一句话:Docker 是解决了运行环境和配置问题的软件容器,方便做持续集成并有助于整体发布的容器虚拟化技术

1.2 容器与虚拟机

虚拟机(Virtual Machine)也是一种带环境安装的解决方案。

虚拟机可以摘一种操作系统里运行另一种操作系统,比如在 Windows 系统里运行 Linux 系统。运行在虚拟机的应用程序毫无感知,因为虚拟机看上去和真实系统一模一样,而对于底层系统来说,虚拟机就是一个普通文件,如果不需要,直接删除,并对系统其他部分毫无影响。

虚拟机终究还是太「重」了,于是 Linux 又发展出另一种虚拟化技术 —— Linux 容器(Linux Containers,缩写为 LXC)。

Linux 容器是与系统其他部分隔离开的一系列进程,从另一个镜像运行,并由该镜像提供支持进程所需的全部文件。容器提供的镜像包含了应用的所有依赖项,因而从开发到测试再到生产的整个过程中,它都具有可移植性和一致性。

Liunx 容器不是模拟一个完整的操作系统, 而是对系统进行隔离。有了容器,就可以将软件运行所需的全部资源打包到一个隔离的容器中。容器与虚拟机不同,不需要捆绑一整套操作系统, 只需要软件工作所需的库资源和设置。系统因此变得高效轻量并保证部署在任何环境中的软件都能始终如一地运行。

Docker 容器是在操作系统层面上实现虚拟化,直接复用本地主机的操作系统,传统虚拟机则是在硬件层面实现虚拟化。与传统的虚拟机相比,Docker 优势体现在启动速度快、占用体积小。

Docker 与传统虚拟化方式的不同:

  • 传统虚拟机技术是虚拟出一套硬件后,运行一个完整操作系统,在该系统上再运行所需应用进程;

  • 容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核也没有进行硬件虚拟。因此容器比传统虚拟机更为轻便;

  • 每个容器之间互相隔离,每个容器有自己的文件系统,容器之间进程不会相互影响,并能区分计算资源。

2. 安装 Docker

Docker 官网:Docker: Accelerated Container Application Development

Docker Hub 官网:Docker Hub Container Image Library | App Containerization

2.1 安装前提

Docker 并非是一个通用的容器工具,它依赖于已存在并运行的 Linux 内核环境。

Docker 实质上是在已经运行的 Linux 下制造了一个隔离的文件环境,因此它执行的效率几乎等同于所部署的 Linux 主机。

因此,Docker 必须部署在 Linux 内核的系统上, 如果有其他系统想要部署 Docker 就必须安装一个 Linux 环境。

查看 Linux 内核信息:

1
2
3
cat /etc/redhat-realease
# or
uname -r

2.2 基本组成

Docker 的基本组成是镜像(image)、容器(container)、仓库(repository)。

镜像

Docker 镜像(Image)是一个 只读 的模板。镜像可以用来创建 Docker 容器,一个镜像可以创建很多容器。它也相当于一个 root 文件系统。比如官方镜像 CentOS7 就包含了一套完整的 CentOS7 最小系统的 root 文件系统。镜像相当于容器的「源代码」,Docker 镜像文件类似于 Java 类,Docker 容器实例类似于 Java 中 new 出来的实例对象。

容器

从面向对象角度来看,Docker 利用容器(Container)独立运行一个或一组应用,应用程序或服务运行在容器里面,容器类似于一个虚拟化的运行环境,容器是用镜像创建的运行实例。就像 Java 中的类和实例对象一样,镜像是静态的定义,容器是镜像运行时的实体。容器为镜像提供了一个标准的、隔离的运行环境,它可以被启动、开始、停止和删除。每个容器都是相互隔离的、保证安全的平台。

从镜像容器角度来看,可以把容器看做是一个简易版的 Linux 环境(包括 root 用户权限、进程空间、用户空间和网络空间等)和运行在其中的应用程序。

仓库

仓库(Repository)是集中存放镜像文件的场所。

类似于:

  • Maven 仓库是存放各种 jar 包的地方
  • GitHub 仓库是存放各种 git 项目的地方

Docker 公司提供的官方 Registry 被称为 Docker Hub,是存放各种镜像模板的地方。

仓库分为公开仓库(Public)和私有仓库(Private)两种形式。最大的公开仓库是 Docker Hub,存放了数量庞大的镜像供用户下载。国内的公开仓库包括阿里云、网易云等。

总结

Docker 本身是一个容器运行载体或者说管理引擎。把应用程序和配置依赖打包好形成一个可交付的运行环境,这个打包好的运行环境就是 Image 镜像文件。只有通过这个镜像文件才能生成 Docker 容器实例(类似 Java 中 new 出来的对象)。

Image 文件可以看作是容器的模板。Docker 根据 Image 文件生成容器的实例。同一个 Image 文件,可以生成多个同时运行的容器实例。

镜像文件:Image 文件生成的容器实例,本身也是一个文件,称之为镜像文件。

容器实例:一个容器运行一种服务,在需要的时候,可以通过 Docker 客户端创建一个对应的运行实例,也就是容器。

仓库:存放一堆镜像的地方,可以把镜像发布到仓库中,需要的时候直接从仓库拉即可。

2.3 架构图解

Docker简易架构图

Docker 是一个 Client-Server 结构的系统,后端是一个松耦合架构,众多模块各司其职。

Docker 守护进程运行在主机上,然后通过 Socket 连接从客户端访问,守护进程从客户端接收命令并管理运行在主机上的容器。

容器,是一个运行时的环境,就像 Docker 图标上的集装箱。

Docker架构图

Docker运行的基本流程为:

  1. 用户使用 Docker Client 与 Docker Daemon 建立通信,并发送请求给后者;
  2. Docker Daemon 作为 Docker 架构的主体部分,首先提供 Docker Server 的功能使其可以接收 Docker Client 的请求;
  3. Docker Engine 执行 Docker 内部的一系列工作,每一项工作都是以一个 Job 的形式存在;
  4. 在 Job 的运行过程中,当需要容器镜像时,会从 Docker Registry 中下载镜像,并通过镜像管理驱动 Graph Driver 将下载镜像以 Graph 的形式存储;
  5. 当需要为 Docker 创建网络环境时,通过网络管理驱动 Network Driver 创建并配置 Docker 容器网络环境;
  6. 当需要限制 Docker 容器运行资源或执行用户指令等操作时,则通过 Exec Driver 完成;
  7. Libcontainer 是一项独立的容器管理包,Network Driver 以及 Exec Driver 都是通过 Libcontainer 来实现对容器进行的具体操作。

2.4 安装 Docker

CentOS 安装 Docker 文档:Install Docker Engine on CentOS

确定 CentOS 版本

执行命令:

1
cat /etc/redhat-release

卸载旧版本

在安装 Docker 之前,需要卸载任何相互冲突的软件包。

使用的 Linux 发行版可能会提供非正式的 Docker 软件包,这可能与 Docker 提供的官方软件包相冲突。在安装Docker 的官方版本之前,必须卸载这些软件包。

1
2
3
4
5
6
7
8
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine

上述命令执行期间可能提示没有安装这些软件包。

卸载 Docker 时,存储在 /var/lib/docker/ 中的镜像(images)、容器(containers)、数据卷(volumes)和网络(networks)不会被自动删除。

yum 安装 gcc 相关

依次执行以下命令:

1
2
yum -y install gcc
yum -y install gcc-c++

安装需要的软件包

首次在新的主机上安装 Docker 之前,需要设置 Docker repository,之后才可以从 repository 中安装和更新 Docker。

安装 yum-utils 软件包(提供了 yum-config-manager 功能 ):

1
sudo yum install -y yum-utils

设置 stable 镜像仓库

官方库,速度慢,不推荐:

1
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

阿里镜像库,推荐

1
sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

更新 yum 软件包索引

1
yum makecache fast

安装 Docker

1
sudo yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

安装过程中会碰到以下询问:

Is this ok [y/d/N]:

键入 y 即可。

启动 Docker

Docker 是一个系统服务,使用启动系统服务方式进行启动:

1
sudo systemctl start docker

成功执行上述命令后,终端不会显示任何信息。

可以执行以下命令查看当前系统中的 Docker 服务:

1
ps -ef | grep docker

测试运行

执行以下命令查看 Docker 版本:

1
docker version

运行 Hello World:

1
docker run hello-world

执行后,终端输出:

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

甚至还会紧接着输出:

docker: Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers).
See 'docker run --help'.

这表示下载远端镜像失败,此时需要配置镜像加速器。

2.5 配置镜像加速器

注册并登录 阿里云,搜索「容器镜像服务」:

配置Docker镜像加速器

执行命令为 Docker 配置镜像加速器:

1
2
3
4
5
6
7
8
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["阿里云镜像加速器地址"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

更多镜像地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"registry-mirrors": [
"https://huecker.io/",
"https://dockerhub.timeweb.cloud",
"https://noohub.ru/",
"https://dockerproxy.com",
"https://docker.mirrors.ustc.edu.cn",
"https://docker.nju.edu.cn",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}

尝试再次执行 docker run hello-world 命令,能够成功下载镜像,并在终端输出以下信息:

Hello from Docker!
This message shows that your installation appears to be working correctly.

执行 docker run 时,Docker 的工作流程:

flowchart LR
	start([开始])
	a[Docker 在本机中寻找镜像]
	b{本机是否有镜像}
	c[以该镜像为模板生产容器实例运行]
	d[去 Docker Hub 上查找该镜像]
	e{Hub 上能否找到}
	f[下载该镜像到本地]
	g["返回失败错误,查不到该镜像"]
	z([结束])
	
	start --> a --> b
	b -- 是 --> c
	b -- 否 --> d
	d --> e
	e -- 是 --> f --> c --> z
	e -- 否 --> g --> z

2.6 卸载 Docker

关闭服务:

1
systemctl stop docker

移除 Docker 相关组件:

1
sudo yum remove docker-ce docker-ce-cli containerd.io

删除镜像、容器、数据卷、自定义配置等文件:

1
2
sudo rm -rf /var/lib/docker
sudo rm -rf /var/lib/containerd

3. 常用命令

3.1 帮助启动类命令

启动 Docker:

1
systemctl start docker

停止 Docker:

1
systemctl stop docker

重启 Docker:

1
systemctl restart docker

查看 Docker 状态:

1
systemctl status docker

设置开机自启动:

1
systemctl enable docker

查看 Docker 概要信息:

1
docker info

查看 Docker 总体帮助文档:

1
docker --help

查看 Docker 命令帮助文档:

1
docker 具体命令 --help

3.2 镜像命令

列出本地主机上的镜像

1
docker images

执行该命令后,无论本地主机上是否有镜像,都会输出以下表头:

REPOSITORY    TAG       IMAGE ID       CREATED       SIZE
  • REPOSITORY 表示镜像的仓库源
  • TAG 表示镜像的标签
  • IMAGE ID 表示镜像 ID
  • CREATED 表示镜像创建时间
  • SIZE 表示镜像大小

同一仓库源可以有多个 TAG 版本,代表这个仓库源的不同个版本,使用 REPOSITORY:TAG 来定义不同的镜像。

如果不指定一个镜像的版本标签,例如只使用 ubuntu,Docker 将默认使用 ubuntu:latest 镜像。

更多选项:

  • -a 列出本地所有的镜像(含历史镜像)
  • -q 只显示镜像 ID

搜索远程仓库中的镜像

1
docker search 镜像名称

执行命令后,会输出以下表头:

NAME    DESCRIPTION       STARS       OFFICIAL       AUTOMATED
  • NAME 表示镜像名称
  • DESCRIPTION 表示镜像说明
  • STARS 表示点赞数量
  • OFFICIAL 表示是否是官方的,[OK] 表示官方的
  • AUTOMATED 表示是否是自动构建的,[OK] 表示自动构建的

更多选项:

  • --limit n 只列出前 n 个镜像,默认 25 个。比如 docker search --limit 5 redis

下载镜像

1
docker pull 镜像名称[:TAG]

TAG 相当于镜像的版本,不添加 TAG 信息时,默认下载最新的镜像,即 TAGlatest

查看镜像、容器、数据卷所占的空间

1
docker system df

强制删除镜像

删除指定镜像:

1
docker rmi -f 镜像名称/ID

使用空格分割镜像信息来删除多个:

1
docker rmi -f 镜像名称1:TAG 镜像名称2:TAG

删除全部镜像:

1
docker rmi -f $(docker images -qa)

优雅地删除镜像

删除镜像使用 docker rmi 命令,-f 选项表示强制删除指定镜像。

不使用 -f 选项,并且删除的镜像正在某个容器中使用,此时会出现以下错误:

Error response from daemon: conflict: unable to remove repository reference "镜像名称" (must force) - container 容器ID is using its referenced image 镜像ID

这表示:某个容器正在使用当前删除的镜像。

执行以下命令查看当前 Docker 中正在运行的容器:

1
docker ps

控制台输出包含先前错误信息中「容器 ID」对应的容器,其 STATUSUp,表示正在运行。

执行下述命令停止容器的运行:

1
docker stop 容器ID

执行以下命令查看 Docker 中的所有容器(使用 -a 选项能够输出所有容器信息,包括正在运行的容器、已停止的容器、创建后从未启动过的容器):

1
docker ps -a

先前被停止运行的容器的 STATUS 更新为 Exited(0)

再执行下述命令删除被停止运行的容器:

1
docker rm 容器ID

最后删除镜像:

1
docker rmi 镜像名称/ID

一道面试题:Docker 的虚悬镜像是什么?

仓库名(REPOSITORY)、标签(TAG)都是 <none> 的镜像,俗称虚悬镜像(dangling image)。

3.3 常用容器命令

有镜像才能创建容器,在开始本节前,先下载一个 ubuntu 镜像:

1
docker pull ubuntu

新建启动容器

1
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

常用选项:

  • --name="容器新名称" 为容器指定一个名称

  • -d 后台运行容器并返回容器 ID,即启动守护式容器(后台运行)

  • -i(interactive)以交互模式运行容器,常与 -t 一起使用

  • -t(tty,terminal)为容器重新分配一个伪输入终端,常与 -i 一起使用,即启动交互式容器(前台有伪终端,等待交互)

  • -e 为容器添加环境变量

  • -P(大写 P随机 端口映射,将容器内暴露的所有端口映射到宿主机随机端口

  • -p (小写 p指定 端口映射

通常使用 -p(小写 p) 指定端口映射,它有多种映射方式:

  • -p hostPort:containerPort,最简单的端口映射,例如 -p 8080:80

  • -p ip:hostPort:containerPort,配置监听地址,例如 -p 10.0.0.1:8080:80

  • -p ip::containerPort,随机分配端口,例如 -p 10.0.0.1::80

  • -p hostPort1:containerPort1 -p hostPort2:containerPort2,一次指定多个端口映射,例如 -p 8080:80 -p 8888:3306

使用示例:使用镜像 ubuntu 以 交互模式 启动一个容器,在该容器内执行 /bin/bash 命令

1
docker run -it ubuntu /bin/bash

上述命令的更多解释:

  • ubuntuubuntu 镜像

  • /bin/bash 放在镜像后的是命令,此处希望有个交互式 Shell,因此使用 /bin/bash

  • 如果想要退出终端,退回到宿主机,直接输入 exit 命令

列出当前所有正在运行的容器

1
docker ps [OPTIONS]

常用选项:

  • -a 列出当前所有 正在运行 的容器和 历史上运行过 的容器

  • -l 显示最近创建的容器

  • -n 显示最近 n 个创建的容器

  • -q 静默模式,只显示容器编号

  • -f 根据指定条件查找

1
2
# 查找已经退出、镜像名称是 tomcat 的容器
docker ps -f status=exited -f ancestor=tomcat

退出容器

前面介绍可以使用 exit 命令退出正在运行的容器,该命令还会将容器一并停止。

如果不想让容器也停止,可以使用快捷键 Ctrl + p + q。之后可以使用以下命令再次进入该容器:

1
docker exec -it 容器ID /bin/bash

启动已停止运行的容器

1
docker start 容器ID/容器名

重启容器

1
docker restart 容器ID/容器名

停止容器

1
docker stop 容器ID/容器名

强制停止容器

1
docker kill 容器ID/容器名

删除容器

删除已经停止的容器:

1
docker rm 容器ID/容器名

强制删除容器:

1
docker rm -f 容器ID/容器名

强制删除所有容器实例(谨慎操作):

1
2
3
docker rm -f $(docker ps -a -q)
# 或者
docker ps -a -q | xargs docker rm -f

3.4 重要容器命令

启动守护式容器(后台服务器)

有镜像才能创建容器,这是根本前提,本节将使用 Redis 6.0.8 镜像进行演示。

在大部分场景下,希望 Docker 的服务是在后台运行的,可以通过 -d 指定容器的后台运行模式:

1
docker run -d 镜像名

比如使用镜像 ubuntu 以后台模式启动一个容器:

1
docker run -d ubuntu

命令执行后会返回容器 ID,如果以 docker ps -a 命令查看当前所有的容器,会发现以 -d 选项启动的容器已经退出。

Docker 容器如果在后台运行,就必须要有一个前台进程。如果容器运行的命令不是那些一直挂起的命令(例如 toptail),就是会自动退出。

这是 Docker 本身的机制,以 Nginx 为例,在正常情况下,配置启动服务只需要启动响应的 service 即可,例如 service nginx start,此时 Nginx 会以后台进程模式运行,导致 Docker 前台没有运行的应用。当容器后台启动后就会立即「自杀」,因为它认为「无事可做」。最佳解决方案是将需要运行的程序以前台进程的形式运行,常见的就是命令行模式,这表示还有交互操作,不能终端,此时容器启动后不再自动退出。

回到 -d 选项的使用,先以前台交互式启动:

1
docker run -it redis:6.0.8

键入 Ctrl + c,会立即退出,使用 docker ps -a 命令查看容器状态,会发现启动的 redis:6.0.8 容器状态为 Exited (0),表示容器已经停止运行。

这显然是不行的,一退出,容器都直接停止了,那还怎么玩?能不能让 Redis 在后台「默默地」运行呢?

不使用 -it 选项,而是使用 -d 选项,以后台守护式启动:

1
docker run -d redis:6.0.8

再使用 docker ps -a 命令查看容器状态,会发现对应容器状态为 Up,即正在运行中。

查看容器日志

1
docker logs 容器ID/容器名

查看容器内运行的进程

1
docker top 容器ID/容器名

查看容器内部细节

1
docker inspect 容器ID/容器名

进入正在运行的容器,并以命令行交互

1
2
3
4
# 1.
docker exec -it 容器ID /bin/bash
# 2.
docker attach 容器ID

两种命令的区别:

  • exec 是在容器中打开新的终端,并且可以启动新的进程,后续用 exit 退出不会停止容器 (推荐使用)

  • attach 直接进入容器启动命令的终端,不会启动新的进程,后续用 exit 退出会直接停止容器

如果多个终端都对同一个容器执行了 docker attach,就会出现类似「投屏显示」的效果。一个终端中输入输出的内容,还会在其他终端上同步显示。

从容器内拷贝文件到主机上

1
docker cp 容器ID:容器内路径 目的主机路径

交换后面两个参数,即可实现从宿主机将文件拷贝到容器中:

1
docker cp 主机路径 容器ID:容器内路径

导入和导出容器

  • export 导出容器的内容流作为一个 tar 归档文件(对应 import 命令);

  • importtar 包中的内容创建一个新的文件系统再导入为镜像(对应 export 命令);

导出:

1
docker export 容器ID > 文件名.tar

导入:

1
cat 文件名.tar | docker import - 镜像用户/镜像名:镜像版本号

容器命令一图流

Docker-Command-Diagram

4. 镜像

4.1 基本概念

再回顾一下:

镜像是一种轻量级、可执行的独立软件包,它包含运行某个软件需要的所有内容,把应用程序和配置依赖打包好形成一个可交付的运行环境(包括代码、运行时需要的库、环境变量、配置文件等),这个打包好的运行环境就是 image 镜像文件。

只有通过这个镜像文件才能生成 Docker 容器实例(类似 Java 中 new 出来一个对象)。

4.2 UnionFS

以拉取最新的 tomcat 镜像为例:

[root@mofan ~]# docker pull tomcat
Using default tag: latest
latest: Pulling from library/tomcat
0e29546d541c: Pull complete
9b829c73b52b: Pull complete
cb5b7ae36172: Pull complete
6494e4811622: Pull complete
668f6fcc5fa5: Pull complete
dc120c3e0290: Pull complete
8f7c0eebb7b1: Pull complete
77b694f83996: Pull complete
0f611256ec3a: Pull complete
4f25def12f23: Pull complete
Digest: sha256:9dee185c3b161cdfede1f5e35e8b56ebc9de88ed3a79526939701f3537a52324
Status: Downloaded newer image for tomcat:latest
docker.io/library/tomcat:latest

可以看到,Docker 镜像在下载过程中似乎是一层一层地被下载。

Docker 中的文件存储驱动叫做 Storage Driver。

Docker 最早支持的 Storage Driver 是 AUFS,它由一层一层的文件系统组成,这种层级的文件系统叫做 UnionFS。

UnionFS(联合文件系统)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite serveral directories into a single virtual filesystem)。

UnionFS 是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像)可以制作各种具体的应用镜像。

特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录。

Docker 在后来版本中还支持 OverlayFS、Btrfs、Device Mapper、VFS、ZFS 等 Storage Driver。

4.3 镜像加载原理

Docker 镜像实际上是由一层一层的文件系统组成,这种层级的文件系统叫做 UnionFS。

bootfs(boot file system)主要包含 bootloader 和 kernel,bootloader 主要用于引导加载 kernel,Linux 刚启动时会加载 bootfs 文件系统。在 Docker 镜像的最底层是引导文件系统 bootfs。这一层与典型的 Linux/Unix 系统是一样的,包含 boot 加载器和内核。当 boot 加载完成之后整个内核就都在内存中了,此时内存的使用权已经由 bootfs 转交给内核,系统也会在这时卸载 bootfs。

rootfs(root file system),在 bootfs 之上,包含Linux 系统中的 /dev/proc/bin/etc 等标准目录和文件。rootfs 是各种不同的操作系统发行版,比如 Ubuntu、CentOS 等。

Docker 镜像底层层次:

Docker镜像底层层次

平时安装的 CentOS、Ubuntu 虚拟机都是好几个 G,为什么它们的 Docker 镜像只有几百兆或者几十兆呢?

对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库。因为底层直接使用 Host 的 Kernel,自己只需要提供 rootfs 即可。所以,对于不同的 Linux 发行版,bootfs 基本是一致的,rootfs 会有些差别,因此不同的发行版可以共用 bootfs。

有差别的rootfs

4.4 分层的镜像

镜像分层的一个最大好处就是共享资源,方便复制迁移,方便复用。

比如说有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需要在磁盘上保存一份 base 镜像,同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了,并且镜像的每一层都可以被共享。

Docker 镜像层都是只读的,容器层是可写的。

当容器启动时,一个新的 可写层 将被加载到镜像的顶部,这一层通常被称为 容器层,容器层之下的都叫 镜像层。所有对容器的改动(无论添加、删除、还是修改文件)都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。

Docker镜像层与容器层

4.5 生成新镜像

docker commit 提交容器副本使之成为一个新的镜像。

1
docker commit -m="提交的描述信息" -a="作者" 容器ID 要创建的目标镜像名:[tag]

使用示例

首先下载 ubuntu 镜像到本地并运行成功,原始默认的 ubuntu 镜像中不包含 vim 命令:

root@8d7a9c64170c:/# vim a.txt
bash: vim: command not found

现在需要在当前容器实例内部安装 Vim,最后使用当前容器副本生成一个包含 vim 命令的 ubuntu 镜像。

首先更新包管理工具:

root@8d7a9c64170c:/# apt-get update

当出现以下字样后,证明包管理工具更新成功:

Reading package lists... Done

之后安装 Vim:

root@8d7a9c64170c:/# apt-get -y install vim

安装完成后尝试使用 vim 命令创建 a.txt 文件,并添加 this is docker 的内容:

root@8d7a9c64170c:/# vim a.txt
root@8d7a9c64170c:/# cat a.txt
this is docker

容器 ID 为 8d7a9c64170c 的容器中已经包含了 vim 命令,在宿主机尝试将其提交并得到一个新的镜像:

1
docker commit -m="add vim cmd" -a="mofan" 8d7a9c64170c mofan/myubuntu:1.0

执行 docker images 命令,能够看到新生成的名为 mofan/myubuntu 的镜像:

[root@mofan ~]# docker images
REPOSITORY       TAG       IMAGE ID       CREATED         SIZE
mofan/myubuntu   1.0       08de280c2075   6 seconds ago   197MB
ubuntu           latest    ba6acccedd29   3 years ago     72.8MB

并且能够发现新生成的镜像体积比原本镜像体积要大得多。

尝试运行新生成的镜像:

1
docker run -it mofan/myubuntu:1.0 /bin/bash

在新容器内部能够使用 vim 命令,并且也能看到先前创建的 a.txt 文件:

root@d7a6a44f7ca3:/# vim a.txt
root@d7a6a44f7ca3:/# cat a.txt
this is docker

总结

Docker 中的镜像分层,支持扩展现有镜像,创建新的镜像。类似在 Java 中继承一个 Base 类,自己再按需扩展。

新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就会在现有镜像的基础上增加一层。

这里涉及到 DockerFile,简单看下即可:

1
2
3
4
5
6
# Version: 0.0.1
FROM debian # 直接在 debain base 镜像上构建
MAINTAINER mylinux
RUN apt-get update && apt-get install -y emacs # 安装 emacs
RUN apt-get install -y apache2 # 安装 apache2
CMD ["/bin/bash"] # 容器启动时运行 bash

Docker镜像创建过程

5. 本地镜像发布到阿里云

阿里云ECS-Docker生态

本节将使用上一节中生成的 mofan/myubuntu:1.0 镜像,并将其发布到阿里云。

首先登录 阿里云,进入「容器镜像服务」页面,创建并进入个人版实例:

阿里云容器镜像服务个人版实例

点击「命名空间」,点击「创建命名空间」,输入想要创建的命名空间名称,最后点击「确定」:

创建命名空间

成功创建mofan212命名空间

切换到「镜像仓库」,选择命名空间为刚刚创建的,并点击「创建镜像仓库」:

准备创建镜像仓库

创建镜像仓库

镜像仓库代码源选择本地仓库

最后点击「创建镜像仓库」完成创建,之后会进入仓库管理页面。

在管理页面的操作指南上存在 将镜像推送到 Registry 的方法:

将镜像推送到Registry

回到终端,登录阿里云 Docker Registry:

1
docker login --username=xxx xxx

用于登录的用户名为阿里云账号全名,密码为开通服务时设置的密码,可以在访问凭证页面修改凭证密码。

输入正确的密码后,出现以下字样,证明登录成功:

Login Succeeded

执行 docker images 命令,查看本地镜像信息:

[root@mofan ~]# docker images
REPOSITORY       TAG       IMAGE ID       CREATED          SIZE
mofan/myubuntu   1.0       08de280c2075   59 minutes ago   197MB
tomcat           latest    fb5657adc892   3 years ago      680MB
ubuntu           latest    ba6acccedd29   3 years ago      72.8MB
hello-world      latest    feb5d9fea6a5   3 years ago      13.3kB
redis            6.0.8     16ecd2772934   4 years ago      104MB

其中镜像 ID 为 08de280c2075 的镜像是需要发布到阿里云的镜像。

为对应镜像取一个别名:

1
2
# [ImageId] 替换成 08de280c2075,[镜像版本号] 替换成 1.0
docker tag [ImageId] xxx.aliyuncs.com/mofan212/myubuntu:[镜像版本号]

推送镜像到阿里云:

1
docker push xxx.aliyuncs.com/mofan212/myubuntu:[镜像版本号]

耐心等待推送完成。

执行命令删除本地镜像:

1
docker rmi -f 08de280c2075

尝试从阿里云拉取刚刚发布的镜像:

1
2
# [镜像版本号] 为先前发布时指定的镜像版本号
docker pull xxx.aliyuncs.com/mofan212/myubuntu:[镜像版本号]

运行拉取的镜像:

1
2
# 发布到阿里云的镜像 ID 不会变,拉取下来的镜像 ID 也和原来一样
docker run -it 08de280c2075 /bin/bash

在这个容器里,可以使用 vim 命令,也存在先前创建的 a.txt 文件。

6. 本地镜像发布到私有库

现阶段中国大陆无法直接访问官方 Docker Hub,并且它也有被阿里云取代的趋势。

在实际工作中,公司通常不会提供镜像给公网,所以需要创建一个本地私人仓库供给团队使用,基于公司内部项目构建镜像。

Docker Registry 是 Docker 官方提供的工具,用于构建私有镜像仓库。

下载镜像 Docker Registry

1
docker pull registry

运行私有库 Registry,相当于本地有个私有 Docker Hub

1
docker run -d -p 5000:5000  -v /mofan/myregistry/:/tmp/registry --privileged=true registry

默认情况,仓库被创建在容器的 /var/lib/registry 目录下,建议自行用容器卷映射,方便于宿主机联调。

确保宿主机上存在 /mofan/myregistry/ 目录,如果不存在,执行以下命令创建:

1
mkdir -p /mofan/myregistry/

创建一个新镜像,ubuntu 安装 ifconfig 命令

运行 ubuntu 镜像:

1
docker run -it ubuntu

原始的 ubuntu 镜像不包含 ifconfig 命令:

root@fc5b8e0407a5:/# ifconfig
bash: ifconfig: command not found

在当前容器内安装 ifconfig 命令并测试通过:

1
2
3
4
5
6
# 更新包管理工具
apt-get update
# 安装 ifconfig
apt-get install net-tools
# 测试 ifconfig
ifconfig

commit 新镜像

在容器外执行 docker ps -a 查看所有容器,会发现容器 ID 为 fc5b8e0407a5ubuntu 镜像容器。

执行以下命令将该容器提交为新镜像:

1
docker commit -m="add ifconfig cmd" -a="mofan" fc5b8e0407a5 mofan/myubuntu:1.1

执行 docker images 命令能够看到新生成的镜像:

[root@mofan /]# docker images
REPOSITORY       TAG       IMAGE ID       CREATED          SIZE
mofan/myubuntu   1.1       7f6f061cbb7c   10 seconds ago   130MB

尝试运行新生成的镜像:

1
docker run -it mofan/myubuntu:1.1

之后容器内执行 ifconfig 命令,检验是否存在 ifconfig

使用 curl 验证私服库上有什么镜像

1
2
# 替换 IP 信息,端口为先前映射的端口,即 5000
curl -XGET http://IP信息:5000/v2/_catalog

如果使用的是云服务器,上述命令的「IP信息」即为云服务器公网 IP。除此之外,还需要把安全组里的 5000 端口打开。

执行上述命令后,终端输出:

{"repositories":[]}

将新镜像修改符合私服规范的 Tag

1
docker tag 镜像名称:Tag Host:Port/Repository:Tag

例如:

1
docker tag mofan/myubuntu:1.1 IP信息:5000/myubuntu:1.1

同样的,如果使用的是云服务器,上述命令的「IP信息」也替换为云服务器公网 IP。

执行 docker images 命令能看到取了别名的镜像:

[root@mofan /]# docker images
REPOSITORY                    TAG       IMAGE ID       CREATED          SIZE
IP信息:5000/myubuntu           1.1       7f6f061cbb7c   27 minutes ago   130MB
mofan/myubuntu                1.1       7f6f061cbb7c   27 minutes ago   130MB

修改配置文件使之支持 http

Docker 默认不允许以 http 的方式来推送镜像,需要通过配置选项来取消这个限制。

先前配置镜像加速器时修改过 /etc/docker/daemon.json 文件,现在需要在该文件中追加 insecure-registries 信息:

1
2
3
4
5
6
7
8
{
"registry-mirrors": [
"阿里云镜像加速器地址"
],
"insecure-registries": [
"IP信息:5000"
]
}

如果使用的是云服务器,上述内容中的「IP信息」还是替换为云服务器公网 IP。

修改完成后,与配置镜像加速器一样,需要重新加载 daemon.json 文件并重启 Docker:

1
2
sudo systemctl daemon-reload
sudo systemctl restart docker

重启 Docker 后,原先运行 Registry 的容器也停止了,需要重新启动:

1
docker start registry

push 到私服库

执行 docker images 查看镜像信息:

[root@mofan /]# docker images
REPOSITORY                    TAG       IMAGE ID       CREATED          SIZE
IP信息:5000/myubuntu           1.1       7f6f061cbb7c   27 minutes ago   130MB
mofan/myubuntu                1.1       7f6f061cbb7c   27 minutes ago   130MB

然后执行以下命令将镜像推送到私服库:

1
docker push IP信息:5000/myubuntu:1.1

耐心等待推送完成。

再次验证私服库上的镜像信息

1
2
# 替换 IP 信息,端口为先前映射的端口,即 5000
curl -XGET http://IP信息:5000/v2/_catalog

执行上述命令后,终端输出:

{"repositories":["myubuntu"]}

拉取私服的镜像到本地运行并验证

像上一节那样,先删除本地镜像,然后拉取私服镜像,最后验证新的 ubuntu 镜像中是否包含 ifconfig 命令:

1
2
3
4
5
6
7
# 删除本地镜像
docker rmi -f 镜像ID
# pull 后的内容与先前执行 push 时一样
docker pull IP信息:5000/myubuntu:1.1
# 运行容器
docker run -it IP信息:5000/myubuntu:1.1
# 进入容器内,验证 ifconfig 命令

7. 容器数据卷

7.1 先来个坑

在使用容器数据卷时,Docker 挂载主机目录访问出现以下错误:

cannot open directory '.': Permission denied

此时在挂载目录后添加 --privileged=true 参数即可解决。

如果使用的是 CentOS7,其安全模块相比之前版本得到了加强,不安全的操作会先被禁止,挂载目录被默认认定为不安全的行为。在 SELinux 里面挂载目录被禁止掉了,如果要开启,一般使用 --privileged=true 命令来扩大容器的权限并解决挂载目录没有权限的问题。使用该参数,容器内的 root 用户才拥有真正的 root 权限,否则只是外部的普通用户权限。

7.2 是什么

容器数据卷就是目录或文件,存在于一个或多个容器中,由 Docker 挂载到容器,但不属于联合文件系统,因此能够绕过 UnionFS,提供一些用于持续存储或共享数据的特性。设计目的是数据的持久化,完全独立于容器的生存周期,因此不会在容器删除时删除其挂载的数据卷。

类似 Redis 里面的 rdb 和 aof 文件,将 Docker 容器内的数据保存进宿主机的磁盘中。

运行一个带有容器数据卷存储功能的容器实例:

1
docker run -it --privileged=true -v /宿主机绝对路径目录:/容器内目录 镜像名

7.3 能干嘛

如果不备份 Docker 容器产生的数据,那么当容器实例删除后,容器内的数据自然也就没有了。为了能够保存这些数据就可以使用容器数据卷。

数据卷的特点:

  1. 数据卷可以在容器之间共享或重用数据
  2. 数据卷中的更改可以直接实时生效
  3. 数据卷中的更改不会包含在镜像的更新中
  4. 数据卷的生命周期一直持续到没有容器使用它为止

7.4 使用案例

宿主机与容器之间映射添加容器卷

1
docker run -it --privileged=true -v /宿主机绝对路径目录:/容器内目录 镜像名

当容器内目录下的文件发生变化时,宿主机下的目录会实时感知到,反之亦然。

如果容器被停止了,之后宿主机目录下的文件发生了变化,当容器再次运行时,容器目录依旧会感知到文件的变化。

使用以下命令查看数据卷是否挂载成功:

1
docker inspect 容器ID

执行命令后会输出包含 Mounts 的信息,其中的 Source 即宿主机下的目录,Destination 即容器内目录。

读写规则映射添加说明

默认规则是 读写

1
docker run -it --privileged=true -v /宿主机绝对路径目录:/容器内目录:rw 镜像名

rwreadwrite,这是默认规则,不加也行。

只读 规则,容器实例内部被限制,只能读取不能写入:

1
docker run -it --privileged=true -v /宿主机绝对路径目录:/容器内目录:ro 镜像名

roread only

如果尝试在容器内写入,会显示如下错误:

Read-only file system

如果宿主机写入了内容,容器可以感知到变化。

卷的继承和共享

启动一个容器:

1
docker run -it --privileged=true -v /mydocker/u:/tmp --name u1 ubuntu

使用 --volumes-from 选项继承 u1 的容器卷映射配置:

1
docker run -it --privileged=true --volumes-from u1 --name u2 ubuntu

8. 常规软件安装

8.1 安装 Tomcat

拉取 tomcat 镜像到本地:

1
docker pull tomcat

查看是否成功拉取到 tomcat 镜像:

1
docker images tomcat

使用 tomcat 镜像创建容器实例:

1
2
# 宿主机的 8080 端口映射到容器的 8080 端口
docker run -d -p 8080:8080 tomcat

在浏览器中以 8080 端口访问 Tomcat 首页,页面出现 404 错误。

如果使用的是云服务器,可以直接以 公网IP:8080 的形式进行访问,在之前记得把 8080 端口添加到安全组。

进入运行 tomcat 的容器,并以命令行进行交互:

1
docker exec -it 容器ID /bin/bash

进入容器后会位于容器的 /usr/local/tomcat 目录下。

查看该目录下的文件:

1
ls -l
drwxr-xr-x 2 root root  4096 Dec 22  2021 webapps
drwxr-xr-x 7 root root  4096 Dec  2  2021 webapps.dist

存在名为 webappswebapps.dist 的两个目录。

如果想要在浏览器上出现 tomcat 的那只猫,而不是显示 404,那么 webapps 目录下应该是有些文件的:

1
2
3
4
# 进入 webapps 目录
cd webapps
# 查看内部文件信息
ls -l
total 0

结果里面什么都没有。

这是因为新版 Tomcat 发生了变化,那些文件都被移动到了 webapps.dist 目录下。

删除现有的 webapps 目录:

1
rm -r webapps

webapps.dist 目录重命名为 webapps

1
mv webapps.dist webapps

之后在用浏览器访问 Tomcat 首页,此时就能看到那只猫。😹

如果不想修改,而是直接使用,可以使用以下 tomcat 镜像:

1
2
docker pull billygoo/tomcat8-jdk8
docker run -d -p 8080:8080 --name mytomcat8 billygoo/tomcat8-jdk8

8.2 安装 MySQL

简单版(存在缺陷)

拉取 mysql 镜像到本地(以 5.7 版本为例):

1
docker pull mysql:5.7

查看 3306 端口是否被占用:

1
netstat -tunlp | grep 3306

如果被占用,可以尝试 kill 掉,也可以在后续创建 mysql 容器实例时,指定额外的端口映射。

使用 mysql 镜像创建容器实例:

1
docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7

查看运行的容器信息,获取 mysql 容器 ID:

1
docker ps

进入容器:

1
docker exec -it 容器ID /bin/bash

进入 MySQL:

1
mysql -uroot -p

输入正确的密码即可进入 MySQL 命令行页面,之后的建表、数据添加都能在此进行,当然也可以使用图形化界面工具连接该 MySQL,效果都差不多。

但这样的 MySQL 仍然有问题,当插入数据包含中文时就会报错。

因为 Docker 上的 MySQL 默认字符集编码存在隐患。

查看 MySQL 字符集相关系统变量:

1
SHOW VARIABLES LIKE 'character%';

其中的 character_set_database 信息为 latin1,这种字符集编码不支持中文,这就是报错的原因。

除此之外,当前启动方式并没有使用到容器数据卷,如果意外将该容器删除了,其中的数据不就全部丢失了?

实战版

从头来新建 mysql 容器实例:

1
2
3
4
5
6
7
docker run -d -p 3306:3306 
--privileged=true
-v /mofan/mysql/log:/var/log/mysql
-v /mofan/mysql/data:/var/lib/mysql
-v /mofan/mysql/conf:/etc/mysql/conf.d
-eMYSQL_ROOT_PASSWORD=123456
--name mysql mysql:5.7

接下来需要解决中文乱码问题,在宿主机的 /mofan/mysql/conf 目录(这个目录是先前创建容器实例时与容器的映射目录)下新建 my.cnf 文件,并写入以下内容:

1
2
3
4
5
[client]
default_character_set=utf8
[mysqld]
collation_server=utf8_general_ci
character_set_server=utf8

重新启动 mysql 容器实例:

1
docker restart mysql

再次进入容器:

1
docker exec -it 容器ID /bin/bash

查看字符集编码:

1
SHOW VARIABLES LIKE 'character%';

此时的 character_set_database 信息为 utf8,后续能够成功插入中文。

创建 mysql 容器实例时挂载了容器数据卷,如果这个容器也被意外删除,之后以同样的方式新建新的容器,在新的容器中依旧存在前一个容器的数据。

8.3 安装 Redis

简单版(未使用容器数据卷)

拉取 redis 镜像到本地(以 6.0.8 版本为例):

1
docker pull redis:6.0.8

创建容器实例:

1
docker run -d -p 6379:6379 redis:6.0.8

查看正在运行的容器信息:

1
docker ps
CONTAINER ID   IMAGE      
ebb1d8ebc293   redis:6.0.8

以命令行交互模式进入该容器:

1
docker exec -it 容器ID /bin/bash

简单使用下 Redis 相关命令:

root@ebb1d8ebc293:/data# redis-cli
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379>

实战版

在宿主机上新建 /app/redis 目录:

1
mkdir -p /app/redis

将一个 redis.conf 配置文件模板拷贝进 /app/redis 目录下:

1
cp /redis/redis.conf /app/redis/

/app/redis 目录下修改 redis.conf 文件:

  • 开启 Redis 验证,指定连接密码:requirepass 密码
  • 允许 Redis 外地连接,注释掉 bind 127.0.0.1
  • 注释 daemonize yes 或者修改为 daemonize no,该配置会和 docker run 中的 -d 参数冲突,进而导致容器一直启动失败
  • 关闭保护模式,设置 protected-mode no
  • 开启 Redis 数据持久化,设置 appendonly yes

使用以下命令创建 redis 容器实例:

1
2
3
4
5
6
7
docker run  
-p 6379:6379
--name myr3
--privileged=true
-v /app/redis/redis.conf:/etc/redis/redis.conf
-v /app/redis/data:/data
-d redis:6.0.8 redis-server /etc/redis/redis.conf

之后正常使用 Redis 即可。