在前面两个小节中,我们讨论了Docker的基本用法。
由于TensorFlow+Python3+Jupyter这样的开发环境,基本上是很多机器学习(深度学习)爱好者的标配。
所以,在本小节,我们就简单讨论一下如何利用Docker技术,来快速搭建这样的开发环境。
3. 下载合适的Docker镜像
首先,我们要下载合适的Docker镜像。
利用前面所学的命令知识,我们很容易搜索到与TensorFlow相关的Docker镜像:
docker search tensorflow

如果我们之间下拉(pull)星星(starts)排名第一的镜像,它很可能不是我们想要的。这是因为,第一,TensorFlow默认Python 2.7,而不是我们想要的Python 3.x。第二,它没有预装TensorFlow,我们还得手动安装这个软件。
怎样才能解决这个问题呢?
这时就得知道TensorFlow的各种标签(tag)。
根据 TensorFlow官方的信息,其 Docker 映像位于 tensorflow/tensorflow Docker Hub 代码库中。映像版本按照以下格式进行标记:
标记(tag) 说明
latest TensorFlow CPU 二进制映像的最新版本。默认。
nightly TensorFlow 映像的每夜版。(不稳定) version 指定 TensorFlow 二进制映像的版本,例如:1.13.0
devel TensorFlow master 开发环境的每夜版。包含 TensorFlow 源代码。
每个基本标记都有用于添加或更改功能的变体:
标记变体 说明 tag-gpu 支持 GPU 的指定标记版本。 tag-py3 支持 Python 3 的指定标记版本。 tag-jupyter 带有 Jupyter 的指定标记版本(包含 TensorFlow 教程笔记本)
我们可以一次使用多个变体。例如,以下命令会将 TensorFlow 版本映像下载到计算机上:
docker pull tensorflow/tensorflow # latest stable release
docker pull tensorflow/tensorflow:devel-gpu # nightly dev release w/ GPU support
docker pull tensorflow/tensorflow:latest-gpu-jupyter # latest release w/ GPU support and Jupyter
如果我们想下载如标题所示的镜像,就要用到多个tag的叠加(冒号『:』后面就是tag的位置,多个tag用『-』隔开),如下命令所示:
docker pull tensorflow/tensorflow:latest-py3-jupyter
如果我们想下载对应的GPU版本,就把上述命令修改为:
docker pull tensorflow/tensorflow:latest-gpu-py3-jupyter
当然,如果你想下载GPU版本的Docker,首先你的宿主机需要事先安装有合适的GPU,第二,还得安装正确的驱动。可以用如下命令来检查GPU驱动是否安装成功(如图3-2所示):
nvidia-smi

当这个镜像下载完毕之后,我们可以用如下命令查看它(如图3-3所示):
docker image ls

4. 启动Docker
4.1 测试Tensorflow 的执行
首先我们使用如下命令启动Tensorflow(如图3-4所示):
$ docker run -it --rm -p 8888:8888 tensorflow/tensorflow:latest-gpu-py3-jupyter bash

我们来简单解释上面的命令:
- -it:表示创建交互式终端(interactive terminal),它对应后面的bash(bash是一个为GNU计划编写的Unix shell)
--rm
:表示在本次容器运行完毕后,自动删除(remove)该容器。需要注意的是, 该参数前是两个短横杠『--』。如果我们想持久化保存对docker 容器的修改,以便下一次使用的话,这个『--rm』选项,就不要添加。- -p(小写):指定要映射的IP和端口,但是在一个指定端口上只能绑定一个容器。其格式为
hostPort:containerPort
(宿主机端口:容器端口)。这里, 8888:8888的含义就是将本地的 8888 端口映射到容器的 8888 端口。
如果我们想启动CPU版本,只需要更改对应的Docker名称即可:
$ docker run -it --rm -p 8888:8888 tensorflow/tensorflow:latest-py3-jupyter bash
在进入Docker的终端之后,我们在输入如下有关Jupyter的配置命令:
root@5c973987de83:/tf# jupyter notebook --ip 0.0.0.0 --allow-root --port 8888 --no-browser
我们也简单解释上面的命令,其中root是用户名,从图3-4可以看出,Docker是不推荐使用root账号登录的。随后的5c973987de83就是这个容器的ID。
jupyter notebook 是在终端启动Jupyter的命令。
--ip 是指定访问Jupyter服务器的IP地址,默认值是127.0.0.1或localhost, 这样无法远程访问的. 这里我们设置为0.0.0.0,表示它可以接纳任何IP。
--allow-root:用户root用户登录
--port 8888:开启8888端口,这个要和前面启动Docker命令的设置一致即可(不一定非得送8888,保持设置一致就行)
--no-browser:表示不需要启动浏览器。后期我们自己手动启动它。
一旦上述命令执行后,便会启动后台Jupyter服务器进程,如图3-5所示。

