Post

docker 简单教程

在制作生信工具算法会存在各种依赖包(packages),如何让其他人可以将方法跑起来,除了算法代码,还有整个算法依赖的环境都要一起打包,Docker可以很方便实现这一目的。

docker 简单教程

Docker用来解决什么问题?在概念上与虚拟机非常类似,它不会去模拟底层的硬件,只会让每一个应用在完全隔离的运行环境;用户可以在环境中配置不同的工具软件,并且不同环境相互不影响,这个“环境”在Docker中也被称作 container容器。

Docker中的三个重要概念 Dockerfile,Image 和 Container:

  • Image镜像:可以把它理解成一个虚拟机的快照(Snapshot),里面包含了你要部署的应用程序以及它所关联的所有库。
  • Container: 通过镜像,我们可以创建许多不同的Container容器,这里的容器就像一台台运行起来的虚拟机,里面运行了不同的应用程序,每个容器时独立运行的,它们相互之间不影响。最后
  • Dockerfile: 像是一个自动化脚本,它主要用来创建Image镜像,这个过程就好比是我们在虚拟机中安装操作系统和软件一样,只不过是通过Dockerfile这个自动化脚本完成了。

这里有个很好的视频参考:https://www.bilibili.com/video/BV17M4y1t7yc/?spm_id_from=333.337.search-card.all.click&vd_source=02bdc60b98e04ca7e774e476d94f04ae

Docker基础用法参考教程:https://yeasy.gitbook.io/docker_practice

镜像获取及使用

获取镜像

pyMC docker配置

这个在docker hub上已经有成熟的镜像,只需pull一下就好。

1
docker pull pymc/pymc

列出镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
docker image ls

$ docker image ls
REPOSITORY                TAG       IMAGE ID       CREATED         SIZE
ln/rstudio                latest    0af0df7663b7   7 weeks ago     1.82GB
condaforge/miniforge3     latest    3e022d1b3b94   2 months ago    440MB
portainer/agent           2.27.4    a599effa096f   2 months ago    171MB
ubuntu                    latest    602eb6fb314b   2 months ago    78.1MB
ubuntu                    noble     602eb6fb314b   2 months ago    78.1MB
portainer/agent           2.21.5    b997d2809266   6 months ago    216MB
visze/cadd-scripts        1.7       212f064db00d   6 months ago    28.4GB
visze/cadd-scripts-v1_7   0.1.1     8991219e945a   7 months ago    17.8GB
busybox                   latest    ff7a7936e930   8 months ago    4.28MB
cnk3x/xunlei              latest    0451df588f92   9 months ago    106MB
mystaar                   0.4       3015bb316333   11 months ago   7.4GB
mystaar                   0.2       4e6f9a750e23   11 months ago   7.4GB
liangjunhao/staar         0.1       84bb016c0c94   11 months ago   7.4GB
staar                     latest    84bb016c0c94   11 months ago   7.4GB
rserve                    latest    d675d1c5401f   15 months ago   2.1GB
hello-world               latest    d2c94e258dcb   2 years ago     13.3kB
              latest    36dc04da5bf8   2 years ago     3.52GB

删除镜像

1
docker image rm [选项] <镜像1> [<镜像2> ...]

镜像如何使用?

有了镜像后,我们就能够以这个镜像为基础启动并运行一个容器,以上面的pymc/pymc为例,如果我们打算启动里面的 bash 并且进行交互操作的话,可以执行下面的命令:

1
2
3
4
5
6
7
8
9
docker run -it --rm pymc/pymc bash

(pymc-dev) jovyan@3ecadc489426:~/work$ ipython
Python 3.10.6 | packaged by conda-forge | (main, Aug 22 2022, 20:36:39) [GCC 10.4.0]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.6.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]:

