Docker 基础篇
封面来源:本文封面来源于网络,如有侵权,请联系删除。
参考链接:尚硅谷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 | cat /etc/redhat-realease |
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 是一个 Client-Server 结构的系统,后端是一个松耦合架构,众多模块各司其职。
Docker 守护进程运行在主机上,然后通过 Socket 连接从客户端访问,守护进程从客户端接收命令并管理运行在主机上的容器。
容器,是一个运行时的环境,就像 Docker 图标上的集装箱。
Docker运行的基本流程为:
- 用户使用 Docker Client 与 Docker Daemon 建立通信,并发送请求给后者;
- Docker Daemon 作为 Docker 架构的主体部分,首先提供 Docker Server 的功能使其可以接收 Docker Client 的请求;
- Docker Engine 执行 Docker 内部的一系列工作,每一项工作都是以一个 Job 的形式存在;
- 在 Job 的运行过程中,当需要容器镜像时,会从 Docker Registry 中下载镜像,并通过镜像管理驱动 Graph Driver 将下载镜像以 Graph 的形式存储;
- 当需要为 Docker 创建网络环境时,通过网络管理驱动 Network Driver 创建并配置 Docker 容器网络环境;
- 当需要限制 Docker 容器运行资源或执行用户指令等操作时,则通过 Exec Driver 完成;
- 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 | sudo yum remove docker \ |
上述命令执行期间可能提示没有安装这些软件包。
卸载 Docker 时,存储在 /var/lib/docker/
中的镜像(images)、容器(containers)、数据卷(volumes)和网络(networks)不会被自动删除。
yum 安装 gcc 相关
依次执行以下命令:
1 | yum -y install gcc |
安装需要的软件包
首次在新的主机上安装 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 配置镜像加速器:
1 | sudo mkdir -p /etc/docker |
更多镜像地址:
1 | { |
尝试再次执行 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 | sudo rm -rf /var/lib/docker |
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
表示镜像 IDCREATED
表示镜像创建时间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
信息时,默认下载最新的镜像,即 TAG
为 latest
。
查看镜像、容器、数据卷所占的空间
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」对应的容器,其 STATUS
为 Up
,表示正在运行。
执行下述命令停止容器的运行:
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 |
上述命令的更多解释:
-
ubuntu
指ubuntu
镜像 -
/bin/bash
放在镜像后的是命令,此处希望有个交互式 Shell,因此使用/bin/bash
-
如果想要退出终端,退回到宿主机,直接输入
exit
命令
列出当前所有正在运行的容器
1 | docker ps [OPTIONS] |
常用选项:
-
-a
列出当前所有 正在运行 的容器和 历史上运行过 的容器 -
-l
显示最近创建的容器 -
-n
显示最近 n 个创建的容器 -
-q
静默模式,只显示容器编号 -
-f
根据指定条件查找
1 | 查找已经退出、镜像名称是 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 | docker rm -f $(docker ps -a -q) |
3.4 重要容器命令
启动守护式容器(后台服务器)
有镜像才能创建容器,这是根本前提,本节将使用 Redis 6.0.8 镜像进行演示。
在大部分场景下,希望 Docker 的服务是在后台运行的,可以通过 -d
指定容器的后台运行模式:
1 | docker run -d 镜像名 |
比如使用镜像 ubuntu 以后台模式启动一个容器:
1 | docker run -d ubuntu |
命令执行后会返回容器 ID,如果以 docker ps -a
命令查看当前所有的容器,会发现以 -d
选项启动的容器已经退出。
Docker 容器如果在后台运行,就必须要有一个前台进程。如果容器运行的命令不是那些一直挂起的命令(例如 top
、tail
),就是会自动退出。
这是 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 | 1. |
两种命令的区别:
-
exec
是在容器中打开新的终端,并且可以启动新的进程,后续用exit
退出不会停止容器 (推荐使用) -
attach
直接进入容器启动命令的终端,不会启动新的进程,后续用exit
退出会直接停止容器
如果多个终端都对同一个容器执行了 docker attach
,就会出现类似「投屏显示」的效果。一个终端中输入输出的内容,还会在其他终端上同步显示。
从容器内拷贝文件到主机上
1 | docker cp 容器ID:容器内路径 目的主机路径 |
交换后面两个参数,即可实现从宿主机将文件拷贝到容器中:
1 | docker cp 主机路径 容器ID:容器内路径 |
导入和导出容器
-
export
导出容器的内容流作为一个tar
归档文件(对应import
命令); -
import
从tar
包中的内容创建一个新的文件系统再导入为镜像(对应export
命令);
导出:
1 | docker export 容器ID > 文件名.tar |
导入:
1 | cat 文件名.tar | docker import - 镜像用户/镜像名:镜像版本号 |
容器命令一图流
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 镜像底层层次:
平时安装的 CentOS、Ubuntu 虚拟机都是好几个 G,为什么它们的 Docker 镜像只有几百兆或者几十兆呢?
对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库。因为底层直接使用 Host 的 Kernel,自己只需要提供 rootfs 即可。所以,对于不同的 Linux 发行版,bootfs 基本是一致的,rootfs 会有些差别,因此不同的发行版可以共用 bootfs。
4.4 分层的镜像
镜像分层的一个最大好处就是共享资源,方便复制迁移,方便复用。
比如说有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需要在磁盘上保存一份 base 镜像,同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了,并且镜像的每一层都可以被共享。
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 | # Version: 0.0.1 |
5. 本地镜像发布到阿里云
本节将使用上一节中生成的 mofan/myubuntu:1.0
镜像,并将其发布到阿里云。
首先登录 阿里云,进入「容器镜像服务」页面,创建并进入个人版实例:
点击「命名空间」,点击「创建命名空间」,输入想要创建的命名空间名称,最后点击「确定」:
切换到「镜像仓库」,选择命名空间为刚刚创建的,并点击「创建镜像仓库」:
最后点击「创建镜像仓库」完成创建,之后会进入仓库管理页面。
在管理页面的操作指南上存在 将镜像推送到 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 | [ImageId] 替换成 08de280c2075,[镜像版本号] 替换成 1.0 |
推送镜像到阿里云:
1 | docker push xxx.aliyuncs.com/mofan212/myubuntu:[镜像版本号] |
耐心等待推送完成。
执行命令删除本地镜像:
1 | docker rmi -f 08de280c2075 |
尝试从阿里云拉取刚刚发布的镜像:
1 | [镜像版本号] 为先前发布时指定的镜像版本号 |
运行拉取的镜像:
1 | 发布到阿里云的镜像 ID 不会变,拉取下来的镜像 ID 也和原来一样 |
在这个容器里,可以使用 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 | 更新包管理工具 |
commit 新镜像
在容器外执行 docker ps -a
查看所有容器,会发现容器 ID 为 fc5b8e0407a5
的 ubuntu
镜像容器。
执行以下命令将该容器提交为新镜像:
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 | 替换 IP 信息,端口为先前映射的端口,即 5000 |
如果使用的是云服务器,上述命令的「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 | { |
如果使用的是云服务器,上述内容中的「IP信息」还是替换为云服务器公网 IP。
修改完成后,与配置镜像加速器一样,需要重新加载 daemon.json
文件并重启 Docker:
1 | sudo systemctl daemon-reload |
重启 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 | 替换 IP 信息,端口为先前映射的端口,即 5000 |
执行上述命令后,终端输出:
{"repositories":["myubuntu"]}
拉取私服的镜像到本地运行并验证
像上一节那样,先删除本地镜像,然后拉取私服镜像,最后验证新的 ubuntu 镜像中是否包含 ifconfig
命令:
1 | 删除本地镜像 |
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 容器产生的数据,那么当容器实例删除后,容器内的数据自然也就没有了。为了能够保存这些数据就可以使用容器数据卷。
数据卷的特点:
- 数据卷可以在容器之间共享或重用数据
- 数据卷中的更改可以直接实时生效
- 数据卷中的更改不会包含在镜像的更新中
- 数据卷的生命周期一直持续到没有容器使用它为止
7.4 使用案例
宿主机与容器之间映射添加容器卷
1 | docker run -it --privileged=true -v /宿主机绝对路径目录:/容器内目录 镜像名 |
当容器内目录下的文件发生变化时,宿主机下的目录会实时感知到,反之亦然。
如果容器被停止了,之后宿主机目录下的文件发生了变化,当容器再次运行时,容器目录依旧会感知到文件的变化。
使用以下命令查看数据卷是否挂载成功:
1 | docker inspect 容器ID |
执行命令后会输出包含 Mounts
的信息,其中的 Source
即宿主机下的目录,Destination
即容器内目录。
读写规则映射添加说明
默认规则是 读写:
1 | docker run -it --privileged=true -v /宿主机绝对路径目录:/容器内目录:rw 镜像名 |
rw
即 read
与 write
,这是默认规则,不加也行。
只读 规则,容器实例内部被限制,只能读取不能写入:
1 | docker run -it --privileged=true -v /宿主机绝对路径目录:/容器内目录:ro 镜像名 |
ro
即 read 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 | 宿主机的 8080 端口映射到容器的 8080 端口 |
在浏览器中以 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
存在名为 webapps
和 webapps.dist
的两个目录。
如果想要在浏览器上出现 tomcat 的那只猫,而不是显示 404,那么 webapps
目录下应该是有些文件的:
1 | 进入 webapps 目录 |
total 0
结果里面什么都没有。
这是因为新版 Tomcat 发生了变化,那些文件都被移动到了 webapps.dist
目录下。
删除现有的 webapps
目录:
1 | rm -r webapps |
将 webapps.dist
目录重命名为 webapps
:
1 | mv webapps.dist webapps |
之后在用浏览器访问 Tomcat 首页,此时就能看到那只猫。😹
如果不想修改,而是直接使用,可以使用以下 tomcat 镜像:
1 | docker pull 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 | docker run -d -p 3306:3306 |
接下来需要解决中文乱码问题,在宿主机的 /mofan/mysql/conf
目录(这个目录是先前创建容器实例时与容器的映射目录)下新建 my.cnf
文件,并写入以下内容:
1 | [client] |
重新启动 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 | docker run |
之后正常使用 Redis 即可。