docker 学习笔记

何为Docker

Docker 使用 Google 公司推出的 Go 语言 进行开发实现,基于 Linux 内核的 cgroupnamespace,以及 OverlayFS 类的 Union FS 等技术,对进程进行封装隔离,属于 操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。最初实现是基于 LXC,从 0.7 版本以后开始去除 LXC,转而使用自行开发的 libcontainer,从 1.11 版本开始,则进一步演进为使用 runCcontainerd

Docker 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得 Docker 技术比虚拟机技术更为轻便、快捷。

下面的图片比较了 Docker 和传统虚拟化方式的不同之处。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。

1280X1280.PNG

1280X1280 _1_.PNG

为何使用docker

  • 更高效的利用系统资源
  • 更快速的启动
  • 一致的运行环境
  • 更轻松迁移
  • 更轻松的维护拓展

特性容器虚拟机启动秒级分钟级硬盘使用一般为 MB一般为 GB性能接近原生弱于系统支持量单机支持上千个容器一般几十个

docker的概念

Docker 包括三个基本概念

  • 镜像Image
  • 容器Container
  • 仓库Repository

镜像

镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。

分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。

容器

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 实例 一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。

容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。也因为这种隔离的特性,很多人初学 Docker 时常常会混淆容器和虚拟机。

前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为 容器存储层

容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。

按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume)、或者 绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。

数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器删除或者重新运行之后,数据却不会丢失。

仓库

镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。

docker隔离机制

a5a7a958-a5ab-49cc-a04b-a4ed545f8978.png

使用ps auxf命令查看进程树,得到的结果更形象直观,docker启动的一系列进程间关系可以和上图对应上:

916ac05f-524f-45df-9d3c-1bad66c7304d.png

(1)docker client:我们熟悉的docker命令行工具(docker ps等命令),我们通过该工具和docker daemon进行命令交互,得到结果。

(2)docker daemon:我们在进程中看到的/usr/bin/dockerd就是docker daemon,docker启动后,会自动监听一个docker.sock,可以用curl命令发送unix-socket类型请求与api来交互。

(3)containerd:一个守护进程,可管理宿主机上容器整个生命周期,另外还负责镜像管理、跨容器网络管理等。

(4)containerd-shim:位于containerd和runc组件之间,containerd 收到请求后不会直接操作容器,而是创建containerd-shim 的进程。由其去操作容器。

(5)runc:广泛使用的一个容器运行时。容器的启停由containerd-shim去调用runc 来启动容器,runc 启动完容器后会退出,containerd-shim成为容器的父进程。

Namespace

Linux内核的Namespaces(命名空间)机制提供了UTS、User、Network、PID等命名空间实现了主机名、用户、网络、进程等六项资源隔离功能:

74baed6b-2930-40c6-ab30-e8f966766d71.png

Namespace可以为容器提供系统资源隔离能力。简单说来,操作系统通过 Namespace隔离宿主机与容器的资源,让启动的docker容器误以为自己就是个独立运行的宿主机。然而,这个容器本质上也就是宿主机众多进程当中一个。

宿主机上,我们可通过ls -alh /proc/pid/ns来查看对应进程的namespace文件描述符。

Cgroup

cgroup:control group的简称,又称控制组,主要做资源控制,属于linux内核提供的一个特性。其原理是将一组进程放在一个控制组里,通过给这个控制组分配指定的可用资源,达到控制这一组进程可用资源的目的。

CGroup有以下几个基本概念:

(1)task(任务):对应于系统中运行的一个实体

(2)subsystem(子系统):,具体的资源控制器,控制某个特定的资源使用

(3)hierarchy(层级树):由一系列CGroup组成的树形结构。每个节点都是一个 CGroup ,CGroup 可以有多个子节点,子节点默认会继承父节点的属性。系统中可以有多个 hierarchy。

Cgroup部分常见子系统如下:

035ec5b0-aa58-495e-a73c-2b808cfcda50.png

我们可通过ls -al /sys/fs/cgroup/查看当前系统cgroup

安装docker

1
警告:切勿在没有配置 Docker APT 源的情况下直接使用 apt 命令安装 Docker.

卸载旧版本

旧版本的 Docker 称为 docker 或者 docker-engine,使用以下命令卸载旧版本:

1
2
3
sudo apt-get remove docker \
docker-engine \
docker.io

安装依赖与CA证书

1
2
3
4
5
6
7
8
sudo apt-get update

sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release

添加密钥

1
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

添加docker软件源

1
2
3
4
echo \
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/debian \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
以上命令会添加稳定版本的 Docker APT 源,如果需要测试版本的 Docker 请将 stable 改为 test。

安装docker 更新 apt 软件包缓存,并安装 docker-ce

1
2
sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io

使用脚本

1
2
3
4
# $ curl -fsSL test.docker.com -o get-docker.sh
$ curl -fsSL get.docker.com -o get-docker.sh
$ sudo sh get-docker.sh --mirror Aliyun
# $ sudo sh get-docker.sh --mirror AzureChinaCloud

