前言
最近在很多场景用到了 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_NAME
docker 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/ ↩