docker run运行容器的命令,具体格式会在容器一节进行详细讲解,这里简要说明一下上面用到的参数:

  • -it: 这是两个参数,一个是 -i: 交互式操作,一个是 -t终端。我们这里打算进入bash执行一些命令并查看返回结果,因此我们需要交互式终端。
  • –rm: 这个参数是说容器推出后随之将其删除。默认情况下,为了排障需求,推出的容器并不会立即删除,除非手动 docker rm。我们这里只是随便执行个命令,看看结果,不需要排除和保留结果,使用–rm可以避免浪费空间。
  • pymc/pymc: 这是指用pymc/pymc镜像为基础来启动容器。
  • bash: 放在镜像名后的是 命令,这里我们希望有个交互式Shell,因此用的是 bash

进入容器后,我们可以在Shell下操作,执行任何所需的命令。这里,我们执行了 ipython,调用python交互界面。

最后我们通过 exit 退出这个容器。

利用 commit 理解镜像构成

注意:docker commit 命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是不要使用 docker commit定制镜像,定制镜像应该使用Dockerfile来完成。如果你想要定制镜像请查看下一小节。

镜像是容器的基础,每次执行docker run的时候都会指定哪个镜像作为容器运行的基础。在之前的例子中,我们所使用的都是来自于Docker hub的镜像。直接使用这些镜像可以满足一定的需求,而当这些镜像无法直接满足需求时,我们就需要定制这些镜像。接下来的几节就将讲解如何定制镜像。

1
docker run --name webserver -d -p 80:80 nginx

这条命令会用nginx镜像启动一个容器,命名为webserver,并且映射了80端口,这样我们可以用浏览器去访问这个nginx服务器。

如果是在本机运行的Docker,那么可以直接访问:http://localhost,如果是在虚拟机、云服务器上安装的Docker,则需要将localhost换为虚拟机地址或者实际云服务器地址。

直接用浏览器访问的话,我们看到默认的Nginx欢迎页面。

image-20250624154737803

现在假设我们非常不喜欢这个欢迎页面,我们希望改成欢迎Docker的文字,我们可以使用docker exec 命令进入容器,修改器内容。

1
2
3
4
$ docker exec -it webserver bash
root@3729b97e8226:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
root@3729b97e8226:/# exit
exit

我们以交互式终端方式进入webserver容器,并执行了bash命令,也就是获得一个可操作的Shell。

然后,我们用 <h1>Hello, Docker!</h1> 覆盖了 /usr/share/nginx/html/index.html 的内容。现在我们再次刷新浏览器,就会发现内容被改变了。

image-20250624154748811

我们修改了容器的文件,也就是改动了容器的存储层。我们可以通过 docker diff命令查看具体改动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ docker diff webserver
C /root
A /root/.bash_history
C /run
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
C /usr/share/nginx/html/index.html
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp

现在我们定制好了变化,我们希望将其保存下来形成镜像。

要知道,当运行一个容器的时候(如果不适用卷的话),我们做到任何文件修改都会被记录于容器存储层里。而Docker提供了一个docker commit命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。

docker commit的语法格式为:

1
docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]

我们可以用一下命令将容器保存为镜像:

1
2
3
4
5
6
$ docker commit \
    --author "Tao Wang <twang2218@gmail.com>" \
    --message "修改了默认网页" \
    webserver \
    nginx:v2
sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa1795214

其中--author是指定的作者信息,而--message则是记录本次修改的内容。这点和git版本控制相似,不过这里这些信息可以省略。

我们可以再 docker image ls中看到这个新定制的镜像:

1
2
3
4
5
$ docker image ls nginx
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               v2                  07e334659748        9 seconds ago       181.5 MB
nginx               1.11                05a60462f8ba        12 days ago         181.5 MB
nginx               latest              e43d811ce2f4        4 weeks ago         181.5 MB

我们还可以用docker history具体查看镜像内的历史记录,比如比较nginx:latest的历史记录,我们会发现新增了我们刚刚提交的这一层:

1
2
3
4
5
6
7
8
9
10
11
$ docker history nginx:v2
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
07e334659748        54 seconds ago      nginx -g daemon off;                            95 B                修改了默认网页
e43d811ce2f4        4 weeks ago         /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon    0 B
<missing>           4 weeks ago         /bin/sh -c #(nop)  EXPOSE 443/tcp 80/tcp        0 B
<missing>           4 weeks ago         /bin/sh -c ln -sf /dev/stdout /var/log/nginx/   22 B
<missing>           4 weeks ago         /bin/sh -c apt-key adv --keyserver hkp://pgp.   58.46 MB
<missing>           4 weeks ago         /bin/sh -c #(nop)  ENV NGINX_VERSION=1.11.5-1   0 B
<missing>           4 weeks ago         /bin/sh -c #(nop)  MAINTAINER NGINX Docker Ma   0 B
<missing>           4 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0 B
<missing>           4 weeks ago         /bin/sh -c #(nop) ADD file:23aa4f893e3288698c   123 MB

新的镜像定制好后,我们可以运行这个镜像。

1
docker run --name web2 -d -p 81:80 nginx:v2

这里我们命名为新的服务为web2,并且映射到81端口。访问http://localhost:81看到结果,其内容和之前修改后的webserver一样。

至此,我们第一次完成了定制镜像,使用的是docker commit命令,手动操作给旧的镜像添加了新的一层,形成新的镜像,对镜像多层存储应该有了更直观的感觉。

慎用 commit

使用 docker commit命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。

首先,如果观察之前的 docker diff webserver的结果,你会发现除了真正想要修改的/usr/share/nginx/html/index.html 文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装包、编译构建,那会有大量的无关内容被添加进来,将会导致镜像极为臃肿。

此外,使用docker commit意味着所有对镜像的操作都是黑箱操作,生存的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像, 别人根本无从得知。而且,即使是这个制作镜像的人,过一段事件后也无法记清具体的操作。这种黑箱镜像的维护工作是非常痛苦的。

而且,回顾之前之前提及的镜像所使用的分层存储的概念,除当前层外,之前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是再当前层进行标记、添加、修改,而不会改动上一层。如果使用 docker commit制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到。

使用Dockerfile定制镜像

从刚才的 docker commit的学习中,我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前体积的无法重复的问题、镜像构建透明性的问题、体积的问题都会解决。这个脚本就是Dockerfile。

Dockerfile是一个文本文件,期内包含了一条条的指令(Instruction),每一条指定构建一层,因此每一条指令的内容就是描述该层应当如何构建。

还以之前定制的nginx镜像为例,这次我们使用Dockerfile来定制。

再一个空白目录中,建立一个文本文件,并命名为Dockerfile:

1
2
3
$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile

其内容为:

1
2
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

这个Dockerfile很简单,一共就两行。涉及到两条指令,FROMRUN

FROM指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个nginx镜像的内容,再进行修改一样,基础镜像必须指定的。而FROM就是指定基础镜像,一次你一个DockerfileFROM是必备的指令,并且必须是第一条指令。

Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginxredismongomysqlhttpdphptomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 nodeopenjdkpythonrubygolang 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。

如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntudebiancentosfedoraalpine 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。

除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

1
2
FROM scratch
...

如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

RUN 执行命令

RUN指令是用来执行命令的。由于命令行的强大能力,RUN指令是在指定镜像时最常见的指令之一。其格式有两种:

  • Shell格式:RUN <命令>,就像在命令行中输入的命令一样。刚才写的Dockerfile中的RUN指令就是这种格式。

    1
    
    RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
    
  • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。

既然 RUN 就像Shell脚本一样可以执行命令,那么我们是否就可以像Shell脚本一样把每个命令对应一个RUN呢?比如这样

1
2
3
4
5
6
7
8
9
FROM debian:stretch

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install

之前说过,Dockerfile中每一个指令都会建立一层,RUN也不例外。每一个RUN的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit这一层的修改,构成新的镜像。

而上面的这种写法,创建了7层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了径向力,比如变异环境、更新的软件包等等。结果就是非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。这是很多初学Docker的人常犯的一个错误。