启动docker

1
2
sudo systemctl enable docker
sudo systemctl start docker

建立docker用户组

建立 docker 组:

1
sudo groupadd docker

将当前用户加入 docker 组:

1
sudo usermod -aG docker $USER

退出当前终端并重新登录,进行如下测试

安装docker compose

Compose 是用于定义和运行多容器 Docker 应用程序的工具。通过 Compose,您可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。

由于github不稳定 443报错可以通过解析域名ip更改host解决

无报错 速度没有进展则刷新几次

1
$ sudo curl -L "https://github.com/docker/compose/releases/download/v2.2.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

将可执行权限应用于二进制文件

1
sudo chmod +x /usr/local/bin/docker-compose

创建软链

1
$ sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

测试

1
docker-compose version

操作容器

简单的说,容器是独立运行的一个或一组应用,以及它们的运行态环境。对应的,虚拟机可以理解为模拟运行的一整套操作系统(提供了运行态环境和其他系统环境)和跑在上面的应用。

启动容器

1
2
3
新建并启动` `docker run
docker run ubuntu:18.04 /bin/echo 'Hello world'
Hello world

上文echo输出之后终止容器 而下文则是启动一个bash终端,允许用户进行交互

1
docker run -t -i ubuntu:18.04 /bin/bash

-t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上

-i 则让容器的标准输入保持打开

1
2
3
4
5
6
7
8
使用docker镜像nginx:latest以后台模式启动一个容器,并将容器命名为mynginx。
docker run --name mynginx -d nginx:latest
使用镜像nginx:latest以后台模式启动一个容器,并将容器的80端口映射到主机随机端口。
docker run -P -d nginx:latest
使用镜像 nginx:latest,以后台模式启动一个容器,将容器的 80 端口映射到主机的 8000 端口,主机的目录 /data1 映射到容器的 /data。
docker run -p 8000:80 -v /data1:/data -d nginx:latest
后台运行ubuntu容器,并返回该容器ID
[root@localhost ~]# docker run -i -d ubuntu:latest

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

  • 检查本地是否存在指定的镜像,不存在就从 registry 下载
  • 利用镜像创建并启动一个容器
  • 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
  • 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
  • 从地址池配置一个 ip 地址给容器
  • 执行用户指定的应用程序
  • 执行完毕后容器被终止
1
2
3
退出即删除容器
docker run -p 宿主机端口:容器端口 -it --rm -v /宿主机目录:/容器目录 ubuntu:14.04 bash
启动已终止的容器

利用 docker container start 命令,直接将一个已经终止(exited)的容器启动运行。

1
docker start xxx

守护态运行

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

1
容器是否会长久运行,是和 docker run 指定的命令有关,和 -d 参数无关。

不使用 -d 运行容器 容器会把输出的结果 (STDOUT) 打印到宿主机上面

1
2
3
4
5
docker run ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
hello world
hello world
hello world
hello world

使用 -d 运行容器 结果可以用 docker logs 查看

1
2
docker run -d ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a

在这里 之前和学弟交流时我理解有误 当时误认为守护态是在启动时写一个死循环并且运行

但是这里的守护态其实是以 -d 参数启动后台运行 那么这里又抛出了一个问题 为什么要写一个死循环呢?

1
"while true; do echo hello world; sleep 1; done"

Docker 启动一个容器可以理解为一个进程 当我们从交互页面退出时 该进程结束容器便终止

因此需要在容器内写一个死循环 保持容器的持续运行 当然也可以写一个脚本并在dockerfile中运行

1
2
3
4
5
6
7
while true;
do

sleep 1

done
CMD ["/bin/sh", "-c", "/exec/run.sh"]#dockerfile详情 见dockerfile优化

终止容器

docker container stop 终止一个运行中的容器

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

进入容器

1
docker exec -it 69d1 bash

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

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

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

删除容器

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

docker container ls -a 命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器。

1
docker container prune

文件传输

1
2
3
docker cp 本地文件路径 ID全称:容器路径
# 或者
docker cp ID全称:容器文件路径 本地路径

使用镜像

获取镜像

从 Docker 镜像仓库获取镜像的命令是 docker pull。其命令格式为

1
docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
  • Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号]。默认地址是 Docker Hub(docker.io)。
  • 仓库名:如之前所说,这里的仓库名是两段式名称,即 <用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
docker pull robd003/python3.9
Using default tag: latest
latest: Pulling from robd003/python3.9
9b99af5931b3: Pull complete
b6013b3e77fe: Pull complete
bbced17b6899: Pull complete
8b609dabefa8: Pull complete
50544bfef33d: Pull complete
e28ddb7e93ed: Pull complete
3e7ef3848425: Pull complete
b9ff2ffeebd6: Pull complete
128f08ed1bc0: Pull complete
Digest: sha256:83a53d5562f108cb28542640b3c2d7f28f9f7d92ae1092631df764ceedae5478
Status: Downloaded newer image for robd003/python3.9:latest
docker.io/robd003/python3.9:latest

