¶简介
Docker的诞生,让应用的部署变得前所未有的高效,它能将应用及其依赖项打包成容器分发部署,从而保证了应用运行环境的一致性。Docker容器其实是一种比虚拟机更轻量的技术,容器中的进程直接运行在宿主机的内核,其启动速度十分快,基本可以做到秒级启动,并不像虚拟机那样对硬件进行模拟,并在之上运行一整套操作系统,所以容器相比虚拟机更为轻便。
¶理解Docker
Docker有三个基本概念:仓库(Repository),镜像(Image)和容器(Container)。
- 仓库(Repository) 是一个集中存放镜像的空间。我们写的代码可以上传到Github仓库中,类似的,Docker的镜像就能上传到Docker Hub仓库,以便镜像的分发部署。Docker Hub是官方的公开服务,每个账号可以建立一个免费的私有仓库。
- 镜像(Image) 是一个特殊的文件系统,其中存储了应用和环境的所有数据,镜像在构建之后是静态的,不可改变的。
- 容器(Container) 是镜像的运行实体,类似于面向对象编程中的类与实例,一个静态的镜像可以产生多个独立动态运行的容器。我们实际使用Docker就是在容器中运行自己的应用,每个容器都有自己独立的运行空间,与宿主系统环境隔绝。
¶快速上手Docker
为了让读者能快速上手Docker,下面以一个实际应用进行说明,让我们从易到难,先假设目前已经构建好一个镜像,后面我们会具体讲如何构建这个镜像。读者可以跟随本文实际操作,学习效果会更佳。
¶一、安装Docker
本文以Ubuntu系统安装Docker CE版本为例,更详细的其他版本安装步骤可以参考Docker官网文档
- 卸载旧版本
1 | sudo apt-get remove docker docker-engine docker.io containerd runc |
- 安装依赖包
1 | sudo apt-get update |
- 添加GPG key,并设置
stable
版本的仓库
1 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - |
- apt-get安装
1 | sudo apt-get update |
- 验证安装结果
1 | sudo docker run --rm hello-world |
--rm
参数代表我们运行后随即删除这个容器,因为我们只需要这个容器输出一段信息而已。
执行后会输出类似这样的信息,说明已经安装成功:
Hello from Docker!
This message shows that your installation appears to be working correctly.To generate this message, Docker took the following steps:
- The Docker client contacted the Docker daemon.
- The Docker daemon pulled the “hello-world” image from the Docker Hub.
(amd64)- The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.- The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bashShare images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/For more examples and ideas, visit:
https://docs.docker.com/get-started/
- 免sudo运行
每次运行docker都要加一个sudo挺麻烦,我们将当前账号加入Docker组里面即可免sudo运行Docker:
1 | sudo groupadd docker |
¶二、镜像操作
¶1、获取Docker镜像
笔者在Ubuntu 16.04镜像的基础上,制作了一个很简单的Docker镜像,里面有一个my_clock.py
Python文件,执行后就会间隔一秒钟输出一次当前系统时间。此镜像我已经上传到自己的Docker Hub公共仓库中,任何人都可以随时下载,执行以下命令即可将此镜像下载到本地:
1 | docker pull chasonlee/ubuntu_demo:latest |
其中chasonlee/ubuntu_demo
是镜像名字,latest
是镜像的标签Tag,不同的Tag代表不同的版本,执行后会显示如下信息:
1 | latest: Pulling from chasonlee/ubuntu_demo |
从信息中我们可以看出,镜像的下载是一层一层分开的,镜像是分层存储,9ff7e2e5f967
是其中一层。Docker有一个优点,不同镜像之间如果基于某些相同的镜像进行定制化修改,就可以共享部分相同的层,从而节省空间的占用。
¶2、查看本地镜像
执行以下命令可以查看当前所有本地镜像:
1 | docker images |
执行后就能看到我们刚刚pull回来的镜像和一个之前的hello-world镜像:
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
可能读者会疑惑为什么有hello-world镜像,因为当我们执行docker run --rm hello-world
的时候,在本地找不到hello-world镜像,就会自动到Docker Hub上找到相应镜像下载回来,再根据此镜像来新建并启动容器。
另外,我们也可以查看中间层镜像:
1 | docker images -a |
如果不同镜像之间有复用的中间层,这里就会看见一些没有镜像名和标签的镜像,很多镜像依赖这些中间层,所以中间层镜像是不能随意删除的。
¶3、删除镜像
当我们想删除一些不需要的镜像时,比如hello-world
镜像,可以执行:
1 | docker rmi hello-world |
如果此时有基于此镜像的容器,则需要先删除相应的容器才能删除此镜像,如果想强制删除镜像,加上-f
参数即可:
1 | docker rmi -f <image name> |
在使用docker images
查看本地镜像时(不加-a
参数时),我们也可能会发现一些没有镜像名和标签的镜像:
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
不同于上面提到的中间层镜像,我们称这种镜像为虚悬镜像(dangling image),有几种情况会产生虚悬镜像,比如上述强制删除一个已经运行容器的镜像,或者使用docker pull
命令更新镜像时,镜像的名称和标签会转移到新镜像中,旧的镜像就会变成虚悬镜像,另外,在使用docker build
构建镜像的时候,如果构建失败也会产生虚悬镜像。一般来说虚悬镜像已经没有实际用处,可以随意删除,一条命令就能清除所有虚悬镜像:
1 | docker image prune |
¶4、导出镜像
上文中我们用docker pull
命令将镜像从Docker Hub下载到本地,如果目标环境不能访问外网时无法下载,我们就可以直接导出镜像文件:
1 | docker save -o ubuntu_demo.tar chasonlee/ubuntu_demo |
-o
参数后面接着输出文件名。chasonlee/ubuntu_demo
是需要导出的镜像名。
注意:这里只是导出一个静态的镜像,根据当前镜像启动的容器环境并不能直接导出,如果需要迁移当前容器的环境,还需要先使用commit
命令自行制作一个镜像再导出,详情可跳转到理解commit章节阅读。
假如镜像文件很大,可以直接压缩导出镜像:
1 | docker save chasonlee/ubuntu_demo:latest | gzip > ubuntu_demo.tar.gz |
¶5、导入镜像
然后拷贝此镜像文件到目标环境中,并导入镜像:
1 | docker load -i ubuntu_demo.tar |
或者直接导入压缩后的镜像:
1 | gunzip -c ubuntu_demo.tar.gz | docker load |
导入后可以即可通过docker images
查看镜像。
¶6、修改镜像名称及标签
如果我们想把镜像名称改为ubuntu_chason:1.0
,则执行:
1 | docker tag chasonlee/ubuntu_demo:latest ubuntu_chason:1.0 |
¶三、容器操作
¶1、新建并启动容器
前面我们有讲过,镜像是静态的,容器才是实际运行的实体,Docker可以基于某个镜像新建容器。可以这么理解,用一个镜像来初始化新建的容器。容器运行之后的所有变化都不会反过来影响镜像,容器在运行终止后,里面新产生的数据将会随之丢弃。
为了方便,我们通常会新建容器并让其后台持续运行:
1 | docker run -dit --name my_ubuntu chasonlee/ubuntu_demo:latest |
-dit
有三个参数,其顺序没有影响,-t
能让Docker分配一个伪终端,并绑定到容器的输入上,-i
能让容器的标准输入保持打开状态,-d
则可以让容器在后台保持运行。--name my_ubuntu
可以为容器命名,方便自己管理。chasonlee/ubuntu_chason:latest
是初始化容器的镜像名加标签Tag。
执行后就会打印一行此容器的ID,说明容器已经成功在后台运行。
另外,我们也可以在让容器一次性运行一条命令:
1 | docker run --rm chasonlee/ubuntu_demo:latest echo "hello world" |
--rm
我们之前解释过,是为了容器停止后直接删除,避免留下一个已经停止的容器。
这条命令看起来和我们在本地直接运行echo "hello world"
并没有什么区别,我们只是在容器中执行了echo指令,输出结果,容器就停止了。
或者我们也可以直接运行容器的bash终端:
1 | docker run -it --rm chasonlee/ubuntu_demo:latest bash |
执行本命令就会看到容器的伪终端,此时我们就继续在容器中执行命令,要注意的是,当我们执行exit
退出容器时,容器就会停止,--rm
参数会在容器停止后直接删除容器。
¶2、查看容器
执行以下命令可以查看所有容器及其状态:
1 | docker ps -a |
执行后会输出以下类似信息:
1 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
可见my_ubuntu就是我们新建好的容器,Up 2 minutes
代表容器正在后台运行,已经运行2分钟。
¶3、进入容器
执行以下命令即可进入容器中的bash:
1 | docker exec -it my_ubuntu bash |
-it
和前面一样代表打开标准输入和分配伪终端。my_ubuntu
是要进入容器的名字,我们如果换成容器IDd346805da373
也是一样的,但使用名字肯定更方便直观。- 最后的参数代表我们需要容器执行的命令,我们需要交互式的Shell,所以这里输入
bash
,如果我们只需要执行容器里面的一个命令,可以将bash
替换成想执行的命令即可。
执行后即可进入容器中:
或者执行docker attach my_ubuntu
也可以进入容器,但不推荐使用,原因后面会讲。
容器里面是独立的系统环境,我已经配置默认采用Python3,并在workspace中存放my_clock.py文件,执行:
1 | python my_clock.py |
程序会每隔一秒在界面中打印系统时间:
我还在容器中安装了htop
工具,用于查看CPU、内存使用情况,执行htop
即可查看:
简单的程序可以如上直接在容器中运行,虽然我们在运行容器时加入了-d
参数能保持程序一直运行,但我们无法在重新进入容器时看到程序以前输出的结果。如果需要长时间保持程序的运行,笔者还是推荐在容器中使用tmux,tmux教程在这里。
¶4、退出容器
退出容器很简单,直接执行:
1 | exit |
即可回到系统bash中,现在再用docker ps -a
查看容器状态,依旧是Up
运行状态:
1 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
这是因为我们进入容器时使用的是docker exec
命令,如果我们使用的是docker attach
命令,退出容器后,容器将会停止运行:
1 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
¶5、启动停止容器
容器停止时,可以启动容器:
1 | docker start my_ubuntu |
容器正在运行时,也可以停止容器:
1 | docker stop my_ubuntu |
¶6、删除容器
当容器已经停止时,可以直接删除容器:
1 | docker rm my_ubuntu |
当容器还在运行,可以强制删除容器:
1 | docker rm -f my_ubuntu |
如果当前有很多已经停止的容器,一个命令即可清空所有已停止容器:
1 | docker container prune |
¶7、导出容器快照(不推荐)
类似镜像的导出,我们同样可以导出容器快照:
1 | docker export my_ubuntu > my_ubuntu.tar |
容器快照会丢弃所有的历史记录和元数据信息,仅保存容器当时的快照状态。
¶8、导入容器快照(不推荐)
有了容器快照文件,即可将容器快照导入新的镜像中:
1 | docker import my_ubuntu.tar my_ubuntu:1.0 |
my_ubuntu.tar
是容器快照。my_ubuntu:1.0
是新镜像名和标签TAG,若不写TAG,默认为latest
。
注意,是导入到镜像中,而不是直接新建一个容器。
¶9、挂载目录
当我们需要访问宿主机的数据时,就需要把宿主机目录挂载到容器中,在新建容器时使用docker run -v
参数即可挂载目录,例如:
1 | docker run -dit --name my_ubuntu -v /hdd:/workspace/hdd chasonlee/ubuntu_demo:latest |
-v
代表挂载目录,宿主机目录和容器目录用:
分开,如果需要挂载多个目录,继续多写几个-v
及其对应目录即可。/hdd
是宿主机目录,必须是绝对路径。/workspace/hdd
是容器里面映射的目录,必须是绝对路径,如果目录不存在会自动创建。
至此阶段,相信读者已经掌握了docker的基本使用方法,接下来将会介绍如何制作自定义的镜像。
¶镜像制作
在Docker Hub中,除了笔者举例的镜像,我们还可以搜索到其他很多高质量的镜像,例如Ubuntu官方镜像、TensorFlow官方镜像等等。有了这些镜像,我们就没有必要重复造轮子,若这些镜像无法完全满足需求,我们只需要基于这些镜像做一些定制化的修改即可。
¶一、理解commit
镜像的制作,有一种最简单快速的方式是docker commit
,只需在某个镜像基础上,启动容器,并在容器中安装自己需要的环境,即可将此容器commit到一个新的镜像中。
基本语法:
1 | docker commit [可选项] 容器 [镜像[:标签]] |
可选项
包括:
-a (–author string): 镜像制作者信息,如"chason.me"
-c (–change list): 使用Dockerfile指令来创建镜像
-m (–message string): 提交时的说明信息
-p (–pause): 在commit时,将容器暂停,默认为True容器
可以写容器名或容器ID镜像[:标签]
代表你要新建的镜像和标签名
假如我们已经在容器my_ubuntu
中安装好一切需要的环境,退出容器后,执行:
1 | docker commit -a "chason.me" -m "new environment" my_ubuntu chason_env:latest |
即可创建一个名为chason_env:latest
镜像,导出此镜像即可将环境迁移至其他机器使用。
需要提醒的是,docker commit
虽然很方便,但并不推荐使用,因为在容器中的每一次操作我们都可能产生很多临时文件,造成镜像越来越臃肿,而且每次操作无法回溯,我们很难知道这个镜像到底执行过什么命令,导致镜像难以维护。
那是否存在一种方法,能让镜像的修改操作透明、同时又能解决臃肿问题呢?当然有,答案就是Dockerfile!
¶二、学习Dockerfile
Dockerfile是一个文本文件,包含了镜像构建的所有命令,通过修改Dockerfile中的命令,就能定制化自己想要的镜像。Dockerfile里面每一个指令都会构建一层镜像,层层叠加最终得到定制化镜像。
¶1、基本指令
让我们重新回到原来的例子,chasonlee/ubuntu_demo
镜像是如何构建的呢?
首先我们在本地新建一个文件夹,专门存放Dockerfile和其他所需文件,并在里面新建my_clock.py
程序以及Dockerfile
文件:
1 | mkdir my_docker |
在Python文件中输入持续打印当前时间的代码:
1 | # coding: utf-8 |
保存代码后,在同样的目录下新建Dockerfile文件:
1 | vim Dockerfile |
输入以下Dockerfile指令:
1 | FROM ubuntu:16.04 |
FROM
指令代表基于哪个镜像进行修改,第一条指令必须是FROM
指令,若我们不想基于任何镜像,可以写FROM scratch
即可完全从零开始构建镜像。ENV
指令可以创建环境变量,比如WORK_DIR
工作目录环境变量,后面就能通过${WORK_DIR}
调用环境变量的值。COPY
指令可以将宿主机中的文件在构建镜像时复制到镜像存储中。WORKDIR
指令可以指定工作目录,在刚进入容器时,系统会自动转到工作目录,默认的工作目录是根目录/
。RUN
指令就是用来执行命令的指令,由于一条指令就会创建一层镜像,而镜像层数是有限制的,一般是127层,当我们需要执行多条命令时,一般都用&&
连接多条命令,从而节省镜像层数。
现在我们逐条命令讲解以上Dockerfile的内容:
- 首先是基于
ubuntu:16.04
镜像开始构建 - 设置环境变量WORK_DIR为
/workspace
- 设置工作目录为WORK_DIR
- 复制宿主机当前目录所有文件到镜像WORK_DIR目录中,包含
Dockerfile
和my_clock.py
- 更新apt列表
- 安装Python3,
-y
代表默认同意安装,这样就不需要手动输入y确认安装。 - 安装htop工具
- 安装tzdata时间工具
- 清除临时软件包
- 清除临时软件包
- 清除临时文件
- 将python软连接到python3
- 将系统默认时区设置为亚洲上海
- 将系统默认时区设置为亚洲上海
从Dockerfile可见,整个镜像的构建过程都是透明的、清晰的,为了避免镜像过于臃肿,需要时刻记得清除无用的数据,否则镜像的大小容易失去控制。
定制自己的镜像就是写一份Dockerfile,把想要的程序或工具的安装命令直接写入Dockerfile即可,比如想用tmux的读者,在Dockerfile里面插入apt install -y tmux
即可。
¶2、拓展指令
¶ADD指令
ADD
指令和COPY
类似,但包含更多功能,比如可以从一个网址下载文件到目标目录中(下载后文件默认权限是600
),另外一个常用的功能是自动解压,支持gzip、bzip2和xz压缩格式,比如ADD file.tar /
会将压缩包解压到目标路径中。
由于ADD
指令语义不够清晰,除了需要自动解压的情况,我们一般都不推荐使用ADD
指令。
¶CMD指令
运行格式:CMD ["可执行文件", "参数1", "参数2"...]
CMD
指令可以用来指定容器默认的运行命令,比如之前我们提到可以这样直接执行容器的bash指令:
1 | docker run -it --rm chasonlee/ubuntu_demo:latest bash |
其实后面的bash
不写也行,因为ubuntu:16.04
镜像中已经用CMD指令设置好默认命令为bash
。
若我们在Dockerfile中加入CMD ["python","my_clock.py"]
,重新构建镜像后,运行容器时后面不加任何命令,就会默认执行python my_clock.py
:
1 | docker run -it --rm chasonlee/ubuntu_demo:latest |
执行后就会每隔一秒钟输出一次系统时间。若我们在后面加入其它命令,就会替换默认的命令。
敬请期待…
ENTRYPOINT
ARG
VOLUME
EXPOSE
USER
HEALTHCHECK
ONBUILD
¶三、构建镜像
完成Dockerfile之后,便可在Dockerfile的目录中直接构建镜像:
1 | docker build -t ubuntu_demo:latest . |
ubuntu_demo:latest
是构建的镜像名和标签,如需上传到Docker Hub,可以修改为<user name>/<image name>:<tag>
形式。.
最后一个参数指向Dockerfile所在的目录,这里是当前目录。
执行后就会开始根据Dockerfile的内容构建镜像,需要等待一段时间,联网下载的部分命令耗时会比较长,读者如果需要安装TensorFlow之类安装包较大的库时,建议先下载好本地whl文件,用COPY
命令复制到镜像中,用RUN
命令安装并把whl文件删除。
如果构建失败,请查看Dockerfile中的命令是否有问题。如果成功,恭喜你已经掌握了基本的镜像构建方法!
执行docker images
即可查看刚刚构建成功的镜像。
¶Docker Hub使用
首先请到Docker Hub官网注册一个账号,每个账号都有一个免费的私有仓库,无限个公有仓库,私有仓库只有你自己能看到,如有需要可以购买更多私有仓库。
¶一、登陆登出Docker Hub
如果需要上传镜像到自己的Docker Hub,需要先在bash中登陆:
1 | docker login |
之后输入自己的账号密码即可。
若需要登陆其他平台的仓库,在后面加上网址即可:
1 | docker login <url> |
需要登出时可执行:
1 | docker logout |
若需要登陆其他账号,可以加入-u
参数,-p
可以输入密码:
1 | docker login -u 用户名 -p 密码 |
¶二、上传镜像
与下载镜像pull
相反,上传镜像用push
命令:
1 | docker push <user name>/ubuntu_demo:latest |
<user name>
是自己的用户名,如果构建镜像时,镜像名称前面忘记加用户名,请用docker tag
命令修改。
镜像上传需要一定时间,取决于镜像大小和网速。上传成功后镜像默认存在私有仓库中,在Setting
中Make public
即可转为公开的仓库。
¶nvidia-docker
Docker支持CUDA环境的隔绝,这是深度学习算法框架使用者的福音,因为TensorFlow的不同版本可能需要不同CUDA版本,这导致无法用virtualenv在同一台机安装多个版本跨度较大的TensorFlow,有了Docker,这种问题就能迎刃而解。
基于前面的内容,读者可能会发现,在之前启动的Docker容器里面并不能执行nvidia-smi
命令,无法使用GPU版本的TensorFlow或PyTorch。不用着急,现在我们开始学习如何配置一个带NVIDIA的Docker,只需在安装Docker的基础之上,继续安装nvidia-docker2即可,具体可参考nvidia-docker说明,主要包含以下几个步骤:
¶一、卸载旧版nvidia-docker
If you have nvidia-docker 1.0 installed: we need to remove it and all existing GPU containers
1 | docker volume ls -q -f driver=nvidia-docker | xargs -r -I{} -n1 docker ps -q -a -f volume={} | xargs -r docker rm -f |
¶二、添加包源
Add the package repositories
1 | curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | \ |
¶三、安装nvidia-docker2
Install nvidia-docker2 and reload the Docker daemon configuration
1 | sudo apt-get install -y nvidia-docker2 |
¶四、测试nvidia-docker
安装完成后,我们可以在启动容器的命令前面加上nvidia-
即可启动支持GPU的Docker。
首先,我们需要先下载一个自带CUDA的镜像,读者可以在DockerHub里面自行寻找,或尝试这个:
1 | docker pull nvidia/cuda:10.0-base-ubuntu16.04 |
镜像拉回本地后,即可使用nvidia-docker
命令创建容器:
1 | nvidia-docker run -dit --name my_ubuntu_gpu nvidia/cuda:10.0-base-ubuntu16.04 |
等同于加--runtime=nvidia
,这种新的用法只有nvidia-docker v2支持:
1 | docker run --runtime=nvidia -dit --name my_ubuntu_gpu nvidia/cuda:10.0-base-ubuntu16.04 |
为了加以区分,容器名后面加上了_gpu
,进入容器的方法不变:
1 | docker exec -it my_ubuntu_gpu bash |
现在进入容器后即可执行nvidia-smi
查看GPU状态。
注意:nvidia-docker只是能让Docker支持GPU而已,Docker镜像需要同时带有CUDA才能使用GPU,如上面的nvidia/cuda:10.0-base-ubuntu16.04
镜像,我们可以基于此定制自己的镜像。另外,也可以基于不带CUDA的镜像,先下载好CUDA安装包,然后在Dockerfile里面写好安装的命令,在构建镜像的过程中安装好CUDA(安装cuDNN也是同样的思路)。
¶结束语
我们从镜像到容器、从运行到构建、从CPU到GPU,一步步学完了Docker的基本使用方法。笔者在本文以实例的方式讲述Docker的使用,是希望读者能以边读边实践方式进行学习,没人可以一次记住所有的命令,熟练的前提是反复不断地练习。另外,本文介绍的内容并非Docker的全部,更多内容有待读者自行探索Docker官网文档,未来我也会持续更新。