Docker基础知识
Docker的优势在于一次创建或配置,便可在任意地方正常运行。使用一个标准镜像构建一套开发容器,在开发完成后,其他人便可使用这个容器来部署代码。
相比传统的虚拟机VM是在硬件层面实现虚拟化,Docker是在操作系统层面实现了虚拟化,直接复制 localhost 的操作系统
Docker 三大组件
镜像
Docker镜像就是一个只读模板,镜像可以用来创建Dockers容器。
容器
容器是从镜像创建的运行实例,Docker 利用容器来运行应用。每个容器之间互相隔离,虽然镜像是只读的,但是容器在启动的时候会创建一层可写层作为最上层。
仓库
仓库是集中存放镜像文件的场所,与 git
类似。除了网络上的公开仓库,用户也可以在本地网络内创建一个私有仓库。
用户可以使用 push
将自己镜像上传到仓库,也可以使用 pull
指令将仓库的镜像下载。
Docker镜像
Docker 运行容器前需要本地存在对应的镜像,如果镜像不存在本地,Docker 会从镜像仓库下载。
获取镜像
使用 docker pull
命令可以从仓库拉取所需镜像:
docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
- Docker 镜像仓库地址:地址的格式一般是
<域名/IP>[:端口号
]。默认地址是 Docker Hub(docker.io
)。 - 仓库名:如之前所说,这里的仓库名是两段式名称,即
<用户名>/<软件名>
。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。
比如可以用该指令从Docker Hub拉取一个Ubuntu操作系统的镜像。
# 因为我的配置问题,我的docker相关命令必须使用sudo获取root权限才可以运行
$ sudo docker pull ubuntu:20.04
20.04: Pulling from library/ubuntu
915eebb74587: Pull complete
Digest: sha256:ed4a42283d9943135ed87d4ee34e542f7f5ad9ecf2f244870e23122f703f91c2
Status: Downloaded newer image for ubuntu:20.04
docker.io/library/ubuntu:20.04
上面的命令中没有给出 Docker 镜像仓库地址,因此将会从 Docker Hub 获取镜像。而镜像名称是 ubuntu:20.04
,因此将会获取官方镜像 library/ubuntu
仓库中标签为 20.04
的镜像。docker pull
命令的输出结果最后一行给出了镜像的完整名称,即:docker.io/library/ubuntu:20.04
列出镜像
可以使用 docker image ls
命令来列出已经下载完成的镜像。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 00285df0df87 5 days ago 342 MB
ubuntu 20.04 0341906bdafc 4 weeks ago 65.7MB
列表中包含了:
- 仓库名:来自哪个仓库,如 ubuntu
- 标签:比如20.04
- ID号:唯一
- 创建时间
- 所占空间:这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同,是因为Docker Hub 中显示的体积是压缩后的体积。
创建镜像
关于CUDA的兼容性问题
相比CPU,显卡是一种比较特殊的硬件设备,因为它有专门的驱动。虽说正常来讲Docker内的 CUDA 版本和物理机无关,但是显卡、CUDA和驱动之间之间复杂的对应关系,让我对Docker和GPU硬件之间的兼容性打了个问号。所以Google了相关问题,看到这篇文章: CUDA兼容性问题(显卡驱动、docker内CUDA)
根据官方文档的描述:
Each release of the CUDA Toolkit requires a minimum version of the CUDA driver. The CUDA driver is backward compatible, meaning that applications compiled against a particular version of the CUDA will continue to work on subsequent (later) driver releases.
也就是说,CUDA 11.x 以后可以对编译后的程序进行向下兼容,也就是程序编译发布后,可以升级CUDA。并给出了最低驱动版本 450.80.02。通过分析CUDA的SDK组件:
- CUDA Toolkit:库文件、运行环境和开发工具,可以理解为面向开发者 CUDA编译环境。
- CUDA Driver:用户驱动组建,用于运行 CUDA 程序,可以理解为 CUDA运行环境。
- Nvidia GPU 驱动:显卡核心驱动,就是硬件驱动。
顺便一提,CUDA的版本遵循 X.Y.Z
的命名方式。
- X:大版本,会改变API,不保证二进制兼容性和编译环境
- Y:也会增删新函数和API,但是二进制兼容性能够保证,不保证编译环境
- Z:补丁版本,编译环境和二进制兼容性都保证
然后根据另一段描述 Using a Minimum Required Version that is different from Toolkit Driver Version could be allowed in compatibility mode -- please read the CUDA Compatibility Guide for details
所谓向后兼容,是依靠提供的兼容模式,来向后兼容运行环境,
Docker 是轻量级容器(不同于VM虚拟机),Docker内包含CUDA,但不包含 Nvidia GPU驱动。因此会存在虽然满足Nvidia最小驱动版本,但是无法使用CUDA Toolkit 编译环境的问题。
总而言之,宿主机(物理主机)的Nvidia GPU 驱动 必须大于 CUDA Toolkit要求的 Nvida GPU 驱动版本。否则 对于开发者 CUDA Toolkit就不能用(仅是运行环境向后兼容)。所以宿主机(物理机)的 Nvidia GPU 驱动选高版本,更容易兼容你Docker内不同CUDA的驱动要求。下面是例子
宿主机 GPU 驱动 | Docker内Cuda版本 | 兼容性 | 备注 |
---|---|---|---|
455.23 | 11.1 | 兼容 | |
455.23 | 11.2 | 不兼容 | 11.2 >= 460.27.03 |
455.23 | 11.3 | 不兼容 | 11.3 >= 460.19.01 |
470.94 | 11.1 | 兼容 | |
470.94 | 11.2.x | 兼容 | |
470.94 | 11.3.x | 兼容 | |
470.94 | 11.5.x | 不兼容 | 11.5 >= 495.29.05 |
因此,即使Docker的CUDA和主机无关,但是Docker和宿主机的驱动有关,为了保证CUDA Toolkit的Nvidia GPU 驱动要求,主机需要升级 Nvidia GPU 驱动。
从上图也可以看出,容器中仅包含CUDA Toolkit,但是不包含CUDA Driver。
修改已有的镜像
不要使用 docker commit
定制镜像,定制镜像应该使用 Dockerfile
。大部分情况下使用的都是来自于Docker Hub的镜像,但有时这些镜像不能满足使用要求,因此需要定制这些镜像。
镜像是多存存储,每层是在上层的基础上进行修改;容器也是多层存储,在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。 Docker的 docker commit
命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。
docker commit
语法格式为:
sudo docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]
虽然使用 commit
命令可以直观的了解镜像分层的概念,但是一般生产环境中并不会这样使用。如果使用这个命令安装软件包或编译构建,除当前层外,之前的层是不会发生改变的,任何修改都是在当前层上进行标记、添加、修改,也就是说会有大量的无关内容被添加进来,进而导致镜像十分臃肿。
此外,commit
对镜像的操作是黑箱操作,生成的镜像被称为黑箱镜像,维护非常困难。
使用 Dockerfile 创建镜像
除了上面提到的 commit
方法来扩展现有镜像,还可以使用 docker build
方法来创建一个新的镜像。首先需要创建一个 Dockerfile
,其中包含创建镜像的指令。
Dockerfile 基本的语法是
#
注释
FROM
告诉 Docker 哪个镜像作为基础
MAINTAINER
维护者的信息
RUN
指令会在创建中运行
FROM指定基础镜像
虽然是自定义的镜像,但是也必然会以一个镜像为基础。FROM
在 Dockerfile
中必须是第一条指令。Docker Hub 中提供了很多官方镜像,也有 ubuntu 之类的操作系统镜像,此外还有一种特殊镜像 scratch
,是虚拟的概念,并不实际存在,它表示一个空白的镜像,即不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
RUN执行命令
RUN
命令有两种格式:
- Shell格式:
RUN <命令>
与直接在terminal中输入的命令一样。 - exec格式:
RUN ["可执行文件", "参数1", "参数2"]
,类似函数调用。
每个 RUN
命令会新建一层,因此注意不要用下面的写法:
FROM debian:stretch
RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install
正确写法如下:
FROM debian:stretch
RUN set -x; buildDeps='gcc libc6-dev make wget' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
&& make -C /usr/src/redis \
&& make -C /usr/src/redis install \
&& rm -rf /var/lib/apt/lists/* \
&& rm redis.tar.gz \
&& rm -r /usr/src/redis \
&& apt-get purge -y --auto-remove $buildDeps
使用 &&
将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。这里为了格式化还进行了换行。Dockerfile
支持 Shell 类的行尾添加 \
的命令换行方式,以及行首 #
进行注释的格式。
还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt
缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。
NOTE:镜像有层数限制,一般来说不超过127层。
构建镜像
在 Dockerfile
所在目录执行:
$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM nginx
---> e43d811ce2f4
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
---> Running in 9cdc27646c7b
---> 44aa4490ce2c
Removing intermediate container 9cdc27646c7b
Successfully built 44aa4490ce2c
其中 -t
指令用来添加 tag,随后的 :
前后的两部分是镜像名称 REPOSITORY:TAG
镜像构建的上下文(Context)
注意最后的 .
不要遗漏,可以理解为当前目录即 ,不过这样理解是错误的。实际上这是在指定上下文路径。Dockerfile
所在目录
Docker build 的工作原理:Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。当我们进行镜像构建的时候,并非所有定制都会通过 RUN 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令、ADD 指令等。而 docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
不理解上下文导致的问题
如果在 Dockerfile
中这么写:
COPY ./package.json /app/
既不是复制执行 docker build
命令所在目录下的 json
,也不是复制 Dockerfile
目录下的 json
,而是在复制上下文目录下的 json
。所以 COPY
这类指令的文件都是相对路径。
因此,在 docker build
时末尾的这个 .
实际上是在指定上下文的目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。如果把 Dockerfile
放到根目录下去构建,会发现在发送一个几十G的东西,因为此时正在将根目录下的所有内容打包。
其他 docker build
方法
- 用 Git repo 进行构建
- 用给定的 tar 压缩包构建
- 从标准输入中读取 Dockerfile 进行构建
- 从标准输入中读取上下文压缩包进行构建
更多 Dockerfile
指令
删除镜像
如果要移除本地的镜像,可以使用 docker rmi
命令。注意 docker rm
命令是移除容器。
在删除镜像之前要先用 docker rm
删掉依赖于这个镜像的所有容器。
导出和导入镜像
如果要导出镜像到本地文件,使用 docker save
命令。
$sudo docker save -o ubuntu_20.04.tar ubuntu:20.04
如果要导入本地文件到本地镜像库,用 docker load
。这将导入镜像以及其相关的元数据信息(包括标签等)。
$ sudo docker load --input ubuntu_20.04.tar
Docker容器
启动容器
启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(exited)的容器重新启动。因为 Docker 的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器。
新建并启动容器
命令为 docker run
$ docker run -t -i ubuntu:20.04 /bin/bash
root@cf4c7a2a2e47:/usr/local#
其中,-t
选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i
则让容器的标准输入保持打开。在交互模式下,用户可以通过所创建的终端来输入命令。
当利用 docker run 来创建容器时,Docker 在后台运行的标准操作包括:
- 检查本地是否存在指定的镜像,不存在就从 registry 下载
- 利用镜像创建并启动一个容器
- 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
- 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
- 从地址池配置一个 ip 地址给容器
- 执行用户指定的应用程序
- 执行完毕后容器被终止
启动已终止容器
可以利用 docker start
命令,直接将一个已经终止(Exited)的容器启动运行。
守护态运行
一般情况下, Docker 容器会在后台以守护态(Daemonized)形式运行。容器启动后会返回一个唯一的 id,也可以通过 docker ps
命令来查看容器信息。
守护态能够:
- 能够长期运行
- 没有交互式会话
- 适合运行应用程序和服务
可以通过添加 -d
参数来实现,此时容器会在后台运行并不会把输出的结果 (stdout) 打印到宿主机上面(输出结果可以用 docker logs
查看)。
终止
可以使用 docker stop
来终止一个运行中的容器,终止状态的容器可以用 docker ps -a
命令看到。
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cf4c7a2a2e47 ubuntu:20.04 "/bin/bash" 40 minutes ago Exited (0) About a minute ago kind_hodgkin
处于终止状态的容器,可以通过 docker start
命令来重新启动。
docker restart
命令会将一个运行态的容器终止,然后再重新启动它。
进入容器
attach
命令
$ sudo docker run -idt dockertest:v0
b64c0db1a9263d239d72ff38b85b5ef7ffe6b1e4c0bdb7a91b80069e31313a12
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b64c0db1a926 dockertest:v0 "/bin/sh -c /bin/bash" 8 seconds ago Up 8 seconds 80/tcp vigilant_poincare
cf4c7a2a2e47 dockertest:v0 "/bin/bash" 2 hours ago Up 2 hours 80/tcp kind_hodgkin
$ sudo docker attach vigilant_poincare
root@b64c0db1a926:/usr/local#
但是使用 attach
命令有时候并不方便。当多个窗口同时 attach
到同一个容器的时候,所有窗口都会同步显示。当某个窗口因命令阻塞时,其他窗口也无法执行操作了。
如果从这个 stdin 输入 exit
,会导致容器的停止。
exec
命令
docker exec
后边可跟多个参数,主要说明 -i
和 -t
参数。
只用 -i
参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回。
当 -i
和 -t
参数一起使用,则可看到 Linux 命令提示符。
$ sudo docker run -idt dockertest:v0
a39344a590d28e46e5e5960856b120e83cc81d8c0003538b4ff402c91b408d4c
$ sudo docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a39344a590d2 dockertest:v0 "/bin/sh -c /bin/bash" 12 seconds ago Up 10 seconds 80/tcp inspiring_kalam
cf4c7a2a2e47 dockertest:v0 "/bin/bash" 2 hours ago Up 2 hours 80/tcp kind_hodgkin
$ sudo docker exec -i a393 bash
ls
bin
etc
games
include
lib
man
sbin
share
src
^C
$ sudo docker exec -it a393 bash
root@a39344a590d2:/usr/local#
如果从这个 stdin 中 exit
,不会导致容器的停止,因此推荐使用 exec
方法。
导入和导出
既可以使用 docker load
来导入镜像存储文件到本地镜像库,也可以使用 docker import
来导入一个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态),而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时可以重新指定标签等元数据信息。
导出容器
如果要导出本地某个容器,可以使用 docker export
命令,会将导出容器快照到本地文件。
docker export a39344a590d2 > ubuntu.tar
会存储在 home
目录下。
导入容器
可以使用 docker import
从容器快照文件中再导入为镜像。
$ cat ubuntu.tar | docker import - test/ubuntu:v1.0
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
test/ubuntu v1.0 9d37a6082e97 About a minute ago 171.3 MB
删除容器
可以使用 docker container rm
来删除一个处于终止状态的容器。例如
清理所有处于终止状态的容器
用 docker container ls -a
命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器。
$ docker container prune
仓库
一个容易混淆的概念是注册服务器(Registry),虽然大部分时候,并不需要严格区分这两者的概念。
实际上注册服务器是管理仓库的具体服务器,每个服务器上可以有多个仓库,而每个仓库下面有多个镜像。从这方面来说,仓库可以被认为是一个具体的项目或目录。例如对于仓库地址 dl.dockerpool.com/ubuntu 来说,dl.dockerpool.com 是注册服务器地址,ubuntu 是仓库名。
参考文章
[1] https://doc.yonyoucloud.com/doc/docker_practice/introduction/what.html
[2] https://blog.csdn.net/Apeopl/article/details/105449362
[3] https://monsoir.github.io/Notes/Docker/create-your-own-docker-image.html
[4] https://yeasy.gitbook.io/docker_practice/image/pull