docker pull 命令的输出结果最后一行给出了镜像的完整名称

1
docker.io/robd003/python3.9:latest

如果上面的命令中没有给出 Docker 镜像仓库地址,则将会从 Docker Hub (docker.io)获取镜像。当镜像名称是 ubuntu:18.04,将会获取官方镜像 library/ubuntu 仓库中标签为 18.04 的镜像。

从下载过程中可以看到我们之前提及的分层存储的概念,镜像是由多层存储所构成。下载也是一层层的去下载,并非单一文件。下载过程中给出了每一层的 ID 的前 12 位。并且下载结束后,给出该镜像完整的 sha256 的摘要,以确保下载一致性。

运行镜像

1
2
3
4
5
6
7
8
9
10
docker run -it --rm robd003/python3.9 bash
PRETTY_NAME="Debian GNU/Linux 10 (buster)"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
  • --rm:这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 docker rm。我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用 --rm 可以避免浪费空间

列出镜像

1
2
3
4
5
6
7
8
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 5f515359c7f8 5 days ago 183 MB
nginx latest 05a60462f8ba 5 days ago 181 MB
mongo 3.2 fe9198c04d62 5 days ago 342 MB
<none> <none> 00285df0df87 5 days ago 342 MB
ubuntu 18.04 329ed837d508 3 days ago 63.3MB
ubuntu bionic 329ed837d508 3 days ago 63.3MB

列表包含了 仓库名标签镜像 ID创建时间 以及 所占用的空间

其中仓库名、标签在之前的基础概念章节已经介绍过了。镜像 ID 则是镜像的唯一标识,一个镜像可以对应多个 标签。因此,在上面的例子中,我们可以看到 ubuntu:18.04ubuntu:bionic 拥有相同的 ID,因为它们对应的是同一个镜像。

虚悬镜像

上面的镜像列表中,还可以看到一个特殊的镜像,这个镜像既没有仓库名,也没有标签,均为 <none>

1
<none>               <none>              00285df0df87        5 days ago          342 MB

这个镜像原本是有镜像名和标签的,原来为 mongo:3.2,随着官方镜像维护,发布了新版本后,重新 docker pull mongo:3.2 时,mongo:3.2 这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了 <none>。除了 docker pull 可能导致这种情况,docker build 也同样可以导致这种现象。由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 <none> 的镜像。这类无标签镜像也被称为 虚悬镜像(dangling image) ,可以用下面的命令专门显示这类镜像

1
2
3
docker image ls -f dangling=true
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 00285df0df87 5 days ago 342 MB

一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以用下面的命令删除

1
docker image prune

中间层镜像

为了加速镜像构建、重复利用资源,Docker 会利用 中间层镜像。所以在使用一段时间后,可能会看到一些依赖的中间层镜像。默认的 docker image ls 列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 -a 参数。这样会看到很多无标签的镜像,与之前的虚悬镜像不同,这些无标签的镜像很多都是中间层镜像,是其它镜像所依赖的镜像。这些无标签镜像不应该删除,否则会导致上层镜像因为依赖丢失而出错。

1
$ docker image ls -a

删除镜像

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

其中,<镜像> 可以是 镜像短 ID镜像长 ID镜像名 或者 镜像摘要

我们可以用镜像的完整 ID,也称为 长 ID,来删除镜像。使用脚本的时候可能会用长 ID,但是人工输入就太累了,所以更多的时候是用 短 ID 来删除镜像。docker image ls 默认列出的就已经是短 ID 了,一般取前3个字符以上,只要足够区分于别的镜像就可以了。

本文原本想针对dockerfile进行详细叙述,但前置知识写了甚多很难分清主次,那么让我们来聊一聊dockersfile。

dockerfile

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

FROM 指定基础镜像

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

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

1
FROM scratch

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

RUN执行命令

  • shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。
1
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。
1
2
RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget

Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。

Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层

正确的写法

1
2
RUN set -x; buildDeps='gcc libc6-dev make wget' \
&& apt-get update \

COPY复制文件

1
COPY [--chown=<user>:<group>] <源路径>... <目标路径>

COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。比如:

1
COPY package.json /usr/src/app/

<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

1
2
COPY hom* /mydir/
COPY hom?.txt /mydir/

<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。

1
2
3
4
COPY --chown=55:mygroup files* /mydir/
COPY --chown=bin files* /mydir/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/

CMD容器启动命令

  • shell 格式:CMD <命令>
  • exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
  • 在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。
1
2
3
4
如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行
CMD echo $HOME
在实际执行中,会将其变更为
CMD [ "sh", "-c", "echo $HOME" ]

记一次dockerfile的优化

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FROM robd003/python3.9

