Docker笔记
基本概念
Docker 包括三个基本概念:
- 镜像(Image)
- 容器(Container)
- 仓库(Repository)
镜像
操作系统分为内核和用户空间。对于Linux,内核启动后会挂载root文件系统为其提供用户空间支持。而Docker镜像就相当于是一个root文件系统。
Docker镜像是一个特殊的文件系统,除了提供容器运行时所需要的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,镜像的内容在构建之后也不会改变。
分层存储
因为镜像包含操作系统完整的root文件系统,体积比较庞大,所以在Docker设计时就将其设置为分层存储的架构。所以实际上镜像只是一个虚拟的概念,并非由一个文件组成,而是由多层文件系统构成。
构建镜像时会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。
分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。
容器
镜像与容器就像类和实例一样,镜像是静态的定义,容器时镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
容器的实质是进程,但与直接在宿主机执行的进程不同,容器进程运行于属于自己的独立的命名空间。因此容易可以拥有自己的root文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。
容器也使用了分层存储的技术,每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为 容器存储层。
容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。
容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用数据卷(Volume)或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器删除或者重新运行之后,数据却不会丢失。
仓库
镜像构建完成后就可以在当前宿主机上运行,但是如果需要在其他服务器上使用这个镜像就需要一个集中的存储、分发镜像的服务,Docker Registry
就是这样的服务。
一个Docker Registry
中可以包含多个仓库(Repository),每个仓库可以包含多个标签(tag),每个标签对应一个镜像。
通常一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。可以通过 <仓库名>:<标签>
的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。
仓库名经常以 两段式路径 形式出现,比如 jwilder/nginx-proxy
,前者往往意味着 Docker Registry
多用户环境下的用户名,后者则往往是对应的软件名。但这并非绝对,取决于所使用的具体 Docker Registry
的软件或服务。
Docker Registry公开服务
Docker Registry
公开服务是开放给用户使用、允许用户管理镜像的Registry服务。
最常使用的 Registry 公开服务是官方的 Docker Hub,这也是默认的 Registry,并拥有大量的高质量的 官方镜像。
安装Docker
首先安装工具:
1 | sudo apt-get install ca-certificates curl gnupg lsb-release |
接着安装官方的GPG key:
1 | sudo mkdir -p /etc/apt/keyrings |
最后将Docker库添加到apt资源列表中:
1 | 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 |
更新一次apt:
1 | sudo apt update |
安装Docker CE版本:
1 | sudo apt-get install docker-ce docker-ce-cli containerd.io |
等待安装完成后,将当前用户添加到docker用户组,避免每次使用docker命令都要sudo
执行:
1 | sudo usermod -aG docker <用户名> |
重启虚拟机。
测试Docker是否安装成功:
1 | docker run --rm hello-world |
说明已经成功。
使用镜像
获取镜像
下载
Docker Hub上有大量高质量的镜像,从Docker 镜像仓库获取镜像的命令是docker pull
。其命令格式为:
1 | $ docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签] |
- Docker镜像仓库地址:地址的格式一般是
<域名/IP>[:端口号]
。默认地址是Docker Hub(docker.io) - 仓库名:两段式名称:
<用户名>/<软件名>
。对于Docker Hub,用户名默认为library
即官方镜像。
比如
1 | docker pull ubuntu:18.04 |
命令中并没有给出Docker镜像仓库地址,一次会从Docker Hub(Docker.io)获取镜像。而镜像名称是ubuntu:18.04
,因此会获取官方镜像library/ubuntu
仓库中标签为18.04
的镜像。输出结果的最后一行给出了镜像的完整名称:docker.io/library/ubuntu:18.04
。
从下载过程中可以发现是一层一层下载,并给出了每一层的ID的前12位。下载结束后会给出该镜像完整的sha256
的摘要以确保下载的一致性。
可能不同的时间下载看到的层ID以及sha256
的摘要都不同,是因为官方镜像一直在维护,版本更新都会进行修复再以原来的标签发布,这样可以确保任何使用这个标签的用户可以获得更安全、更稳定的镜像。
运行
以上面的ubuntu:18.04
为例,如果我们打算启动里面的 bash
并且进行交互式操作的话,可以执行下面的命令:
1 | docker run -it --rm ubuntu:18.04 bash |
含义:
-i
允许用户进行交互式操作,-t
打开终端。我们打算进入bash
执行命令并查看返回结果,所以需要交互式终端。--rm
:容器退出后将其删除,默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动docker rm
。这里节省空间自动删除。- 用
ubuntu:18.04
镜像为基础来启动容器 bash
放在镜像名字后的是命令,我们希望有一个交互式shell,所以使用bash
。
进入容器后,我们可以在 Shell 下操作,执行任何所需的命令。
这里,执行了 cat /etc/os-release
,这是 Linux 常用的查看当前系统版本的命令,从返回的结果可以看到容器内是 Ubuntu 18.04.1 LTS
系统。
使用exit
可以退出容器。
可以手动指定名称运行,在使用docker run
命令时添加--name
参数即可:
1 | docker run --name=test hello-world |
列出镜像
想要列出下载成功的镜像可以使用:
1 | docker image ls |
包含了仓库名、标签、镜像ID、创建时间与占用的存储空间。
其中仓库名、标签在之前的基础概念章节已经介绍过了。镜像ID 则是镜像的唯一标识,一个镜像可以对应多个 标签。
镜像大小
如果仔细观察,会注意到,这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同。比如,ubuntu:18.04
镜像大小,在这里是 63.2MB,但是在 Docker Hub 显示的却是 25.47 MB。这是因为 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。而 docker image ls
显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。
另外,docker image ls
列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。
可以通过docker system df
来便捷查看镜像、容器、数据卷占用的存储空间:
中间层镜像
为了加速镜像构建、重复利用资源,Docker会利用中间层镜像。默认的docker image ls
列表中只会显示顶层镜像,如果要显示包括中间层镜像在内的所有镜像的话,需要加 -a 参数:
1 | docker image ls -a |
这样会看到很多无标签的镜像,这些无标签的镜像很多都是中间层镜像,是其它镜像所依赖的镜像。只要删除那些依赖它们的镜像后,这些依赖的中间层镜像也会被连带删除。
列出部分镜像
不加任何参数的情况下,docker image ls
会列出所有顶层镜像,但是有时候我们只希望列出部分镜像,需要一些参数:
- 根据仓库名列出镜像:
docker image ls ubuntu
- 指定仓库名和标签:
docker image ls ubuntu:18.04
-f
过滤器。如果希望看到在ubuntu:18.04
之后建立的镜像,可以使用:docker image ls -f since=ubuntu:18.04
。想查看某个之前的只需要把since
换成before
。- 如果镜像构建时定义了
LABEL
还可以通过LABEL
来过滤:$ docker image ls -f label=com.example.version=0.1...
以特定格式显示
默认情况下,docker image ls 会输出一个完整的表格,但是我们并非所有时候都会需要这些内容,这个时候就用到了 -q 参数:
1 | docker image ls -q |
--filter
配合 -q
产生出指定范围的 ID 列表,然后送给另一个 docker 命令作为参数,从而针对这组实体成批的进行某种操作的做法在 Docker 命令行使用过程中非常常见。
另外一些时候,我们可能只是对表格的结构不满意,希望自己组织列;或者不希望有标题,这样方便其它程序解析结果等就用到了 Go 的模板语法。
比如,下面的命令会直接列出镜像结果,并且只包含镜像ID和仓库名:
1 | docker image ls --format "{{.ID}}: {{.Repository}}" |
删除本地镜像
要删除本地镜像可以使用docker image rm
,其格式为:
1 | docker image rm [选项] <镜像1> [<镜像2> ...] |
用ID、镜像名、摘要删除镜像
其中,<镜像>
可以是镜像长ID、镜像短ID、镜像名或者镜像摘要。
我们可以用镜像的完整 ID,也称为 长 ID,来删除镜像。使用脚本的时候可能会用长 ID,但是人工输入就太累了,所以更多的时候是用 短 ID 来删除镜像。docker image ls 默认列出的就已经是短 ID 了,一般取前3个字符以上,只要足够区分于别的镜像就可以了。
我们也可以用镜像名,也就是 <仓库名>:<标签>,来删除镜像。
当然,更精确的是使用 镜像摘要 删除镜像。先使用docker image ls --digests
显示摘要,然后使用sha256
进行删除:
1 | $ docker image ls --digests |
Untagged和Deleted
删除行分为两类,一类是Untagged
,另一类是Deleted
。
因为镜像的唯一标识是其 ID 和摘要,而一个镜像可以有多个标签,所以当使用命令删除镜像的时候,实际上是在要求删除某个标签的镜像。所以首先需要做的是将满足我们要求的所有镜像标签都取消,这就是我们看到的 Untagged 的信息。因为一个镜像可以对应多个标签,因此当我们删除了所指定的标签后,可能还有别的标签指向了这个镜像,如果是这种情况,那么 Delete 行为就不会发生。所以并非所有的 docker image rm 都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已。
当该镜像所有的标签都被取消了,该镜像很可能会失去了存在的意义,因此会触发删除行为。镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变得非常容易,因此很有可能某个其它镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层。这就是为什么,有时候会奇怪,为什么明明没有别的标签指向这个镜像,但是它还是存在的原因,也是为什么有时候会发现所删除的层数和自己 docker pull 看到的层数不一样的原因。
除了镜像依赖以外,还需要注意的是容器对镜像的依赖。如果有用这个镜像启动的容器存在(即使容器没有运行),那么同样不可以删除这个镜像。容器是以镜像为基础,再加一层容器存储层,组成这样的多层存储结构去运行的。因此该镜像如果被这个容器所依赖的,那么删除必然会导致故障。如果这些容器是不需要的,应该先将它们删除,然后再来删除镜像。
用docker image ls命令配合
可以使用 docker image ls -q
来配合使用 docker image rm
,这样可以成批的删除希望删除的镜像。
比如我们需要删除所有仓库名为 redis
的镜像:
1 | docker image rm $(docker image ls -q redis) |
或者删除所有在 mongo:3.2
之前的镜像:
1 | $ docker image rm $(docker image ls -q -f before=mongo:3.2) |
docker commit
待续
操作容器
启动容器
启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(exited)的容器重新启动。
新建并启动
使用docker run
启动。
比如,输出一个”Hello World”然后终止容器:
1 | docker run ubuntu:18.04 /bin/echo 'Hello world' |
或者启动一个bash
终端,允许用户进行交互:
1 | docker run -t -i ubuntu:18.04 /bin/bash |
可以通过创建的终端输入命令,比如:
当利用docker run
来创建容器时,Docker在后台运行的操作包括:
- 检查本地是否存在指定的镜像,不存在就从 registry 下载
- 利用镜像创建并启动一个容器
- 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
- 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
- 从地址池配置一个 ip 地址给容器
- 执行用户指定的应用程序
- 执行完毕后容器被终止
启动已终止容器
可以使用docker container start
命令,直接将一个已经终止(exited)的容器启动运行。
容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必需的。除此之外,并没有其它的资源。可以在伪终端中利用 ps 或 top 来查看进程信息:
可见,容器中仅运行了指定的 bash 应用。这种特点使得 Docker 对资源的利用率极高。
守护态运行
更多的时候,需要让 Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加 -d
参数来实现。
如果不使用-d
参数运行容器,容器就会把输出的结果打印到宿主机上面:
但是如果使用了参数-d
运行容器:
1 | docker run -d ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done" |
此时容器会在后台运行并不会把输出的结果打印到宿主机上面(输出结果可以用 docker logs
查看)。但是容器是否会长久运行,是和 docker run
指定的命令有关,和 -d
参数无关。
使用 -d
参数启动后会返回一个唯一的 id,也可以通过 docker container ls
命令来查看容器信息:
要获取容器的输出信息,可以通过docker container logs
命令。
终止
可以使用docker container stop
来终止一个运行中的容器。
当Docker容器中指定的应用终结时,容器也自动终止。例如只启动了一个终端的容器,通过exit
或ctrl + d
命令来退出时,所创建的容器立刻终止。
终止状态的容器可以用docker container ls -a
命令查看
处于终止状态的容器可以通过docker container start
命令来重新启动。此外,docker container restart
命令会将一个运行态的容器终止,然后再重新启动它。
进入容器
在使用参数-d
时,容器启动后会进入后台。某些时候需要进入容器进行操作,包括使用 docker attach
命令或 docker exec
命令,常用的是docker attach
与docker exec
。
attach命令
可以使用docker attach
命令进入容器:
exec命令
-i -t参数
docker exec
后面可以使用多个参数,主要是-i
与-t
。
只使用-i
参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回。
当-i
、-t
一起使用时,就可以在交互式界面使用Linux命令:
如果从这个交互式页面中exit
,不会导致容器的停止。
所以docker exec
比docker attach
更常用。
导入和导出
导出容器
如果要导出本地某个容器,可以使用docker export
命令
这样将到处容器快照到本地文件。
导入容器快照
使用docker import
可以从容器快照文件中再导入为镜像:
1 | cat ubuntu.tar | docker import - test/ubuntu:v1.0 |
结果如下:
此外也可以通过指定URL或者某个目录来导入,比如:
1 | docker import http://example.com/exampleimage.tgz example/imagerepo |
事实上,我们既可以使用 docker load
来导入镜像存储文件到本地镜像库,也可以使用 docker import 来导入一个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态),而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时可以重新指定标签等元数据信息。
删除
删除容器
如果要删除一个正处于终止状态的容器,可以使用:
1 | docker container rm ubuntu:18.04 |
如果要删除一个运行中的容器,可以添加 -f
参数。Docker 会发送 SIGKILL
信号给容器。
清理所有处于终止状态的容器
用docker container ls -a
可以查看所有已经创建的包括终止状态的容器,可以通过一条命令清理所有处于终止状态的容器:
1 | docker container prune |
访问仓库
仓库(Repository)是集中存放镜像的地方。
与注册服务器(Registry)不同的是,注册服务器是管理仓库的具体服务器,每个服务器上可以有多个仓库,而每个仓库下面有多个镜像,仓库可以被认为是一个具体的项目或目录。例如对于仓库地址 docker.io/ubuntu
来说,docker.io
是注册服务器地址,ubuntu
是仓库名。
Docker Hub
拉取镜像
可以通过docker search
命令查找官方仓库中的镜像并利用docker pull
命令下载到本地。
例如搜索centos
:
1 | docker search centos |
可以看到返回了很多包含关键字的镜像,其中包括镜像名、描述、收藏数(表示该镜像的受关注程度)、是否官方创建(OFFICIAL)、是否自动构建(AUTOMATED)。
根据是否是官方提供,可将镜像分为两类:
一种是类似 centos
这样的镜像,被称为基础镜像或根镜像。这些基础镜像由 Docker 公司创建、验证、支持、提供。这样的镜像往往使用单个单词作为名字。
还有一种类型,比如 ansible/centos7-ansible
镜像,它是由 Docker Hub 的注册用户创建并维护的,往往带有用户名称前缀。可以通过前缀 username/
来指定使用某个用户提供的镜像,比如 ansible
用户。
另外,在查找的时候通过 --filter=stars=N
参数可以指定仅显示收藏数量为 N
以上的镜像。
下载官方centos
镜像到本地:
1 | docker pull centos |
推送镜像
用户也可以在登录后通过 docker push
命令来将自己的镜像推送到 Docker Hub。
如果注册了Docker账号,就可以更换username
来推送:
1 | $ docker tag ubuntu:18.04 username/ubuntu:18.04 |
自动构建
2021 年 7 月 26 日之后,该项功能仅限付费用户使用。
自动构建(Automated Builds)可以自动触发构建镜像,方便升级镜像。
有时候,用户构建了镜像,安装了某个软件,当软件发布新版本则需要手动更新镜像。
而自动构建允许用户通过 Docker Hub 指定跟踪一个目标网站(支持 GitHub 或 BitBucket)上的项目,一旦项目发生新的提交 (commit)或者创建了新的标签(tag),Docker Hub 会自动构建镜像并推送到 Docker Hub 中。
配置自动构建包括以下步骤:
- 登录 Docker Hub;
- 在 Docker Hub 点击右上角头像,在账号设置(Account Settings)中关联(Linked Accounts)目标网站;
- 在 Docker Hub 中新建或选择已有的仓库,在 Builds 选项卡中选择 Configure Automated Builds;
- 选取一个目标网站中的项目(需要含 Dockerfile)和分支;
- 指定 Dockerfile 的位置,并保存。
之后,可以在 Docker Hub 的仓库页面的 Timeline 选项卡中查看每次构建的状态。
私有仓库
有时使用Docker Hub这种公共仓库不方便,所以可以创建一个本地仓库。
docker-registry
是官方提供的工具,可以用于构建私有的镜像仓库。
安装运行docker-registry
容器运行
可以使用官方registry
镜像运行:
1 | docker run -d -p 5000:5000 --restart=always --name registry registry |
-d
:让容器在后台运行(以分离模式)。-p 5000:5000
:将主机的端口5000映射到容器的端口5000。它建立了主机和容器之间的网络连接,使得外部应用可以通过主机上的端口5000访问容器内的服务。--restart=always
:这个选项确保容器在任何原因导致停止时会自动重新启动。--name registry
:这个选项为运行中的容器分配了名称”registry”。registry
:这是正在使用以创建容器的Docker镜像的名称。
默认情况下,仓库会被创建在容器的/var/lib/registry
目录下。我们可以通过-v
参数将镜像文件存放在本地的指定路径,比如下面的例子将上传的镜像放到本地的/opt/data/registry
目录:1
docker run -d -p 5000:5000 -v /opt/data/registry:/var/lib/registry registry
在私有仓库上传、搜索、下载镜像
创建好私有仓库后,就可以使用docker tag
来标记一个镜像,然后推送它到仓库。
通过容器名查看私有仓库地址:
1 | docker exec registry hostname -i |
所以私有仓库地址为172.17.0.2:5000
。
先在本机查看已有的镜像:
使用docker tag
将ubuntu:latest
这个镜像标记为172.17.0.2:5000/ubuntu:latest
:
1 | docker tag ubuntu:latest 172.17.0.2:5000/ubuntu:latest |
重新查看一下:
使用docker push
上传标记的镜像:docker push 172.17.0.2:5000/ubuntu:latest
遇到问题:
无法推送镜像。
查询得知,Docker 默认不允许非 HTTPS 方式推送镜像。我们可以通过 Docker 的配置选项来取消这个限制,或者查看下一节配置能够通过 HTTPS 访问的私有仓库。
解决方法:对于使用 systemd 的系统,在 /etc/docker/daemon.json 中写入如下内容(如果文件不存在则新建该文件):
1 | { |
或者使用127.0.0.1:5000
作为私有仓库地址。
使用使用docker tag
将ubuntu:latest
这个镜像标记为127.0.0.1:5000/ubuntu:latest
,格式为docker tag IMAGE[:TAG] [REGISTRY_HOST[:REGISTRY_PORT]/]REPOSITORY[:TAG]
:
1 | docker tag ubuntu:latest 172.17.0.2:5000/ubuntu:latest |
使用docker push
上传标记的镜像:
1 | docker push 127.0.0.1:5000/ubuntu:latest |
用curl
查看仓库中的镜像:
1 | curl 127.0.0.1:5000/v2/_catalog |
可以看到{"repositories":["ubuntu"]}
,说明镜像已经被成功上传了。
先删除已有镜像,再尝试从私有仓库中下载这个镜像:
1 | docker image rm 127.0.0.1:5000/ubuntu:latest |
私有仓库高级配置
待续
Nexus 3
待续
数据管理
数据卷
数据卷是一个可供一个或多个容器使用的特殊目录,绕过UnionFS,可以提供很多有用的特性:
- 数据卷可以在容器之间共享和重用
- 对数据卷的修改会立刻生效
- 对数据卷的更新不会影响镜像
- 数据卷默认会一直存在即使容器被删除。
创建一个数据卷
创建一个名为my-vol
的数据卷
1 | docker volume create my-vol |
查看所有的数据卷:
1 | docker volume ls |
使用如下命令可以查看指定数据卷的信息:
1 | docker volume inspect my-vol |
启动一个挂载数据卷的容器
在使用docker run
命令的收,使用--mount
标记将数据卷挂载到容器里。在一次docker run
中可以挂载多个数据卷。
创建一个名为web
的容器,并加载一个数据卷到容器的/usr/share/nginx/html
目录:
1 | $ docker run -d -P --name web --mount source=my-vol, target=/usr/share/nginx/html nginx:alpine |