我们要记住最后的那个token(类似访问口令):
http://(5c973987de83 or 127.0.0.1):8888/?token=dd24b802377a125bb217394261af061dc96ba52f8755efa4
然后我们在宿主机的浏览器上输入:
http://127.0.0.1:8888/token=dd24b802377a125bb217394261af061dc96ba52f8755efa4
即可进入如图所示的访问界面。
这证明我们可以正常使用Docker容器内部的Jupyter了!

在图3-5中,我们看到,Jupyter服务器进程占据一个终端界面,我们无法关闭它,因为一旦关闭这个终端,那么这个Jupyter服务器进程就会死亡。其中的逻辑是这样的,我们在终端通过命令行开启了Jupyter服务进程,因此,终端是父进程,Jupyter服务器进程是终端进程的子进程,该子进程因父进程而存在,一旦爹没了,孩子也就失去了依靠。这样就好比『皮之不存毛将焉附』。
5.解决方案
有两种方式可以解决上述问题。
(1)保留现有局面,『另辟蹊径』。
这里的『另辟蹊径』是指,保持图3-5的界面不变,在宿主机上重新打开一个终端,然后在新终端上输入『docker ps -a』命令,查看当前运行的所有容器。然后查询到正在运行的目标容器ID,如c1fd7d9c9b5f(这个值因为『--rm』选项的存在,每次运行后,容器的ID都不一样,无需记忆,只需拷贝),如图3-7所示。

然后我们利用如下命令重新进入它的交互式终端,如图3-8所示。
docker exec -it c1fd7d9c9b5f bash #进入容器ID为 c1fd7d9c9b5f的终端shell(

这里简单解释一下docker命令的一个重要参数选项『exec』:
exec: Run a command in a running container(在一个正在运行的容器中执行命令)
它和另外『start』选项的不同在于:
docker start命令,直接将已经终止的容器启动运行起来(Start one or more stopped containers)。
这样一来,Docker容器一方面提供了TensorFlow+Jupyter服务,另一方面,又愉快地提供了我们需要的交互式的终端服务。
(2) 后台运行Jupyter,前台照常使用bash
还有一种方法,就是利用前面我们介绍的如何搭建Jupyter服务器的方法,请参考:
玉来愈宏:如何设置远程访问的Jupyter Notebook服务器-04(服务器篇)zhuanlan.zhihu.com
在容器内部,使用如下命令:
nohup jupyter notebook --ip 0.0.0.0 --allow-root --port 8888 --no-browser &
其中,nohup的含义,nohup就是不挂起的意思( no hang up)。该命令的一般形式为:
nohup command &
使用&命令后,作业被提交到后台运行,当前终端(控制台)没有被占用。
然后,输入回车键,将Jupyter服务器切换到后台。这时,Jupyter服务器和终端是没有关系的,即使终端关闭了,Jupyter服务还会照常提供(如图3-9所示)。

这里需要注意的是,由于在宿主机的浏览器上,还需要用到Jupyter给出的token,才能使用Jupyter,而Jupyter的输出全部定向输出到nohup.out文件中,它是一个文本文件,因此,我们需要用cat命令输出它,这样就可以查看到这个token。
cat nohup.out
在输出的结果中(如图3-9),拷贝token,在宿主机浏览器地址栏中输入:
http://127.0.0.1:8888/?token=a05597ed1cb93aefec1a9d006741191551a5d9650185f90b
于是,你就能看到类似于图3-6的Jupyter界面。聪慧如你,你肯定观察到了,『127.0.0.1』就是本机(local host)的意思。
那么,问题来了,如何把docker当做一个可以远程访问服务器来使用呢?如何避免使用『繁琐的token』来使用Jupyter呢?
这是我们下一节将要讨论的内容。
本文作者:张玉宏。著有《深度学习之美:AI时代的数据处理与最佳实践》(张玉宏著,电子工业出版社,2018年7月出版,已重印6次,繁体版发行至中国台湾)。更多理论推导及实战环节,请参阅该书。