Union FS室友最大层限制的,比如AUFS,曾经时最大不得超过42层,现在时不得超过127层。

上面的 Dockerfile争取额写法应该是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM debian:stretch

RUN set -x; buildDeps='gcc libc6-dev make wget' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

首先,之前所有的命令只有一个目的,就是变异、安装redis可执行文件。因此没必要建立很多层,这只是一层的事情。因此这里没有使用多个RUN—对应不同的命令,二十仅仅使用了一个 RUN指令,并使用&&将各个所需命令串联起来。将之前的7层简化为了一层。在撰写Dockerfile的时候,要经常提醒自己,这并不是写Shell脚本,二十定义每一层该如何构建。

并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以及行首 # 进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。

此外,还可以看到这一组命令的最后添加了清理工作的命令,删除为了变异构建所需要的软件,清理所有下载、展开的文件,并且还清理了apt缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

很多人初学Docker制作出了很臃肿的镜像原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

构建镜像

好了,让我们再回到之前定制的nginx镜像的Dockerfile来。现在我们明白了这个Dockerfile 的内容,那么让我们来构建这个镜像吧。

Dockerfile文件所在目录执行:

1
2
3
4
5
6
7
8
9
$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM nginx
 ---> e43d811ce2f4
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in 9cdc27646c7b
 ---> 44aa4490ce2c
Removing intermediate container 9cdc27646c7b
Successfully built 44aa4490ce2c

从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在Step 2中,如同我们之前所说的那样,RUN 指令启动了一个容器 9cdc27646c7b,执行了所要求的命令,并最后提交了这一层 44aa4490ce2c,随后删除了所用到的这个容器 9cdc27646c7b

这里我们使用了 docker build 命令进行镜像构建。其格式为:

1
docker build [选项] <上下文路径/URL/->

在这里我们指定了最终镜像的名称 -t nginx:v3,构建成功后,我们可以像之前运行 nginx:v2 那样来运行这个镜像,其结果会和 nginx:v2 一样。

镜像构建上下文(Context)

如果注意到,会发现docker build命令最后有一个 ..表示当前目录,而 Dockerfile就在当前目录,因此不少初学者以为这个路径是在指定Dockerfile所在路径,这么理解其实不准确的。如果对应伤命的格式命令,你可能会发现,这是在指定上下文路径。那么什么是上下文呢?

首先我们要理解 docker build 的工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。

当我们进行镜像构建的时候,并非所有定制都会通过 RUN指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指定、ADD指令等。而 Docker build命令构建镜像,其实并非在本地构建,二十在服务端,也就是Docker 引擎中构建的。那么在这种客服端/服务端的架构中,如何才能让服务端获得本地文件呢

这就引入了上下文的概念。当构建的时候,用户会指定构建镜像的上下文路径,docker build命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样Docker 迎请受到这个上下文包后,展开就会获得构建镜像所需的一切文件。

如果在 Dockerfile中这么些:

1
COPY ./package.json /app/

这并不是要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json

因此,COPY 这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 COPY ../package.json /app 或者 COPY /opt/xxxx /app 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。

现在就可以理解刚才的命令 docker build -t nginx:v3 . 中的这个 .,实际上是在指定上下文的目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。

如果观察 docker build 输出,我们其实已经看到了这个发送上下文的过程:

1
2
3
$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
...

其它 docker build用法

  • 直接用 Git repo 进行构建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    # $env:DOCKER_BUILDKIT=0
    # export DOCKER_BUILDKIT=0
      
    $ docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world
      
    Step 1/3 : FROM scratch
     --->
    Step 2/3 : COPY hello /
     ---> ac779757d46e
    Step 3/3 : CMD ["/hello"]
     ---> Running in d2a513a760ed
    Removing intermediate container d2a513a760ed
     ---> 038ad4142d2b
    Successfully built 038ad4142d2b
    

    这行命令指定了构建所需的Git repo,并且指定分支为 master,构建目录为 /amd64/hellow-world,然后Docker就会自己去 git clone这个项目、切换到指定分支、并进入到指定目录后开始构建。

  • 用给定的 tar 压缩包构建

    1
    
    $ docker build http://server/context.tar.gz
    

    如果所给出的 URL 不是个 Git repo,而是个 tar 压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。