RUN mkdir /exec && mkdir /app
COPY exec /exec
RUN cp /exec/sources.list /etc/apt/sources.list && \
cd /tmp && wget https://download.java.net/java/GA/jdk17.0.2/dfd4a8d0985749f896bed50d7138ee7f/8/GPL/openjdk-17.0.2_linux-x64_bin.tar.gz && \
tar xvf openjdk-17.0.2_linux-x64_bin.tar.gz && \
rm openjdk-17.0.2_linux-x64_bin.tar.gz && \
mv jdk-17.0.2/ /opt/jdk-17 && \
apt update && apt install sudo -y && \
echo 'export JAVA_HOME=/opt/jdk-17' | sudo tee /etc/profile.d/java17.sh && \
echo 'export PATH=$JAVA_HOME/bin:$PATH'|sudo tee -a /etc/profile.d/java17.sh && \
/bin/bash -c "source /etc/profile.d/java17.sh" && \
/bin/bash -c "source /etc/environment"

CMD ["/bin/sh", "-c", "/exec/run.sh"]

Compose

1
2
3
4
5
6
7
8
9
10
11
version: "2"

services:
web:
build: .
image: ubuntu21_base
restart: always
ports:
- "8080:8080"
volumes:
- "/root/envTest/src:/app"

以上是 一份QQbot环境的dockerfile 当时刚把所有功能写好对于上线时间 急急急 转了许多弯子

优化部分为环境变量配置部分

1
2
3
4
echo 'export JAVA_HOME=/opt/jdk-17' | sudo tee /etc/profile.d/java17.sh && \ 
echo 'export PATH=$JAVA_HOME/bin:$PATH'|sudo tee -a /etc/profile.d/java17.sh && \
/bin/bash -c "source /etc/profile.d/java17.sh" && \
/bin/bash -c "source /etc/environment"

这里是将环境变量声明命令写入sh 放在profile.d 目录下 并无作用

后面尝试 source 读取变量信息 但是重新连接shell后仍无法找到变量

最终在/etc/bash.bashrc 中写入 source /etc/profile.d/java17.sh 每次连接都能获取到环境变量

原因在于 loginshell 与 nonloginshell的环境读取差别 详情见上一篇文章

优化后dockerfile如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM robd003/python3.9

RUN mkdir /exec && mkdir /app
COPY exec /exec
RUN cp exec/sources.list /etc/apt/sources.list && \
cd /tmp && wget https://download.java.net/java/GA/jdk17.0.2/dfd4a8d0985749f896bed50d7138ee7f/8/GPL/openjdk-17.0.2_linux-x64_bin.tar.gz && \
tar xvf openjdk-17.0.2_linux-x64_bin.tar.gz && \
rm openjdk-17.0.2_linux-x64_bin.tar.gz && \
mv jdk-17.0.2/ /opt/jdk-17 && \
apt update && apt install sudo -y && \
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
echo 'export JAVA_HOME=/opt/jdk-17' >> /etc/bash.bashrc && \
echo 'export PATH=$JAVA_HOME/bin:$PATH' >> /etc/bash.bashrc && \
echo 'export LANG="C.UTF-8"' >> /etc/bash.bashrc && \
/bin/bash -c "source /etc/bash.bashrc"

CMD ["/bin/sh", "-c", "/exec/run.sh"]

docker的安全问题

https://wiki.teamssix.com/CloudNative/Docker/docker-risks.html

容器镜像存在风险

业务镜像使用了log4j2 等不安全的第三方组件 亦或是 在dockerhub中下载到包含漏洞、恶意镜像

当然在打包数据时 将账号密码等敏感信息打包到镜像中 便会导致敏感信息泄露

不安全的配置与挂载

如果为容器设定了不安全的配置,会导致容器本身的隔离机制失效,容器的两大隔离机制如下:

  • Linux 命名空间(NameSpace):实现文件系统、网络、进程、主机名等方面的隔离
  • Linux 控制组(cgroups):实现 CPU、内存、硬盘等方面的隔离

如果设定了以下配置就会导致相应的隔离机制失效:

  • –privileged:使容器内的 root 权限和宿主机上的 root 权限一致,权限隔离被打破
  • –net=host:使容器与宿主机处于同一网络命名空间,网络隔离被打破
  • –pid=host:使容器与宿主机处于同一进程命令空间,进程隔离被打破
  • –volume /:/host:宿主机根目录被挂载到容器内部,文件系统隔离被打破

容器与漏洞检测

