解决环境依赖问题的神器:Docker

在做项目时,最烦人的莫过于环境依赖问题。不同的项目依赖于不同的环境,一旦对环境就行修改升级,就会导致其他项目无法运行,比如一些python版本问题,numpy版本问题,CUDA/cudnn版本问题,protobuf版本问题,pytorch版本问题等等这些环境bug专业户。解决这类问题的一个最好的选择就是使用Docker。


使用Docker的好处在于:1)在Docker容器中运行程序,可以直接使用宿主机硬件资源、cpu、内存等利用率上有很大优势;2)Docker镜像方便传播,便于快速部署到任何linux机器上,无需重复配环境造轮子;3)Docker容器中环境修改,不会影响到其他环境。具体关于docker的介绍可以查看docker官方文档:https://docs.docker-cn.com/。

Docker 安装

docker目前提供了两个版本:社区版(CE)和企业版(EE),可以根据各自需要安装对应版本。我选择的是在Ubuntu16.04上社区版docker.

docker支持一下的ubuntu操作系统

  • Ubuntu Xenial 16.04 (LTS)
  • Ubuntu Trusty 14.04 (LTS)
  • Ubuntu Zesty 17.04
  • Ubuntu Yakkety 16.10

如果需要安装docker,则需要安装以上ubuntu版本之一的64版本。

卸载旧版本

Docker 的早期版本称为 docker 或 docker-engine。如果安装了这些版本,卸载它们:

1
$ sudo apt-get remove docker docker-engine docker.io

如果 apt-get 报告未安装任何这些软件包,这表示情况正常。Docker CE 软件包现在称为 docker-ce。

安装docker

目前,可以通过两种方式安装 Docker CE:镜像仓库安装和deb软件包安装。
1.更新apt软件包索引

1
$ sudo apt-get update

2.安装软件包,以允许 apt 通过 HTTPS 使用镜像仓库:

1
$ sudo apt-get install apt-transport-https ca-certificates curl software-properties-common

3.添加 Docker 的官方 GPG 密钥并验证指纹:

1
2
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add
$ sudo apt-key fingerprint 0EBFCD88

4.使用下列命令设置stable镜像仓库:

1
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

安装docker ce

1.更新 apt 软件包索引。

1
$ sudo apt-get update

2.安装最新版本的 Docker CE,或者转至下一步以安装特定版本。将替换任何现有的 Docker 安装版本。

1
$ sudo apt-get install docker-ce

注意:在生产系统上,应该安装特定版本的 Docker CE,而不是始终使用最新版本。此输出将被截断。列出可用版本:

1
$ apt-cache madison docker-ce


选择要安装的特定版本,第二列是版本字符串,第三列是存储库名称,它指示包来自哪个存储库,以及扩展它的稳定性级别。要安装一个特定的版本,将版本字符串附加到包名中,并通过等号(=)分隔它们:

1
$ sudo apt-get install docker-ce=<VERSION>

3.验证是否正确安装了 Docker CE,方法是运行 hello-world 镜像。

1
$ sudo docker run hello-world

此命令将下载一个测试镜像并在容器中运行它。容器运行时,它将输出一条参考消息并退出。

问题:Got permission denied问题解决方案

1
2
3
$ sudo groupadd docker #添加docker用户组
$ sudo usermod -aG docker $USER
$ docker ps

退出当前终端并重新登录,进行如下测试。

在线查找镜像并拉取镜像

在Docker hub上有大量的高质量的镜像可以使用。可以在线查找镜像。比如在线查找ubuntu,使用如下命令:

1
2
$ docker search Ubuntu 
$ docker pull Ubuntu:16.04

安装NVIDIA-docker

因为GPU属于特定的厂商产品,需要特定的driver,Docker本身并不支持GPU,以前如果需要在Docker中使用GPU,就需要在container中安装主机上使用GPU的driver,然后把主机上的GPU设备(例如:/dev/nvdia0)映射到container中。所以这样的docker image 并不具备可移植性。
Nvidia-docker项目就是为了解决这个问题,它让Docker image不需要知道底层GPU的相关信息,而是通过启动container设备和驱动文件来实现。
1.nvidia-docker 安装
如果之前安装过docker1.0版本,需要先删掉该版本和之前创建的容器。