Dockerfile指令详解

https://yeasy.gitbook.io/docker_practice/image/dockerfile

还有Dockerfile多阶段构建

操作容器

启动

启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(exited)的容器重新启动。

因为 Docker 的容器实在太轻量化了,很多使用用户都是随时删除和新创建容器。

新建并启动

所需要的命令主要为 docker run

  • 直接在镜像下运行命令并输出

    1
    2
    
    $ docker run -it --rm pymc/pymc echo "hello world"
    hello world
    
  • 启动一个bash终端,允许用户进行交互

    1
    
    docker run -it --rm pymc/pymc bash
    

当利用docker run来创建容器时,Docker在后台运行的标准操作包括:

  • 检查本地是否存在指定的镜像,不沉溺在就从 regestry下载
  • 利用镜像创建并启动一个容器
  • 分配一个系统稳健,并在只读的镜像层外面挂在一层可读写层
  • 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
  • 从地址池配置一个ip地址给容器
  • 执行用户指定的应用程序
  • 执行完毕后容器被终止

启动已终止容器

  • 可以使用docker container ls -a 查看所有的容器,包括停止运行的容器。ls默认只列出正在运行的容器。
  • 可以利用docker container start命令,直接将一个已经终止的容器启动运行。

这里再多介绍几个命令:

  • 利用 docker container kill [ID] 停止指定容器。
  • 利用 docker container rm [ID] 删除指定容器。

守护态运行

更多时候,需要让Docker再后台运行而不是直接把执行命令的结果输出到当前宿主机下。此时,可以通过添加 -d 参数来实现。

下面举两个例子来说明一下:

  • 如果不使用 -d 参数运行容器

    1
    2
    3
    4
    5
    6
    
    docker run pymc/pymc sh -c "while true; do echo hello world; sleep 1; done"
      
    hello world
    hello world
    hello world
    hello world
    

    容器会把结果打印到宿主机上面。

  • 如果使用了 -d 参数运行容器。

    1
    
    docker run -d pymc/pymc sh -c "while true; do echo hello world; sleep 1; done"
    

    此时容器会在后台运行并不会把输出结果打印到宿主机上面(输出结果可以用 docker logs查看)

    使用 -d 参数启动后会返回一个唯一的 id,也可以通过 docker container ls 命令查看容器信息

    1
    2
    3
    
    $ docker container ls
    CONTAINER ID   IMAGE                           COMMAND                  CREATED              STATUS                          PORTS                                       NAMES
    481f7dc4e8e4   pymc/pymc                       "tini -g -- sh -c 'w…"   About a minute ago   Up About a minute (unhealthy)   8888/tcp                                    jovial_moser
    

    获取容器的输出信息,可以通过 docker container logs命令。

    1
    2
    3
    4
    5
    6
    7
    8
    
    $ docker container logs 481f7dc4e8e43e49bf68dad36abcae62ee0ac718ceb636a3e040b7df21f900df
    hello world
    hello world
    hello world
    hello world
    hello world
    hello world
    hello world
    

终止

可以使用 docker container stop来终止一个运行中的容器。

此外,当 Docker容器中指定的应用终结时,容器也自动终止。

例如对于上一章中只启动一个终端的容器,用户通过 exit 命令或者 Ctrl+d来推出终端时,所创建的容器立刻终止。

终止状态的容器可以用 docker container ls -a 命令查看。

处于终止状态的容器,可以通过 docker container start命令来重新启动。

此外,docker container restart 命令会将一个运行态的容器终止,然后再重新启动它。

进入容器

再使用 -d 参数时,容器启动后会进入后台。

