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 参数。

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

可使用 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
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 参数可以覆盖环境变量的设置。

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. 1.万物皆可RSS,一个开源易用的RSS生成器,推荐使用,https://docs.rsshub.app/ ↩