1
2
$ docker volume ls -q -f driver=nvidia-docker | xargs -r -I{} -n1 $ docker ps -q -a -f volume={} | xargs -r docker rm –f
$ sudo apt-get purge -y nvidia-docker

添加代码仓库

1
2
3
4
5
6
$ curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | \
$ sudo apt-key add -
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
$ curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | \
$ sudo tee /etc/apt/sources.list.d/nvidia-docker.list
$ sudo apt-get update

安装docker2

1
2
$ sudo apt-get install -y nvidia-docker2
$ sudo pkill -SIGHUP dockerd

nvidia-docker 测试

1
$ docker run --runtime=nvidia --rm nvidia/cuda:9.0-base nvidia-smi

查看镜像信息

1
$  nvidia-docker images

可以看见在安装nvidia-docker时,生成了一个nvidia/cuda的镜像。

基本概念

Docker包括三个基本概念:1)镜像(image);2)容器(container);3)仓库(repository)。
1.镜像(image)
docker镜像相当于一个root文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含一些运行时准备的一些配置参数。比如上面pull下来的Ubuntu:16.04镜像就包含了完整的一套ubuntu16.04最小系统的root文件系统。
2.容器(container)
镜像(image)和容器(container)的关系,就像是面向对象程序设计中的实例的关系一样。镜像是静态的定义,容器是镜像运行的实体。容器可以被创建、启动、停止、删除等操作。
3.仓库(repository)
镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry就是这样的服务。这个和GitHub代码托管类似。一个docker仓库可以包含多个仓库;每个仓库可以包含多个标签(tag),每个标签对应一个镜像。

docker操作

获取镜像/列出镜像/删除本地镜像

1.获取镜像
在Docker Hub上存在大量公开的高质量的镜像可以用,这里说一下如何获取这些镜像。从DOcker镜像仓库中获取镜像命令格式:

1
docker pull [选项] [Docker Registry 地址[:端口号]/]仓库[:标签]

  • Docker镜像仓库地址:地址格式一般为<域名/IP>[:端口号]。默认地址是Docker Hub.
  • 仓库名: 仓库名为格式为<用户名/软件名>.对于Docker Hub,如果不指定用户名,则默认为library,也就是官方镜像。

比如:

1
2
方式一:docker pull ubuntu:16.04  #从默认library中获取镜像
方式二:docker pull coreapps/ubuntu16.04 #从某用户

2.列出镜像/删除镜像
要想列出本地已经下载下来的镜像,可以使用docker images命令。

1
2
3
4
5
6
7
8
9
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
maskrcnn-benchmark pytorch-nightly-1.1.0 e08131e576dd 37 hours ago 8.49GB
nicehuster/pytorch-nightly-1.1.0 maskrcnn_benchmark_FCOS e08131e576dd 37 hours ago 8.49GB
nvidia/cuda 9.0-cudnn7-devel-ubuntu16.04 365b60829b2a 10 days ago 2.72GB
ubuntu 16.04 9361ce633ff1 5 weeks ago 118MB
nvidia/cuda 9.0-base 411830f910a9 6 weeks ago 135MB
hello-world latest fce289e99eb9 3 months ago 1.84kB
node 9.4.0-slim 45ddb9d51c7a 15 months ago 231MB

删除镜像使用docker rmi [镜像ID]命令

1
2
3
4
$ docker rmi 45ddb9d51c7a
Untagged: node:9.4.0-slim
Untagged: node@sha256:41812a18ec7c647e85a3d8faba9ce1a213f35945321a5f6b4aef0283ec6ed740
Deleted: sha256:45ddb9d51c7aeb943f90cb62e5d523da2791586caa567e5f26204f640decb376

如果无法删除可以添加参数 -f强制删除,即docker rmi -f 45ddb9d51c7a

docker启动/运行/终止/镜像修改保存

1.激活一个已有的镜像,为它建立一个容器(container):

1
$ nvidia-docker run -it -d --name='nice' -v /mnt:/mnt 365b  /bin/bash