1
2
3
4
5
6
7
8
9
10
11
12
判断当前环境是不是容器环境
cat /proc/1/cgroup | grep -qi docker && echo "Is Docker" || echo "Not Docker"
是否特权模式
cat /proc/self/status | grep -qi "0000003fffffffff" && echo "Is privileged mode" || echo "Not privileged mode"
挂载 docker socket
ls /var/run/ | grep -qi docker.sock && echo "Docker Socket is mounted." || echo "Docker Socket is not mounted."
挂载 procfs
find / -name core_pattern 2>/dev/null | wc -l | grep -q 2 && echo "Procfs is mounted." || echo "Procfs is not mounted."
挂载宿主机根目录
find / -name passwd 2>/dev/null | grep /etc/passwd | wc -l | grep -q 7 && echo "Root directory is mounted." || echo "Root directory is not mounted."
Docker remote api 未授权访问
IP=`hostname -i | awk -F. '{print $1 "." $2 "." $3 ".1"}' ` && timeout 3 bash -c "echo >/dev/tcp/$IP/2375" > /dev/null 2>&1 && echo "Docker Remote API Is Enabled." || echo "Docker Remote API is Closed."

检测内核漏洞

1
2
3
4
5
6
1、CVE-2016-5195 DirtyCow 逃逸
执行 uname -r 命令,如果在 2.6.22 <= 版本 <= 4.8.3 之间说明可能存在 CVE-2016-5195 DirtyCow 漏洞。
2、CVE-2020-14386
执行 uname -r 命令,如果在 4.6 <= 版本 < 5.9 之间说明可能存在 CVE-2020-14386 漏洞。
3、CVE-2022-0847 DirtyPipe 逃逸
执行 uname -r 命令,如果在 5.8 <= 版本 < 5.10.102 < 版本 < 5.15.25 < 版本 < 5.16.11 之间说明可能存在 CVE-2022-0847 DirtyPipe 漏洞。

容器逃逸检测脚本https://github.com/teamssix/container-escape-check

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
wget https://raw.githubusercontent.com/teamssix/container-escape-check/main/container-escape-check.sh -O -| bash
#!/bin/bash


echo -e ""
echo -e "\033[34m=============================================================\033[0m"
echo -e "\033[34m Containers Escape Check v0.3 \033[0m"
echo -e "\033[34m-------------------------------------------------------------\033[0m"
echo -e "\033[34m Author: TeamsSix \033[0m"
echo -e "\033[34m Twitter: TeamsSix \033[0m"
echo -e "\033[34m Blog: teamssix.com \033[0m"
echo -e "\033[34m WeChat Official Accounts: TeamsSix \033[0m"
echo -e "\033[34m Project Address: github.com/teamssix/container-escape-check \033[0m"
echo -e "\033[34m=============================================================\033[0m"
echo -e ""

# Supported detection methods:
#
# 1. Privileged Mode
# 2. Mount Docker Socket
# 3. Mount Procfs
# 4. Mount Root Directory
# 5. Open Docker Remote API
# 6. CVE-2016-5195 DirtyCow
# 7. CVE-2020-14386
# 8. CVE-2022-0847 DirtyPipe
# 9. CVE-2017-1000112
# 10. CVE-2021-22555
# 11. Mount Host Var Log
# 12. CAP_DAC_READ_SEARCH
# 13. CAP_SYS_ADMIN
# 14. CAP_SYS_PTRACE
# 15. CVE-2022-0492


CheckCommandExists(){
$1 >/dev/null 2>&1
ret=$?
if [ "$ret" -eq 0 ]; then
return 1
fi
return 0
}


# Install command
InstallCommand(){
# install command if not present
CheckCommandExists $1
if [ $? -eq 0 ]; then
# Check network
timeout 3 bash -c "echo -e >/dev/tcp/baidu.com/80" > /dev/null 2>&1 && IsNetWork=1 || IsNetWork=0
if [ $IsNetWork -eq 1 ];then
echo -e "\033[93m[!] It is detected that the $1 command does not exist in the current system, and the command is being installed.\033[0m"

CheckCommandExists sudo
if [ $? -eq 0 ]; then
CheckCommandExists apt-get
if [ $? -eq 0 ];then
if [ "$1" = "capsh" ];then
apt-get -y update >/dev/null 2>&1 && apt-get install -y libcap2-bin >/dev/null 2>&1
else
apt-get -y update >/dev/null 2>&1 && apt-get install -y $1 >/dev/null 2>&1
fi
fi
CheckCommandExists yum
if [ $? -eq 0 ];then
if [ "$1" = "capsh" ];then
yum -y update >/dev/null 2>&1 && yum install -y libcap >/dev/null 2>&1
else
yum -y update >/dev/null 2>&1 && yum install -y $1 >/dev/null 2>&1
fi
fi
else
CheckCommandExists apt-get
if [ $? -eq 0 ];then
if [ "$1" = "capsh" ];then
sudo apt-get -y update >/dev/null 2>&1 && apt-get install -y libcap2-bin >/dev/null 2>&1
else
sudo apt-get -y update >/dev/null 2>&1 && apt-get install -y $1 >/dev/null 2>&1
fi
fi
CheckCommandExists yum
if [ $? -eq 0 ];then
if [ "$1" = "capsh" ];then
sudo yum -y update >/dev/null 2>&1 && yum install -y libcap >/dev/null 2>&1
else
sudo yum -y update >/dev/null 2>&1 && yum install -y $1 >/dev/null 2>&1
fi
fi
fi
CheckCommandExists $1
if [ $? -eq 0 ]; then
echo -e "\033[93m[!] $1 command installation failed.\033[0m"
else
echo -e "\033[93m[!] $1 command installation completed.\033[0m"
fi
fi
fi
}


