Docker 基本使用技巧

前言

最近在很多场景用到了 docker,太方便了,相见恨晚。之前 tensorflow-serving 时虽然也用到了,但懵懵懂懂地只是照葫芦画瓢改参数,现在自己尝试构建 docker 镜像过后,才真正算是体会到 docker 的便利,在此极力推荐给每位同学,不分前后端,不分工作背景,都推荐学习。这里简单总结了常见命令和技巧,备忘。

Docker 用武之地

对我个人来说,最大的好处就是部署服务时免去了环境配置的繁琐。

当我作为 docker 提供方,交付极快。

比如说,做好了一个项目原型,给到其他同学体验和部署时。一个 docker image 和 参数文档 就够了,而如果把 源码、Dockerfile 也一并给过去,相当于 工作原理、服务架构、部署方法 全都一股脑铺在了面前,没理由还跑不起来。

当我作为 docker 使用方,不仅部署和启动快,调整魔改也方便。

比如说,最近入手了群晖NAS,使用 docker 部署了不下十个五花八门的工具,而这些东西如果都要脱离docker一个一个手动部署,那是不可想象的。Docker 虽然说是跑在 container 盒子里,但是这个盒子是开放的,可以方便地 mount 路径,也可以替换和修改内部文件以符合自己使用要求,甚至可以在启动前修改环境变量,自由度极大。

Docker学习参考资料

RUNOOB-Docker教程

阮一峰-Docker入门教程

Docker Documentation

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
2
3
4
5
6
7
8
9
10
11
// 可以看到自己是否在 docker 组里面
cat /etc/group | grep 'docker'

// 将自己加入 docker 用户组里面
sudo usermod -a -G docker $your_username

// 重启 docker 服务
sudo service docker restart

// 切换用户组(或者重新登录)
newgrp docker

本地镜像常见操作:

1
2
3
4
5
6
7
8
9
10
11
// 列出本地镜像
docker images

// 删掉本地镜像
docker rmi $IMAGE_ID

// 导出镜像为一个 tar 文件
docker save --output backup.tar $IMAGE_ID

// 导入一个镜像
docker load --input backup.tar

本地容器常见操作:

1
2
3
4
5
// 列出正在运行的 docker 容器列表
docker ps

// 结束运行中的 docker
docker kill $CONTAINER

映射端口, 使用 -p 参数:

例如,如下命令 Tensorflow-serving 中,容器映射了 8500-8500, 8501-8501 两对端口,: 前为宿主机端口,: 后为容器内端口。宿主机端口可以自己设定,容器内端口则要按照文档使用,Dockerfile 构建时候的 EXPOSE 已经指定好该端口值了 。

1
2
3
4
5
sudo docker run -p 8500:8500 -p 8501:8501 \
--mount type=bind,source=path_to_serving_folder/twingan_latent_encoder,target=/models/twingan_latent_encoder \
--mount type=bind,source=path_to_serving_folder/latent_search_model,target=/models/latent_search_model \
-t tensorflow/serving \
--model_config_file=/models/serving_models.config

映射文件,挂载路径

使用 -v--mount 参数,详见下面 [Docker 文件相关](#Docker 文件相关)一节的说明。

上面命令中也有使用到 --mount 参数。

启动容器后,执行命令并附带参数

可使用 ENTRYPOINTCMD 两种方式,需要在使用 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 --alldocker 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
2
3
4
5
$ docker run -d \
-it \
--name devtest \
-v "$(pwd)"/target:/app \
nginx:latest

-v 命令以 : 为分隔符,分隔符前面为 Host 路径,分隔符后面为 Container 中的路径。

挂载路径当然会有读写权限问题、谁主谁次的问题,因此也有一套权限标识符,好像比较复杂,我这只试过留白默认的,即 rprivate,实际用下来已经足够实用。

简单来说,挂载路径是后生效的,且静默完成,宿主机的现有文件会覆盖 container 里文件。也就是说,可以轻松替换 docker 中一整个文件夹的内容,这点对于机器学习部署的服务替换新的模型非常方便。

-v 命令也可以重复多个,实现多个路径的挂载。

Docker container 内外拷贝文件

跟 shell 中 cp 命令使用类似,命令名为 docker ps ,container 路径要以 $CONTAINER: 开头

1
2
3
4
5
6
7
// 使用 docker ps 获得 docker container id, 假设为 da0bbe00f49b

//从 docker container 内向宿主机拷贝文件
docker cp da0bbe00f49b:/tmp/test.txt test_renamed.txt

//从宿主机向 docker container 拷贝文件
docker cp test.txt da0bbe00f49b:/tmp/

官方文档: 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
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
FROM node:10-slim

LABEL MAINTAINER https://github.com/DIYgod/RSSHub/


ENV NODE_ENV production
ENV TZ Asia/Shanghai

ARG USE_CHINA_NPM_REGISTRY=0;
ARG PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1;

RUN ln -sf /bin/bash /bin/sh

RUN apt-get update && apt-get install -yq libgconf-2-4 apt-transport-https git dumb-init --no-install-recommends && apt-get clean \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY package.json clean-nm.sh /app/

RUN if [ "$USE_CHINA_NPM_REGISTRY" = 1 ]; then \
echo 'use npm mirror'; npm config set registry https://registry.npm.taobao.org; \
fi;

RUN if [ "$PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" = 0 ]; then \
echo 'pass'
fi;

COPY . /app

EXPOSE 1200
ENTRYPOINT ["dumb-init", "--"]

CMD ["npm", "run", "start"]

FROM 指定了基础镜像,node:10-slim 中, : 后的内容制定了具体 tag, 可以用 :latest 来表明使用最新编译出来的版本。跟 docker pull 都是一样用法,可以从 docker hub 查到对应镜像。

ENV 用来设置 docker 中的环境变量,和 Shell 下的行为是一致,例如 python 中 os.environ 获得的值会与这里一致。 在本例中,使用 ENV TZ Asia/Shanghai 设置了时区,调整显示的时间,尤其是日志时间。 docker run 时候增加 --env 参数可以覆盖环境变量的设置。

ARGENV 一样也是设置 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 用于声明将暴露的端口。

ENTRYPOINTCMD 都是用来指定容器启动程序及参数。前者格式是在 container 启动完毕后,会执行两者,

两者可以独立的使用,在一起使用的时候,ENTRYPOINTCMD 会拼在一起作为一条命令执行。两者都可以在 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. 1.万物皆可RSS,一个开源易用的RSS生成器,推荐使用,https://docs.rsshub.app/