这里需要使用GPU的话,使用的nvidia-docker命令,否则可以使用docker,推荐都使用前者激活,-it表示让Docker分配一个伪终端并绑定到container的标准输入上,且让容器的标准输入保持打开 -d表示激活的容器会在后台运行,推荐使用-d参数选项,方面后面镜像的修改和保存。—name=’nice’表示给激活的container命名为nice,方便运行,也可以不指定。-v表示将宿主机的指定目录/mnt挂载到该容器container上。365b表示nvidia/cuda:9.0-cudnn7-devel-ubuntu16.04镜像ID的前4位。/bin/bash表示使用交互式终端。
2.使用docker ps查看刚刚建立的container的id,执行该container:

1
2
3
4
5
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cb6359a94673 365b "/bin/bash" 5 seconds ago Up 3 seconds nice

$docker exec -it nice /bin/bash #运行container。也可以使用命令 docker exec -it cb6359a94673 /bin/bash

现在就可以在container中为所欲为了,随便安装你想要的。进入container之后,就是root用户权限,因此安装依赖包的时候就不需要使用sudo安装。注意:在安装之前先执行apt update命令,然后在使用命令apt install [包]进行安装。
3.退出docker,直接使用exit命令就可以退出。如果对container安装了许多依赖包,如果想保存修改:

1
docker commit nice ubuntu16.04-cuda9.0-cudnn7-devel:latest

保存之后使用docker images命令可以看到自己刚刚commit的镜像文件,即第一个文件。

1
2
3
4
5
6
7
8
9
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu16.04-cuda9.0-cudnn7-devel latest b85c54bed0fe About a minute ago 2.79GB
maskrcnn-benchmark pytorch-nightly-1.1.0 e08131e576dd 38 hours ago 8.49GB
nicehuster/pytorch-nightly-1.1.0 maskrcnn_benchmark_FCOS e08131e576dd 38 hours ago 8.49GB
nvidia/cuda 9.0-cudnn7-devel-ubuntu16.04 365b60829b2a 10 days ago 2.72GB
ubuntu 16.04 9361ce633ff1 5 weeks ago 118MB
nvidia/cuda 9.0-base 411830f910a9 6 weeks ago 135MB
hello-world latest fce289e99eb9 3 months ago 1.84kB

之后就可以使用自己修改的镜像文件,按前面的1.2.3步骤使用就行。

4.没用的container可以停掉,停止命令docker container xxx(container id)

1
2
3
4
5
6
7
niceliu@ise:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
27eb0294db88 e081 "/bin/bash" About a minute ago Up About a minute thirsty_sinoussi
cb6359a94673 365b "/bin/bash" About an hour ago Up About an hour nice
b1728a855f42 2d97 "/bin/bash" 39 hours ago Up 39 hours flamboyant_wing
niceliu@ise:~$ docker container stop b1728a855f42
b1728a855f42

docker上传镜像至Docker Hub

为了避免重复造轮子,一般都会将自己的镜像文件上传到DockerHub。这样的话,可以方面在不同的机器上部署相同的环境。比如我在学校创建的maskrcnn_benchamrk环境,方面以后入职在公司使用,就可以之间在自己的Docker Hub账户上下载就行,避免重新配置环境。
1.申请docker账户并创建reposity。

如上,我创建了一个nicehuster/hello-world的repository。
2.为镜像打标签并push上传镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
niceliu@ise:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu16.04-cuda9.0-cudnn7-devel latest b85c54bed0fe About a minute ago 2.79GB
maskrcnn-benchmark pytorch-nightly-1.1.0 e08131e576dd 38 hours ago 8.49GB
nicehuster/pytorch-nightly-1.1.0 maskrcnn_benchmark_FCOS e08131e576dd 38 hours ago 8.49GB
nvidia/cuda 9.0-cudnn7-devel-ubuntu16.04 365b60829b2a 10 days ago 2.72GB
ubuntu 16.04 9361ce633ff1 5 weeks ago 118MB
nvidia/cuda 9.0-base 411830f910a9 6 weeks ago 135MB
hello-world latest fce289e99eb9 3 months ago 1.84kB
niceliu@ise:~$ docker tag hello-world:latest nicehuster/hello-world:test
niceliu@ise:~$ docker login
Authenticating with existing credentials...
WARNING! Your password will be stored unencrypted in /home/niceliu/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
niceliu@ise:~$ docker push nicehuster/hello-world:test
The push refers to repository [docker.io/nicehuster/hello-world]
af0b15c8625b: Mounted from library/hello-world
test: digest: sha256:92c7f9c92844bbbb5d0a101b22f7c2a7949e40f8ea90c8b3bc396879d95e899a size: 524
niceliu@ise:~$