# 0. Check The Current Environment
CheckTheCurrentEnvironment(){
if [ ! -f "/proc/1/cgroup" ];then
IsContainer=0
else
cat /proc/1/cgroup | grep -qi docker && IsContainer=1 || IsContainer=0
fi

if [ $IsContainer -eq 0 ];then
echo -e "\033[31m[-] Not currently a container environment.\033[0m"
exit 1
else
echo -e "\033[33m[!] Currently in a container, checking ......\033[0m"
VulnerabilityExists=0
fi
}


# 1. Check Privileged Mode
CheckPrivilegedMode(){
if [ ! -f "/proc/self/status" ];then
IsPrivilegedMode=0
else
cat /proc/self/status | grep -qi "0000003fffffffff" && IsPrivilegedMode=1 || IsPrivilegedMode=0
cat /proc/self/status | grep -qi "0000001fffffffff" && IsPrivilegedMode=1 || IsPrivilegedMode=0
fi

if [ $IsPrivilegedMode -eq 1 ];then
echo -e "\033[92m[+] The current container is in privileged mode.\033[0m"
VulnerabilityExists=1
fi

}


# 2. Check Docker Socket Mount
CheckDockerSocketMount(){
if [ ! -f "/var/run/docker.sock" ];then
IsDockerSocketMount=0
else
ls /var/run/ | grep -qi docker.sock && IsDockerSocketMount=1 || IsDockerSocketMount=0
fi

if [ $IsDockerSocketMount -eq 1 ];then
echo -e "\033[92m[+] The current container has docker socket mounted.\033[0m"
VulnerabilityExists=1
fi
}


# 3. Check Procfs Mount
CheckProcfsMount(){

find / -name core_pattern 2>/dev/null | wc -l | grep -q 2 && IsProcfsMount=1 || IsProcfsMount=0

if [ $IsProcfsMount -eq 1 ];then
echo -e "\033[92m[+] The current container has procfs mounted.\033[0m"
VulnerabilityExists=1
fi
}


# 4. Check Root Directory Mount
CheckRootDirectoryMount(){

find / -name passwd 2>/dev/null | grep /etc/passwd | wc -l | grep -q 7 && IsRootDirectoryMount=1 || IsRootDirectoryMount=0

if [ $IsRootDirectoryMount -eq 1 ];then
echo -e "\033[92m[+] The current container has root directory mounted.\033[0m"
VulnerabilityExists=1
fi
}


# 5. Check Docker Remote API
CheckDockerRemoteAPI(){
InstallCommand hostname
for PORT in "2375" "2376"
do
IP=`hostname -i | awk -F. '{print $1 "." $2 "." $3 ".1"}' ` && timeout 3 bash -c "echo -e >/dev/tcp/$IP/$PORT" > /dev/null 2>&1 && DockerRemoteAPIIsEnabled=1 || DockerRemoteAPIIsEnabled=0
if [ $DockerRemoteAPIIsEnabled -eq 1 ];then
echo -e "\033[92m[+] The Docker Remote API for the current container is enabled.\033[0m"
VulnerabilityExists=1
fi
done
}


LinuxKernelVersion=`uname -r | awk -F '-' '{print $1}'`
KernelVersion=`echo -e $LinuxKernelVersion | awk -F '.' '{print $1}'`
MajorRevision=`echo -e $LinuxKernelVersion | awk -F '.' '{print $2}'`
MinorRevision=`echo -e $LinuxKernelVersion | awk -F '.' '{print $3}'`


# 6. Check CVE-2016-5195 DirtyCow
# 2.6.22 <= ver <= 4.8.3
CheckCVE_2016_5195DirtyCow(){
# 2.6.22 <= ver <= 2.6.xx
if [[ "$KernelVersion" -eq 2 && "$MajorRevision" -eq 6 && "$MinorRevision" -ge 22 ]];then
echo -e "\033[92m[+] The current container has the CVE-2016-5195 DirtyCow vulnerability.\033[0m"
VulnerabilityExists=1
fi

# 2.7 <= ver <= 2.x
if [[ "$KernelVersion" -eq 2 && "$MajorRevision" -ge 7 ]];then
echo -e "\033[92m[+] The current container has the CVE-2016-5195 DirtyCow vulnerability.\033[0m"
VulnerabilityExists=1
fi

# ver = 3
if [[ "$KernelVersion" -eq 3 ]];then
echo -e "\033[92m[+] The current container has the CVE-2016-5195 DirtyCow vulnerability.\033[0m"
VulnerabilityExists=1
fi

# 4.x <= ver <= 4.8
if [[ "$KernelVersion" -eq 4 && "$MajorRevision" -lt 8 ]];then
echo -e "\033[92m[+] The current container has the CVE-2016-5195 DirtyCow vulnerability.\033[0m"
VulnerabilityExists=1
fi

# 4.8.x <= ver <= 4.8.3
if [[ "$KernelVersion" -eq 4 && "$MajorRevision" -eq 8 && "$MinorRevision" -le 3 ]];then
echo -e "\033[92m[+] The current container has the CVE-2016-5195 DirtyCow vulnerability.\033[0m"
VulnerabilityExists=1
fi
}


