前言
最近在很多场景用到了 docker,太方便了,相见恨晚。之前 tensorflow-serving 时虽然也用到了,但懵懵懂懂地只是照葫芦画瓢改参数,现在自己尝试构建 docker 镜像过后,才真正算是体会到 docker 的便利,在此极力推荐给每位同学,不分前后端,不分工作背景,都推荐学习。这里简单总结了常见命令和技巧,备忘。
Docker 用武之地
对我个人来说,最大的好处就是部署服务时免去了环境配置的繁琐。
当我作为 docker 提供方,交付极快。
比如说,做好了一个项目原型,给到其他同学体验和部署时。一个 docker image 和 参数文档 就够了,而如果把 源码、Dockerfile 也一并给过去,相当于 工作原理、服务架构、部署方法 全都一股脑铺在了面前,没理由还跑不起来。
当我作为 docker 使用方,不仅部署和启动快,调整魔改也方便。
比如说,最近入手了群晖NAS,使用 docker 部署了不下十个五花八门的工具,而这些东西如果都要脱离docker一个一个手动部署,那是不可想象的。Docker 虽然说是跑在 container 盒子里,但是这个盒子是开放的,可以方便地 mount 路径,也可以替换和修改内部文件以符合自己使用要求,甚至可以在启动前修改环境变量,自由度极大。
Docker学习参考资料
Docker 的一些基本概念
为了隔离运行、规避环境配置的麻烦,以前使用 虚拟机,但是 虚拟机 启动慢、资源占用多,Docker 跟 虚拟机 相似,但它是基于 linux 容器的一种实现,隔离程度没有那么强,与宿主机的联系更多。Docker 的使用首先要启动 docker 服务,即dockerd,多个docker是共同托管在这个 dockerd 上,每个docker运行时又各自在其 container 内来实现隔离。
使用docker最好了解镜像、容器的概念,这篇写的不错。
Docker 安装及常见命令
使用 Docker 首先要在本机安装并启动 docker 服务,一般安装 docker-ce 版本(社区版)即可,安装的源需要自己设定,请按照官方文档一步步来。 这里下方的笔记里写得相对简略些,可以参考。
如果是要在 windows 上使用,则需要安装一个 Docker Desktop for Windows 应用,步骤参考官方文档。在 windows 上使用时,要手动启动 Hyper-V 功能,印象里好像还得“以管理员身份运行”,否则 mount 路径时会无效。
Docker 获得镜像并部署运行
通常我们会用到的 Docker 都会在 Docker hub 仓库被托管,这时候使用 docker pull 可以从仓库把对应 docker 下载下来,然后使用 docker run 来启动。
例如,假设我打算部署一个 RSSHub 服务[1],先到 docker hub 搜索,或使用下面命令搜索:
1  | docker search rsshub  | 
会返回多个结果,获得对应 docker 的全名: diygod/rsshub
下载镜像:
1  | docker pull diygod/rsshub  | 
下载完毕后,查看本地查看镜像列表:
1  | docker images  | 
运行镜像(参考对应项目的文档说明):
1  | docker run -p 1200:1200 diygod/rsshub  | 
总体上来说,部署启动一个 docker 服务就是这么简单。
在国内的话,使用 docker hub 源可能速度有些慢,可以通过修改 hub 镜像地址来改善,参考这里。
Docker 基本用法及命令
去掉 sudo 权限的要求
通过将自己加入 docker 用户组,避免每次 docker 命令都需要在前面加 sudo :
1  | // 可以看到自己是否在 docker 组里面  | 
本地镜像常见操作:
1  | // 列出本地镜像  | 
本地容器常见操作:
1  | // 列出正在运行的 docker 容器列表  | 
映射端口, 使用 -p 参数:
例如,如下命令 Tensorflow-serving 中,容器映射了 8500-8500, 8501-8501 两对端口,: 前为宿主机端口,: 后为容器内端口。宿主机端口可以自己设定,容器内端口则要按照文档使用,Dockerfile 构建时候的 EXPOSE 已经指定好该端口值了 。
1  | sudo docker run -p 8500:8500 -p 8501:8501 \  | 
映射文件,挂载路径
使用 -v 或 --mount 参数,详见下面 [Docker 文件相关](#Docker 文件相关)一节的说明。
上面命令中也有使用到 --mount 参数。
启动容器后,执行命令并附带参数
可使用 ENTRYPOINT 或 CMD 两种方式,需要在使用 Dockerfile 构建时配置,详见本文下面 [Docker 镜像生成](#Docker 镜像生成)一节。
上面命令中最后一行,即通过 ENTRYPOINT 的方式向容器传递运行时临时参数。
容器运行时及 attach、detach 操作
运行容器时,希望进入容器内命令行继续操作:
docker run时增加-it参数, 即docker run -it $IMAGE_NAMEdocker start时增加-ai参数。
已经在容器内,想脱离(detach)容器而保持容器运行:
先按Ctrl+P 再按 Ctrl+Q,或者三个键一起按。参考这里
容器已经在运行,想要进入容器的命令行:
docker attach $CONTAINER命令进入如果 docker 启动时已经使用
CMD之类的执行了 daemon 服务,现在想要进入修改下再跑起来,这个时候需要进入 container 的同时覆盖CMD命令(否则这个 docker 只会在 stopped 和 执行 CMD 两个状态里摇摆):1
docker exec -it $CONTAINER bash
Docker container 清理
使用 docker run 启动的服务在执行完毕后,虽然会自动跳出回到宿主环境,但是 container 其实还是存在的(stopped状态),可以通过 docker container ls --all 或 docker ps -a  来确认所有的 container。
使用 docker run $IMAGE_NAME 命令启动 docker 时,每次总会从 image 创建一个新的 container 然后再在其中运行,而每个 container 还要占用另外一份文件空间, 如果操作不当,就会积累大量冗余数据占用硬盘。
选用下面的方法正确处理好 container 空间占用的问题:
- 使用 
docker container rm $CONTAINER_ID命令删掉对应 container。 - 使用 
docker container prune命令快速删掉所有没在运行的 container,还有不少其他方便的 prune 的命令,参考官方文档 Prune unused Docker objects,要谨慎使用。 - 使用 
docker container start $CONTAINER_ID来重新启动结束中的 container,而不是每次新建一个。 - 使用 
docker run --rm $IMAGE_NAME来从镜像启动docker,增加--rm参数后,容器在运行结束后会被自动删掉(Automatically remove the container when it exits),适合于调试阶段或容器中运行时数据无价值的情况。 
Docker 文件相关
Docker 挂载路径 - Bind mounts
可以用 -v 或 --mount 两套方法来实现。前者实现较早,用的时候敲得字少;后者更清晰,似乎功能更强。
两者需要关注的区别:用 -v 的时候会帮忙自动建路径,无论是容器内还是宿主机,而--mount检查路径发现没有时会报错。这里以 -v 为例子。
1  | $ docker run -d \  | 
-v 命令以 : 为分隔符,分隔符前面为 Host 路径,分隔符后面为 Container 中的路径。
挂载路径当然会有读写权限问题、谁主谁次的问题,因此也有一套权限标识符,好像比较复杂,我这只试过留白默认的,即 rprivate,实际用下来已经足够实用。
简单来说,挂载路径是后生效的,且静默完成,宿主机的现有文件会覆盖 container 里文件。也就是说,可以轻松替换 docker 中一整个文件夹的内容,这点对于机器学习部署的服务替换新的模型非常方便。
-v 命令也可以重复多个,实现多个路径的挂载。
Docker container 内外拷贝文件
跟 shell 中 cp 命令使用类似,命令名为 docker ps ,container 路径要以 $CONTAINER: 开头
1  | // 使用 docker ps 获得 docker container id, 假设为 da0bbe00f49b  | 
官方文档: docker cp
Docker 镜像生成
Docker 的镜像需要通过准备一个 Dockerfile 文件来生成。写好 Dockerfile 文件后,在与该文件同级的目录下,使用下面命令生成 docker 镜像:
1  | docker build -t $IMAGE_NAME .  | 
Dockerfile 的写法可以通过这篇文章-Dockerfile 定制镜像来快速了解下。Dockerfile 的指令比较多,这里还是主张”实践出真知”,自己尝试封装一个docker服务,现学现用要高效得多。我们在此通过简单 review 一下 RSSHub 的 Dockerfile 来学习下。
Review RSSHub Dockerfile
进入 Github 项目根目录即可以看到一个 Dockerfile 文件,如下:
1  | FROM node:10-slim  | 
FROM 指定了基础镜像,node:10-slim 中, : 后的内容制定了具体 tag, 可以用 :latest 来表明使用最新编译出来的版本。跟 docker pull 都是一样用法,可以从 docker hub 查到对应镜像。
ENV 用来设置 docker 中的环境变量,和 Shell 下的行为是一致,例如 python 中  os.environ 获得的值会与这里一致。 在本例中,使用 ENV TZ Asia/Shanghai 设置了时区,调整显示的时间,尤其是日志时间。 docker run 时候增加 --env 参数可以覆盖环境变量的设置。
ARG 和 ENV 一样也是设置 docker 中的环境变量,但只在构建阶段生效,所以在本例中,把它用来作为构建阶段的选项,当参数不同时,构建步骤会有些不同,即后面 RUN if 代码块部分。
RUN 命令,就相当于在构建 docker 阶段使用 Shell 脚本执行动作。这里需要注意的是,要尽量把多个 shell 命令压缩在一次 RUN 命令中一起执行,像这个 dockerfile 中一样, apt-get 的 update、install、clean 以及 rm -rf 都使用 && 联在了一起,使用一条 RUN 命令一并执行。这是因为 Dockerfile 中的每一条命令都会产生一次 docker commit, 而每次 commit都会固化一层镜像,如果把命令拆开成多个 RUN 命令的话,最终镜像将会很臃肿,层数多且占存储空间较大。也可以参考下 kaldi 的 docker是怎样竭尽所能把各个命令压缩在一起执行的。
由于每次 docker commit 后,镜像会固化一层,所以删除文件的动作要在 commit 之前执行,否则后面就删不掉了。因此,我们可以从上面的 Dockerfile 里看到,为了删除 apt-get安装时下载的临时文件,使用 rm -rf 的动作是与 apt-get 在同一个 RUN 命令中完成的。
WORKDIR 命令用于指定 docker 的默认目录,也可以在 build 阶段切换当前目录,docker 启动后的目录会自动跳到最后一次 WORKDIR 指定的目录。如果在编译阶段需要切换目录后再执行动作,要么通过 WORKDIR 先切换目录,要么把 cd 命令和要进行的动作放在一起执行,否则在下次 RUN 时,目录会切回到 WORKDIR 的目录。
COPY 用于从构建上下文目录中向镜像内复制文件。
EXPOSE 用于声明将暴露的端口。
ENTRYPOINT 和 CMD 都是用来指定容器启动程序及参数。前者格式是在 container 启动完毕后,会执行两者,
两者可以独立的使用,在一起使用的时候,ENTRYPOINT 和 CMD 会拼在一起作为一条命令执行。两者都可以在 docker run 的时候重新被覆盖掉,前置要在 -t $IMAGE_NAME 前通过 --entrypoint 参数来覆盖,后者是 -t $IMAGE_NAME 后的内容都会被解析为 CMD 。具体区别和用法可以再看看这篇 Docker 的 ENTRYPOINT 和 CMD 参数探秘,总结下最佳实践方案:
如果可提供的参数比较少的话,用 CMD 就好了,docker run 需要修改的时候,一整句替代掉,易于理解。
而如果提供的可修改参数较多的话,可以在 Docker 中用 ENTRYPOINT 启动,在 run docker 时用覆盖 CMD 的方式传递参数(参考 tensorflow/serving 的 Dockerfile  以及上文中启动 Tensorflow-serving 的用法)。
结语
docker 还有非常多的其他用法,这里就不再细讲了,以后碰到再补充。
Docker 毕竟只是个工具,常用常新,有应用场景、实践过才能感受到它的实用、强大,还没试过的话,不妨考虑按这篇《Docker 微服务教程》来搭个网站体验一下。
- 1.万物皆可RSS,一个开源易用的RSS生成器,推荐使用,https://docs.rsshub.app/ ↩