上传步骤:1)列出镜像文件选择需要push上传镜像;2)给需要上传的镜像打标签,上面选择的是hello-world:latest镜像,打上了nicehuster/hello-world:test的标签。nicehuster/hello-world即在Docker Hub上创建的repository;3)docker login登录账户;4)上传nicehuster/hello-world:test镜像。

使用Dockerfile定制镜像

定制特定环境的镜像,可以使用Dockerfile脚本。Dockerfile是一个文本文件,其中包含一条条指令(instruction),每一条指令构建一层,每一条指令的内容描述了该层如何构建(需要提及的一点,docker镜像都是采用的是分层存储方式)。
1.创建一个Dockerfile文件,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
ARG CUDA="9.0"   #设置cuda版本为9.0
ARG CUDNN="7" #设置cudnn版本为7

FROM nvidia/cuda:${CUDA}-cudnn${CUDNN}-devel-ubuntu16.04
#使用nvidia/cuda仓库中的cuda9.0-cudnn7-devel-ubuntu16.04作为基础镜像,这个基础镜已经安装好cuda和cudnn。你也可以自定义基础镜像


RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections

# 安装基础依赖库
RUN apt-get update -y \
&& apt-get install -y apt-utils git curl ca-certificates bzip2 cmake tree htop bmon iotop g++ \
&& apt-get install -y libglib2.0-0 libsm6 libxext6 libxrender-dev

# 安装 Miniconda
RUN curl -so /miniconda.sh https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh \
&& chmod +x /miniconda.sh \
&& /miniconda.sh -b -p /miniconda \
&& rm /miniconda.sh

ENV PATH=/miniconda/bin:$PATH

# 创建一个 Python 3.6 环境
RUN /miniconda/bin/conda install -y conda-build \
&& /miniconda/bin/conda create -y --name py36 python=3.6.7 \
&& /miniconda/bin/conda clean -ya

ENV CONDA_DEFAULT_ENV=py36
ENV CONDA_PREFIX=/miniconda/envs/$CONDA_DEFAULT_ENV
ENV PATH=$CONDA_PREFIX/bin:$PATH
ENV CONDA_AUTO_UPDATE_CONDA=false

RUN conda install -y ipython
RUN pip install ninja yacs cython matplotlib opencv-python tqdm

2.编译生成镜像:

1
$ nvidia-docker build -t fcos:latest .    #指定生成的镜像名为fcos:latest

使用docker images 就可以看到刚生成的镜像文件。Dockerfile指令说明:

  • FROM : 指定基础镜像。一般以ubuntu,centos等系统作为基础镜像。比如指定ubuntu14.04命令为:FROM ubuntu:14.04
  • RUN : 执行命令行命令。用来安装依赖库。
  • ENV: 指定容器中安装程序的环境变量。

3.在编写Dockerfile文件时需要主要如下几点:

  • 为了保持 Dockerfile 文件的可读性,可理解性,以及可维护性,建议将长的或复杂的 RUN指令用反斜杠 \ 分割成多行,避免使用多个RUN指令
  • 不要使用 RUN apt-get upgrade 或 dist-upgrade,因为许多基础镜像中的「必须」包不会在一个非特权容器中升级。
  • 永远将 RUN apt-get update 和 apt-get install 组合成一条 RUN 声明。

最后,强烈推荐一本书,《docker-从入门到实践》,这本书已经免费开源了pdf而且可以在gitbook上免费阅读。地址在这里:https://yeasy.gitbooks.io/docker_practice/content/