# 7. CVE-2020-14386
# 4.6 <= ver < 5.9
CheckCVE_2020_14386(){
# 4.6 <= ver < 4.x
if [[ "$KernelVersion" -eq 4 && "$MajorRevision" -ge 6 ]];then
echo -e "\033[92m[+] The current container has the CVE-2020-14386 vulnerability.\033[0m"
VulnerabilityExists=1
fi

# 5.x <= ver < 5.9
if [[ $KernelVersion -eq 5 && $MajorRevision -lt 9 ]];then
echo -e "\033[92m[+] The current container has the CVE-2020-14386 vulnerability.\033[0m"
VulnerabilityExists=1
fi
}


# 8. CVE-2022-0847 DirtyPipe
# 5.8 <= ver < 5.10.102 < ver < 5.15.25 < ver < 5.16.11
CheckCVE_2022_0847(){
if [ $KernelVersion -eq 5 ];then
# 5.8 <= ver < 5.10.x
if [[ "$MajorRevision" -ge 8 && "$MajorRevision" -lt 10 ]];then
echo -e "\033[92m[+] The current container has the CVE-2022-0847 DirtyPipe vulnerability.\033[0m"
VulnerabilityExists=1
fi
# 5.10.x <= ver < 5.10.102
if [[ "$MajorRevision" -eq 10 && "$MinorRevision" -lt 102 ]];then
echo -e "\033[92m[+] The current container has the CVE-2022-0847 DirtyPipe vulnerability.\033[0m"
VulnerabilityExists=1
fi
# 5.10.102 < ver <= 5.10.x
if [[ "$MajorRevision" -eq 10 && "$MinorRevision" -gt 102 ]];then
echo -e "\033[92m[+] The current container has the CVE-2022-0847 DirtyPipe vulnerability.\033[0m"
VulnerabilityExists=1
fi

# 5.10.x < ver < 5.15.x
if [[ "$MajorRevision" -gt 10 && "$MajorRevision" -lt 15 ]];then
echo -e "\033[92m[+] The current container has the CVE-2022-0847 DirtyPipe vulnerability.\033[0m"
VulnerabilityExists=1
fi

# 5.15.x <= ver < 5.15.25
if [[ "$MajorRevision" -eq 15 && "$MinorRevision" -lt 25 ]];then
echo -e "\033[92m[+] The current container has the CVE-2022-0847 DirtyPipe vulnerability.\033[0m"
VulnerabilityExists=1
fi
# 5.15.25 < ver <= 5.15.x
if [[ "$MajorRevision" -eq 15 && "$MinorRevision" -gt 25 ]];then
echo -e "\033[92m[+] The current container has the CVE-2022-0847 DirtyPipe vulnerability.\033[0m"
VulnerabilityExists=1
fi

# 5.16.x <= ver < 5.16.11
if [[ "$MajorRevision" -eq 16 && "$MinorRevision" -lt 11 ]];then
echo -e "\033[92m[+] The current container has the CVE-2022-0847 DirtyPipe vulnerability.\033[0m"
VulnerabilityExists=1
fi
fi
}


# 9. CVE-2017-1000112
# 4.4 <= ver<=4.13
CheckCVE_2017_1000112(){
# 4.4 <= ver <= 4.13
if [[ "$KernelVersion" -eq 4 && "$MajorRevision" -ge 4 && "$MajorRevision" -le 13 ]];then
echo -e "\033[92m[+] The current container has the CVE-2017-1000112 vulnerability.\033[0m"
VulnerabilityExists=1
fi
}


# 10. CVE-2021-22555
# 2.6.19 <= ver <= 5.12
CheckCVE_2021_22555(){
# 2.6.19 <= ver <= 2.6.xx
if [[ "$KernelVersion" -eq 2 && "$MajorRevision" -eq 6 && "$MinorRevision" -ge 19 ]];then
echo -e "\033[92m[+] The current container has the CVE-2021-22555 vulnerability.\033[0m"
VulnerabilityExists=1
fi
# 2.7 <= ver <= 2.x
if [[ "$KernelVersion" -eq 2 && "$MajorRevision" -ge 7 ]];then
echo -e "\033[92m[+] The current container has the CVE-2021-22555 vulnerability.\033[0m"
VulnerabilityExists=1
fi

# ver = 3 or ver = 4
if [[ "$KernelVersion" -eq 3 || "$KernelVersion" -eq 4 ]];then
echo -e "\033[92m[+] The current container has the CVE-2021-22555 vulnerability.\033[0m"
VulnerabilityExists=1
fi

# 5.x <= ver <= 5.12
if [[ $KernelVersion -eq 5 && $MajorRevision -le 12 ]];then
echo -e "\033[92m[+] The current container has the CVE-2021-22555 vulnerability.\033[0m"
VulnerabilityExists=1
fi
}