某些时候需要进入容器进行操作,包括使用 docker attach命令活docker exec命令,推荐大家使用 docker exec命令,原因会在下面说明。

  • attach 命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    $ docker run -dit pymc/pymc
    6cdac037bf5b34d75a462e0b25d5727bb2034292c3857fe5b25aacc98d9fc2cd
    [tb@pan03 Wed Jun 18 11:52:12 /home/tb]
    $ docker container ls
    CONTAINER ID   IMAGE                           COMMAND                  CREATED          STATUS                            PORTS                                       NAMES
    6cdac037bf5b   pymc/pymc                       "tini -g -- conda ru…"   10 seconds ago   Up 7 seconds (health: starting)   8888/tcp                                    stoic_cannon
    [tb@pan03 Wed Jun 18 11:52:19 /home/tb]
    $ docker attach 6cdac037bf5b
      
    

    注意: 如果从这个 stdin 中 exit,会导致容器的停止。

  • exec命令

    docker exec后面可以跟多个参数,这里主要说明 -t -i 参数。

    只用 -i 参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回。

    -i -t 参数一起使用时,则可以看到我们熟悉的 Linux 命令提示符。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    $ docker run -dit pymc/pymc
    35dd64b249a93f22d0b72ed9c27dd46c0df486c0927bf090f663039ef49b9d69
    [tb@pan03 Wed Jun 18 11:57:45 /home/tb]
      
    $ docker container ls -a
    CONTAINER ID   IMAGE                           COMMAND                  CREATED          STATUS                     PORTS                                       NAMES
    35dd64b249a9   pymc/pymc                       "tini -g -- conda ru…"   22 seconds ago   Up 20 seconds (healthy)    8888/tcp                                    elegant_galois
    [tb@pan03 Wed Jun 18 11:58:06 /home/tb]
      
    $ docker exec -it 35dd64b249a9 bash
    (pymc-dev) jovyan@35dd64b249a9:~/work$
      
    

    如果从这个 stdin 中 exit,不会导致容器的停止。这就是为什么推荐使用 docker exec的原因。

导出和导入

导出容器

如果要导出本地某个容器,可以使用 docker export 命令

1
docker export [ID] > 命名.tar

这样将导出容器快照到本地文件

导入容器快照

可以使用 docker import 从容器快照文件中再导入为镜像:

1
cat 命名.tar | docker import - test/命名:v1.0

此外,也可以通过指定URL或者某个目录来导入,例如

1
docker import http://example.com/exampleimage.tgz example/imagerepo

删除

  • 删除容器:可以使用 docker container rm 来删除一个处于终止状态的容器

  • 清理所有处于终止状态的容器:如果数量太多,一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器。

    1
    
    docker container prune
    

数据管理

image

这一张介绍如何在 Docker 内部以及容器之间管理数据,在容器中管理数据主要有两种方式:

  • 数据卷(Volumes)
  • 挂在主机目录(Bind mounts)

数据卷

数据卷是一个可供一个或多个容器使用的特殊目录,它绕过 UnionFS,可以提供很多有用的特性:

  • 数据卷可以在容器之间共享和重用
  • 对数据卷的修改会立马生效
  • 对数据卷的更新,不会影响镜像
  • 数据卷默认会一直存在,即使容器被删除。

注意:数据卷的使用类似于 Linux下对目录或文件进行mount,镜像中的被指定为挂载点的目录中的文件会复制到数据卷中(仅数据卷为空时会复制)

创建一个数据卷

1
docker volume create tb-vol

查看所有的数据卷

1
2
3
4
5
docker volume ls

DRIVER    VOLUME NAME
local     tb-vol

在主机里使用一下命令查看指定数据卷的信息

1
docker volume inspect tb-vol

启动一个挂在数据卷的容器

在用 docker run 命令的时候,使用 –mount 标记来将数据卷挂载到容器里。在一次 docker run 中可以挂载多个数据卷。

下面创建一个名为web的容器,并加载一个数据卷到容器的 /usr/share/nginx/html目录