# 11. Mount Host Var Log
CheckVarLogMount(){
if [ ! -f "/var/run/secrets/kubernetes.io/serviceaccount/token" ];then
IsPodEnv=0
else
IsPodEnv=1
fi
if [ $IsPodEnv -eq 1 ];then
find / -name lastlog 2>/dev/null | wc -l | grep -q 3 && IsVarLogMount=1 || IsVarLogMount=0
if [ $IsVarLogMount -eq 1 ];then
echo -e "\033[92m[+] The current container has /var/log mounted.\033[0m"
VulnerabilityExists=1
fi
fi
}


# 12. Check CAP_DAC_READ_SEARCH
ChekckCAP_DAC_READ_SEARCH(){
if command -v capsh >/dev/null 2>&1; then
cap_dac_read_searchNum=`capsh --print | grep cap_dac_read_search | wc -l`
if [ $cap_dac_read_searchNum -gt 0 ];then
echo -e "\033[92m[+] The current container has the CAP_DAC_READ_SEARCH permission.\033[0m"
VulnerabilityExists=1
fi
fi
}


# 13. Check CAP_SYS_ADMIN
CheckCAP_SYS_ADMIN(){
if command -v capsh >/dev/null 2>&1; then
cap_sys_adminNum=`capsh --print | grep cap_sys_admin | wc -l`
if [ $cap_sys_adminNum -gt 0 ];then
echo -e "\033[92m[+] The current container has the CAP_SYS_ADMIN permission.\033[0m"
VulnerabilityExists=1
fi
fi
}


# 14. Check CAP_SYS_PTRACE
CheckCAP_SYS_PTRACE(){
if command -v capsh >/dev/null 2>&1; then
cap_sys_ptraceNum=`capsh --print | grep cap_sys_ptrace | wc -l`
if [ $cap_sys_ptraceNum -gt 0 ];then
echo -e "\033[92m[+] The current container has the CAP_SYS_PTRACE permission.\033[0m"
VulnerabilityExists=1
fi
fi
}


# 15. Check CVE-2022-0492, Code By https://github.com/PaloAltoNetworks/can-ctr-escape-cve-2022-0492/blob/main/can-ctr-escape-cve-2022-0492.sh
CheckCVE_2022_0492(){
# Setup test dir
test_dir=/tmp/.cve-2022-0492-test
if mkdir -p $test_dir ; then
# Test whether escape via user namespaces is possible
while read -r subsys
do
if unshare -UrmC --propagation=unchanged bash -c "mount -t cgroup -o $subsys cgroup $test_dir 2>&1 >/dev/null && test -w $test_dir/release_agent" >/dev/null 2>&1 ; then
echo -e "\033[92m[+] The current container has the CVE-2022-0492 vulnerability.\033[0m"
fi
done <<< $(cat /proc/$$/cgroup | grep -Eo '[0-9]+:[^:]+' | grep -Eo '[^:]+$')
umount $test_dir >/dev/null 2>&1 && rm -rf $test_dir >/dev/null 2>&1
fi
}


main()
{
# 0. Check the current environment
CheckTheCurrentEnvironment

# 1. Check Privileged Mode
CheckPrivilegedMode

# 2. Check Docker Socket Mount
CheckDockerSocketMount

# 3. Check Procfs Mount
CheckProcfsMount

# 4. Check Root Directory Mount
CheckRootDirectoryMount

# 5. Check Docker Remote API
CheckDockerRemoteAPI

# 6. Check CVE-2016-5195 DirtyCow
CheckCVE_2016_5195DirtyCow

# 7. CVE-2020-14386
CheckCVE_2020_14386

# 8. CVE-2022-0847 DirtyPipe
CheckCVE_2022_0847

# 9. CVE-2017-1000112
CheckCVE_2017_1000112

# 10. CVE-2021-22555
CheckCVE_2021_22555

# 11. Mount Host Var Log
CheckVarLogMount

InstallCommand capsh

# 12. Check CAP_DAC_READ_SEARCH
ChekckCAP_DAC_READ_SEARCH

# 13. Check CAP_SYS_ADMIN
CheckCAP_SYS_ADMIN

# 14. Check CAP_SYS_PTRACE
CheckCAP_SYS_PTRACE

# 15. Check CVE-2022-0492
CheckCVE_2022_0492


if [ $VulnerabilityExists -eq 0 ];then
echo -e "\033[33m[!] Check completed, no vulnerability found. \033[0m"
else
echo -e "\033[33m[!] Check completed.\033[0m"
fi
}

main