1
docker run -d -P --name web --mount source=tb-vol,target=/usr/share/nginx/html pymc/pymc
  • -P: Publish all exposed ports to random ports
  • --name: 对容器进行命名
  • --mount: 定义数据卷

查看数据卷的具体信息

在主机里使用以下命令可以查看web容器的信息

1
docker inspect web

数据卷信息在“Mounts” Keys下面

1
2
3
4
5
6
7
8
9
10
11
12
"Mounts": [
            {
                "Type": "volume",
                "Name": "tb-vol",
                "Source": "/var/lib/docker/volumes/tb-vol/_data", # 这个位置就是数据卷保存的默认位置
                "Destination": "/usr/share/nginx/html",
                "Driver": "local",
                "Mode": "z",
                "RW": true,
                "Propagation": ""
            }
        ],

删除数据卷

1
docker volume rm tb-vol

数据卷是被设计用来持久化数据的,它的生命周期独立于容器,Docker不会再容器被删除后自动删除数据卷,并且也不存在垃圾回收这样的极致来处理没有任何容器引用的数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v这个命令。

无助的数据卷可能会占用很多空间,需要清理使用一下命令:

1
docker volume prune

挂载主机目录

挂载一个主机目录作为数据卷

使用--mount 标记可以指定挂载一个本地主机的目录到容器中去。

1
docker run -d -P --name web --mount type=bind,source=/home/tb/MR/JCWC,target=/usr/share/nginx/html pymc/pymc

上面的命令加载主机上的 /home/tb/MR/JCWC目录到容器的 /usr/share/nginx/html 目录。这个功能在进行测试的时候十分方便,比如用户可以防止一些程序到本地目录中,用来查看容器是否正常工作。本地目录的路径必须是绝对路径。

Docker挂载主机目录的默认权限是读写,用户也可以增加 readonly指定为只读。

1
docker run -d -P --name web --mount type=bind,source=/home/tb/MR/JCWC,target=/usr/share/nginx/html,readonly pymc/pymc

查看数据卷的具体信息

1
docker inspect web

数据卷信息在“Mounts” Keys下面

1
2
3
4
5
6
7
8
9
10
"Mounts": [
            {
                "Type": "bind",
                "Source": "/home/tb/MR/JCWC",
                "Destination": "/usr/share/nginx/html",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            }
        ]

挂载一个本地主机文件作为数据卷

--mount标记也可以从主机挂载单个文件到容器中

1
2
3
4
5
6
7
8
9
$ docker run --rm -it \
   # -v $HOME/.bash_history:/root/.bash_history \
   --mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history \
   ubuntu:18.04 \
   bash

root@2affd44b4667:/# history
1  ls
2  diskutil list

这样就可以记录在容器输入过的命令了。

注意:挂载目录一般要重新创建容器,或者更改配置文件(太麻烦了)

实践

拉下来的pymc镜像有个小问题,jupyter 会默认使用 base 环境作为核,需要添加pymv-dev环境作为核:

1
2
(pymc-dev) jovyan@83b2b800d286:~/work$  python -m ipykernel install --user --name=pymc-dev --display-name "pymc-dev"
Installed kernelspec pymc-dev in /home/jovyan/.local/share/jupyter/kernels/pymc-dev

这样我们可以在jupyter中测试我们的代码。

1
2
3
4
5
6
7
8
docker run -it continuumio/miniconda3 bash


conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/linux-64/
conda config --set show_channel_urls yes

这样可以使用 docker commit一个镜像,但体积会非常臃肿,我还是使用Dockerfile进行构建吧

Dockefile构建所需镜像

1
2
3
4
5
6
7
8
FROM continuumio/miniconda3

RUN conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ \
    && conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/ \
    && conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/ \
    && conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/linux-64/ \
    && conda install -c conda-forge "pymc>=5" \
    && apt-get purge -y --auto-remove $buildDeps

这样确实可以很方便地构建一个docker,但这个docker太大了,有6GB,这样可能还不如使用conda来管理环境。

This post is licensed under CC BY 4.0 by the author.