Docker
容器是一种沙盒技术。沙盒就是能够像一个集装箱一样把应用(进程)装起来。这样应用(进程)与应用(进程)之间因为有了边界而互不干扰,被装进集装箱的一个用也可以方便的搬来搬去。
- 边界->运行态->container
- 打包->静态->image
image与container是1:n的关系。一个image可以生成多个container。就是 类与对象的概念。
运行态
容器的本质是进程
容器的本质是进程
容器的本质是进程
进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
程序运行起来之后计算机执行环境(比如内存中的数据、寄存器里的值、堆栈种的指令、打开的文件、以及各种设备的状态信息的一个集合)的总和叫做进程
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
对于进程而言,它的静态表现就是程序,一个二进制包或者一堆库加可执行文件。而一旦运行起来,就变成了计算机里的数据和状态的总和,这就是它的动态表现。
容器技术的核心功能就是通过约束和修改进程的动态表现,从而为其创造出一个边界
通常来说是个通过Cgroups技术来约束(限制Memory、CPU等),而通过Namespace技术来修改进程视图(网络、用户等)。
Namespace
# 查看所有namespace
# Cgroup namespace 目前还没有被 docker 采用。
ll /proc/1/ns/
- Mount Namespace -> 挂载
- UTS Namespace -> HostName
- IPC Namespace -> 进程间通信
- PID Namespace -> 进程号
- Network Namespace -> 网络
- User Namespace -> 用户
进入某一个进程的namespace可以用nsenter 命令
$ nsenter --help
Usage:
nsenter [options] [<program> [<argument>...]]
Run a program with namespaces of other processes.
Options:
-a, --all enter all namespaces
-t, --target <pid> target process to get namespaces from
-m, --mount[=<file>] enter mount namespace
-u, --uts[=<file>] enter UTS namespace (hostname etc)
-i, --ipc[=<file>] enter System V IPC namespace
-n, --net[=<file>] enter network namespace
-p, --pid[=<file>] enter pid namespace
-C, --cgroup[=<file>] enter cgroup namespace
-U, --user[=<file>] enter user namespace
-S, --setuid <uid> set uid in entered namespace
-G, --setgid <gid> set gid in entered namespace
--preserve-credentials do not touch uids or gids
-r, --root[=<dir>] set the root directory
-w, --wd[=<dir>] set the working directory
-F, --no-fork do not fork before exec'ing <program>
-Z, --follow-context set SELinux context according to --target PID
-h, --help display this help
-V, --version display version
For more details see nsenter(1).
PID Namespace是用来隔离进程ID的 同样一个进程在不同的PID Namespace里可以拥有不同的PID。可以看到的一个现象是。在docker container中 ps -ef
时,前台的进程PID是1.但是在容器外,该进程有不同的PID
Q: 如何找到容器内进程对应的容器外部进程PID?或者如果通过PID确定是哪个容器产生的进程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G2o3cVPp-1586222850065)(en-resource://database/3698:1)]
ps -ef 命令可以看到进程以及父进程的PID。
从上图中可以看到 PID为8739的/bin/sh进程的父进程为8718。PID为8718的进程中可以看到"/var/lib/containerd/io.containerd.runtime.v1.linux/moby/2e7cd2b51b9"。这其中 "2e7cd2b51b9"就是container id
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lqckopfN-1586222850069)(en-resource://database/3700:1)]
docker inpect
“State”.“Pid”
UTC Namespace 主要用来隔离nodename和domainname两个系统标识
hostname
IPC Namespace用来隔离System V IPC(信号量、消息队列、共享内存)和 POSIX message queues
#查询 message queue
ipcs -q
# 创建 message queue
ipcmk -Q
Mount Namespace 用来隔离各个进程看到的挂载点视图在不同Namespace的进程中,看到的文件系统层次是不一样的。在Mount Namespace中调用mount和unmount仅仅只会影响当前Namespace的文件系统。而对全局的文件系统没有影响。
df -h
User Namespace 主要是隔离用户的用户组ID 也就是说,一个进程的User ID和Group ID在User Namespace内外是可以不同的。
cat /etc/passwd
# 用postgresql镜像切换用户 到 psostgre
top
# 在宿主机
ps aux|grep top
挂载的文件在容器内外显示的属组和属主不同不是因为这个,是因为文件存储信息中保存的是userID。在容器内外的userID对应的用户名是不一样的。
Network Namespace 是用来隔离网络设备、IP地址端口等网络栈的
ip addr
Cgroups
Linux Cgroups提供了对一组进程以及将来子进程的资源限制、控制和统计的能力。这些资源包括CPU、内存、存储、网络等。通过Cgroups,可以方便地限制某个进程的资源占用。
- cgroup 对进程分组管理的一种机制,一个cgroup包含一组进程。并且可以在这个cgroup上增加subsystem的各种参数配置。
- subsystem是一组资源控制的模块。
lssubsys -a
- hierarchy的功能是把一组cgroup串成一个树状结构。
手动配置Cgroups:
# 首先创建并挂载一个hierarchy(cgroup树).
$ mkdir cgroup-test
$ mount -t cgroup -o none,name=cgroup-test cgroup-test ./cgroup-test
$ ls ./cgroup-test/
cgroup.clone_children cgroup.procs cgroup.sane_behavior notify_on_release release_agent tasks
- cgroup.clone_children cpuset的subsystem会读取这个配置文件,如果这个值是1(默认是0)子cgroup才会继承父cgroup的cpuset的配置
- cgroup.procs是树中当前节点cgroup中的进程组ID,现在的位置是在根节点,这个文件中会有现在系统中所有进程组的ID
- notify_on_release和release_agent会一起使用,notify_on_release表示当这个cgroup最后一个进程退出的时候是否执行了release_agent; release_agent 则是一个路径,通常用作进程退出后自动清理不再使用的cgroup。
- tasks 标识该cgroup下面的进程ID,如果把一个进程ID写到tasks文件中,便会将相应的进程加入到这个cgroup中。
# 在刚刚创建好的hierarchy上cgroup根节点扩展出两个子cgroup。
$ cd ./cgroup-test/
$ mkdir cgroup-1
$ mkdir cgroup-2
$ tree
.
├── cgroup-1
│ ├── cgroup.clone_children
│ ├── cgroup.procs
│ ├── notify_on_release
│ └── tasks
├── cgroup-2
│ ├── cgroup.clone_children
│ ├── cgroup.procs
│ ├── notify_on_release
│ └── tasks
├── cgroup.clone_children
├── cgroup.procs
├── cgroup.sane_behavior
├── notify_on_release
├── release_agent
└── tasks
可以看到在一个cgroup的目录下创建文件夹时,内核会把文件夹标记为这个cgroup的子group,他们会继承父cgroup的属性。
# 在cgroup中添加进程
$ cat /proc/$$/cgroup
13:name=cgroup-test:/
12:memory:/user.slice
11:perf_event:/
10:cpuset:/
9:devices:/user.slice
8:blkio:/user.slice
7:pids:/user.slice/user-1000.slice/session-8.scope
6:rdma:/
5:hugetlb:/
4:freezer:/
3:cpu,cpuacct:/user.slice
2:net_cls,net_prio:/
1:name=systemd:/user.slice/user-1000.slice/session-8.scope
0::/user.slice/user-1000.slice/session-8.scope
$ echo $$ >> cgroup-1/tasks
$ cat /proc/$$/cgroup
13:name=cgroup-test:/cgroup-1
12:memory:/user.slice
11:perf_event:/
10:cpuset:/
9:devices:/user.slice
8:blkio:/user.slice
7:pids:/user.slice/user-1000.slice/session-8.scope
6:rdma:/
5:hugetlb:/
4:freezer:/
3:cpu,cpuacct:/user.slice
2:net_cls,net_prio:/
1:name=systemd:/user.slice/user-1000.slice/session-8.scope
0::/user.slice/user-1000.slice/session-8.scope
可以看到当前进程已经被添加到cgroup-test:/cgroup-1中了。
# 通过subsystem限制cgroup中进程的资源。
# 在上面创建hierarchy的时候,这个hierarchy并没有关联到任何的subsystem,所以没有办法通过那个hierarchy中的cgroup节点限制进程的资源占用,其实系统已经为每一个subsystem创建了一个默认的hierarchy,比如memory的hierarchy。
$ mount|grep memory
$ cd /sys/fs/cgroup/memory
# 在不做限制的情况下,启动一个占用内存的stress进程
$ stress --vm-bytes 200m --vm-keep -m 1
$ mkdir test-limit-memory
$ cd test-limit-memory
# 设置最大的内存占用为100M
$ echo 100m > memory.limit_in_bytes
# 把当前的进程移动到这个cgroup中
$ echo $$ > tasks
$ stress --vm-bytes 200m --vm-keep -m 1
docker 使用cgroup,(以内存为例子)
$ docker run -itd -m 100m docker-proxy.repo.inspur.com/busybox
$ cd /sys/fs/cgroup/memory/docker/<container ID>/
$ cat memory.limit_in_bytes
104857600
静态(镜像)
Mount Namespace
Mount Namespace 修改的,是容器进程对文件系统“挂载点”的认知。在 Linux 操作系统里,有一个名为 chroot 的命令可以帮助你在 shell 中方便地完成这个工作。顾名思义,它的作用就是帮你“change root file system”,即改变进程的根目录到你指定的位置。Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace。
为了能够让容器的这个根目录看起来更“真实”,我们一般会在这个容器的根目录下挂载一个完整操作系统的文件系统,比如 Ubuntu16.04 的 ISO。这样,在容器启动之后,我们在容器里通过执行 “ls /” 查看根目录下的内容,就是 Ubuntu 16.04 的所有目录和文件。
挂载在容器根目录上,用来为容器进程提供隔离后执行环境的文件系统就是 容器镜像。
rootfs
rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。
同一台机器上的所有容器,都共享宿主机操作系统的内核。这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身
正是由于 rootfs 的存在,容器才有了一个被反复宣传至今的重要特性:一致性。
由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。
但是,如果修改了某个文件需要重新制作一次rootfs。新旧rootfs之间就没有任何关系了。不过既然所有的修改都基于一个旧的rootfs,我们能不能用增量的方式去做这些修改?正是因为这个,Docker镜像在设计的时候引用了层(layer)的概念。用到了一种叫做联合文件系统(Union FIle system 也叫UnionFS)的技术。
Union File System
Union File System最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。比如,我现在有两个目录 A 和 B,它们分别有两个文件:
$ tree
.
├── A
│ ├── a
│ └── x
└── B
├── b
└── x
然后,我使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上:
$ mkdir C
$ mount -t aufs -o dirs=./A:./B none ./C
再查看目录 C 的内容,就能看到目录 A 和 B 下的文件被合并到了一起:
$ tree ./C
./C
├── a
├── b
└── x
对于一个镜像。它最关键的目录结构在/var/lib/docker/
一个容器的rootfs可以分为三部分:
第一部分,只读层:
这一部分完全继承自镜像
第二部分,可读写层。
它是容器的最上面一层,在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,你修改产生的内容就会以增量的方式出现在这个层中。如果是删除,可读写层里会创建一个 whiteout 文件,把只读层里的文件“遮挡”起来。
第三部分,Init 层。
它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。
最终。这三部分会联合挂载到一个目录下,表现为一个完成的镜像供容器使用
使用dockerfile build镜像时,为了控制镜像的大小,删除文件的命令应该和增加这个文件的命令在同一条语句。
如何找到镜像对应的哪几层?
$ docker inspect <image id>
...
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:195be5f8be1df6709dafbba7ce48f2eee785ab7775b88e0c115d8205407265c5",
"sha256:0faad07fc16f96bdb8e433cc55068528472eb3756cc36b7e0134e3581f2babf0",
"sha256:876b5eb1778f3084b020cf291c2d1e241c964092b0de8044305aba1f620202e5"
]
},
...
$ cat /var/lib/docker/image/overlay2/layerdb/sha256/<layers-id>/cache-id
容器网络
在上面的介绍中,通过Namespace和Cgroup技术实现了容器进程的隔离,并且通过联合文件挂载系统让容器拥有自己的文件系统,容器中的进程所感受到的环境就像在一台虚拟机上一样,但是,这台虚拟机还没有插上网线。下面就给这个虚拟机插上网线。
前面介绍了Network Namespace,可以给容器分配独立的网络空间,那么怎么给这个网络空间增加网络配置呢。
Linux 虚拟网络设备
Linux Veth
Veth是成对出现的网络虚拟设备,发送到Veth一端虚拟设备的请求会送另一端的虚拟设备中发出。在容器化的虚拟场景中,经常会使用Veth连接不同网络的namespace。
# 创建两个网络namespace
$ ip netns add ns1
$ ip netns add ns2
#查看创建的网络namespace
$ ip netns ls
$ ls /var/run/netns/ns
# 创建一对Veth
$ ip link add veth0 type veth peer name veth1
# 分别将两个Veth移动到两个namespace中
$ ip link set veth0 netns ns1
$ ip link set veth1 netns ns2
# 去ns1中查看网络设备
$ ip netns exec ns1 ip link
# 配置每个veth的网络地址和namepace路由
$ ip netns exec ns1 ifconfig veth0 172.18.0.2/24 up
$ ip netns exec ns2 ifconfig veth1 172.18.0.3/24 up
# dev是设备 区别于gw 是网关
$ ip netns exec ns1 route add default dev veth0
$ ip netns exec ns2 route add default dev veth1
$ ip netns exec ns2 ip addr
Linux Bridge
Bridge虚拟设备是用来连接桥接的网络设备,它相当于交换机,可以连接不同的网络设备,当请求到达Bridge设备时,可以通过报文中的Mac地址进行广播或转发。
# 创建veth设备并将一段移入namespace
$ ip netns add ns3
$ ip link add veth2 type veth peer name veth3
$ ip link set veth3 netns ns3
# 创建网桥
$ brctl addbr br0
# 挂载网络设备
$ brctl addif br0 veth2
linux 路由表
路由表是Linux内核的一个模块,通过定义路由表来决定某个网络Namespace中包的流向,从而定义请求会到哪个网络设备上。
# 启动虚拟网络设备,并设置它在Net Namespace中的IP地址。
$ ip link set veth2 up
$ ip link set br0 up
$ ip netns exec ns3 ifconfig veth3 172.18.0.4/24 up
# 分别设置ns1网络空间的路由和宿主机的路由
# default代表0.0.0.0/0 即在Net Namespace 中所有流量都经过veth1的网络设备流出
$ ip netns exec ns3 route add default dev veth3
# 在宿主机上将172.18.0.0/24的网段请求路由到br0网桥
$ route add -net 172.18.0.0/24 dev br0
# 查看宿主机的IP地址
$ ifconfig ens33
# 从Namespace中访问宿主机
$ ip netns exec ns3 ping -c 1 192.168.141.146
$ ping -c 172.18.0.4
Linux iptables
iptables 是对Linux内核的netfilter模块进行操作和展示的工具,用来管理包的流动和传送。iptables定义了一套链式处理的结构,在网络包传输的各个阶段可以使用不同的策略对包进行加工、传送或丢弃。在容器虚拟化的技术中,经常会用到两种策略MASQUERADE和DNAT
MASQUERADE
iptables中的MASQUERADE策略可以将请求包中的源地址转换成一个网络设备的地址。上边介绍的那个Namespace中网络设备的地址是172.18.0.4.这个地址虽然在宿主机上可以路由到br0网桥,倒是到达宿主机外部之后,是不知道如何路由到这个IP地址的,如果请求外部的地址的话需要先通过MASQUERADE策略将这个IP转化成宿主机出口网卡的IP
# 打开ip转发
$ sysctl -w net.ipv4.conf.all.forwarding=1
# 对Namespace中发出的包添加网络地址转换
# 在名为POSTROUTING的nat表梿尾添加策略为MASQUERADE的匹配,将来自172.18.0.0/24的包通过 ens33 网卡发出
$ iptables -t nat -A POSTROUTING -s 172.18.0.0/24 -o ens33 -j MASQUERADE
在Namespace中请求宿主机外部地址时,将Namespace中的源地址转换成宿主机的地址作为源地址,既可以在Namespace中访问宿主机外的网络了。
DNAT
DNAT策略也是做网络地址的转换,不过他是要更换不妙地址,经常用于将内部网络地址的端口映射到外部去。
# 将宿主机上的80端口的请求转发到Namespace的IP上
$ iptables -t nat -A PREROUTING -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.18.0.4:80
iptables -nvL -t nat
Docker的架构图:
C/S之间可以通过三种方式连接:
- unix:///val/run/docker.sock
- tcp://host:port
- fd://socketfd
Docker 常用命令
# 启动容器
# -i 显示 console
# -t 交互
# --rm 容器退出后删除容器
# container name 可以自定义
# image 可以是image name 也可以是image id
# cmd 是容器运行的命令,跟在entrypoint之后。(dockerfile 时详细解释)
$ docker run -it --rm -n <container name> <image> <CMD>
# -d 后台运行
# --restart=always,容器异常退出时,重启容器,比如dockerd重启之后。
# -p 端口映射 宿主机端口:容器内端口
# -v 挂载volume 宿主机目录:容器内目录。必须时绝对路径,当前目录可以用$(pwd)
# --cpu 设置cpu最大限制
# -m 设置内存最大限制。
$ docker run -d --restart=always -p 8080:8080 -v $(pwd):/root/ --cpu=1 -m 10m -n <container name> <image>
# 停止容器
# container可以是 container name,也可以是container ID
$ docker stop <container>
# 查看容器日志
# -f 可以实时显示
# --tail=10 只显示最后10行
$ docker logs --tail=10 <container> -f
# 进入容器
$ docker exec -it <container> /bin/sh
# 删除容器
# -f 参数可以强制停止正在运行中的容器并删除
$ docker rm -f <container>
#镜像重命名
$ docker tag <source image name/id> <target image name>
# 构建镜像
# -t 可以指定都建出来的镜像的name及tag
$ docker build <Dockerfile path> -t <image name>
# 查看容器/镜像的详细信息
$ docker inspect <container or image>
# 查看docker server的详细信息
$ docker info
# 查看docker 占用的磁盘情况
# -v 列出详细信息
$ docker system df -v
# 删除没用的数据,包括镜像、容器。
$ docker system prune
# 从容器内外复制文件
$ docker cp CONTAINER:SRC_PATH DEST_PATH
$ docker cp SRC_PATH CONTAINER:DEST_PATH
# 列出所有 image
$ docker images
# 列出所有容器
# -a 显示所有容器
$ docker ps -a
# 列出容器资源占用情况
# container 可选
$ docker stats <container>
Dockerfile
FROM:指定基础镜像
第一条指令。scratch是虚拟的镜像,表示一个空白的镜像。
LABEL: 添加元数据
LABEL <key>=<value> <key>=<value> <key>=<value> ...
LABEL multi.label1="value1" \
multi.label2="value2" \
other="value3"
一般需要在LABEL中至少添加 Name和Version两条元数据
RUN:执行命令
shell 格式: RUN <命令> ,RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
exec 格式: RUN ["可执行文件", "参数1", "参数2"] 。run可以写多个,每一个指令都会建立一层,所以正确写法应该是↓
RUN buildDeps='gcc libc6-dev make' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.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
COPY:复制文本
COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]
<源路径> 可以是多个、以及使用通配符,通配符规则满足Go的filepath.Match 规则,如:COPY hom* /mydir/ COPY hom?.txt /mydir/
<目标路径>使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。
ADD:高级复制文件
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
<源路径> 可以是一个 URL ,如果是tgz的文件可以自动解压。下载后的文件权限自动设置为 600 。
CMD:容器启动命令
shell 格式: CMD <命令>
exec 格式: CMD ["可执行文件", "参数1", "参数2"...]
CMD ["nginx", "-g", "daemon off;"]
ENTRYPOINT:入口点
同CMD,指定容器启动程序及参数。
通过–entrypoint 参数在运行时替换。
用例一:使用CMD要在运行时重新写命令才能追加运行参数,ENTRYPOINT则可以运行时接受新参数。
示例:
FROM ubuntu:16.04RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]
追加-i参数
$ docker run myip -i
......
当前 IP:61.148.226.66 来自:北京市 联通
ENV:设置环境变量
在其他指令中可以直接引用ENV设置的环境变量。
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
示例:
ENV VERSION=1.0 DEBUG=on NAME="Happy Feet"
ARG:构建参数
与ENV不同的是,容器运行时不会存在这些环境变量。可以用 docker build --build-arg <参数名>=<值> 来覆盖。
VOLUME:定义匿名卷
容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
volume和run -v参数的区别。
VOLUME指令只是起到了声明了容器中的目录作为匿名卷,但是并没有将匿名卷绑定到宿主机指定目录的功能。但是当我们生成镜像的Dockerfile中以Volume声明了匿名卷,并且我们以这个镜像run了一个容器的时候,docker会在安装目录下的指定目录下面生成一个目录来绑定容器的匿名卷。
EXPOSE:暴露端口
EXPOSE <端口1> [<端口2>...]
EXPOSE :EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
WORKDIR:指定工作目录
WORKDIR <工作目录路径>
RUN cd /app
RUN echo "hello" > world.txt
两次run不在一个环境内,可以使用WORKDIR。
USER:指定当前用户
这个用户必须是事先建立好的,否则无法切换。
USER <用户名>
最佳实践:
http://dockone.io/article/9658
Dockerfile 扫描
-
hadolint是目前为止成熟最高的一个工具,一共支持60多条的自带规则。而且hadolint还支持对Run命令中带的shell命令进行审计。这一点功能是目前其他竞品不具备的。不过hadolint使用Haskell语言编写,且目前不支持规则文件导入。
如果有新的规则需要编程实现。 -
dockerlint 使用coffee_script语言编写,自带规则数量小于hadolint;也不支持规则文件导入功能。
-
dockerfile_lint 红帽发布,使用node.js编写的一个dockerfile扫描工具。支持的规则数量小于hadolint,但是支持通过yaml文件方式导入用户自定义规则。
dockerfile_lint 工具的使用
dockerfile_lint代码在github.com/projectatomic/dockerfile_lint下,有两种方法执行dockerfile_lint:
- 从dockerhub官方下载dockerfile_lint latest的docker镜像。
- 在nodejs执行环境上,运行dockerfile_lint扫描工具。
docker run -it --rm -v $(pwd):/target/ docker-proxy.repo.inspur.com/projectatomic/dockerfile-lint:latest /bin/bash
[root@4298ab08644f /]#dockerfile_lint -f /target/Dockerfile -v
dockerfile_lint 支持下面参数
-h 打印help信息
-j 结果以jason格式输出
-r rulefile 指定一个规则文件。
-v 打印debug新秀
-f 指定dockerfile的路径
-e 打印出目前的规则
dockerfile_lint 规则
目前为止dockerfile_lint默认情况下带了default_rules.yaml和base_rules.yaml。如果不指定任何rulefile,那么系统默认采用这两个规则文件,在docker镜像的/opt/dockerfile_lint/config/目录下。此外在/opt/dockerfile_lint/sample_rules下面还带了很多事例rule。可以直接在容器内部修改default_rules.yaml和base_rules.yaml文件,然后再运行dockerfile_lint。
可以看到dockerfile_lint的规则基本上都是基于正则匹配的,所以用户可以很方便的扩展新的规则。
需要注意的是dockerfile_lint语法中required_instruction域中的count字段在19年12月截止的版本中是无效的。
dive
https://github.com/wagoodman/dive
dive <image>
Docker daemon的常用配置
{
"dns": ["10.100.1.12"], # 设定容器DNS的地址,在容器的 /etc/resolv.conf文件中可查看。
"insecure-registries": [],#配置docker的私库地址,配置之后可以解决https免证书问题。
"registry-mirrors":["xxxx"], #镜像加速的地址,增加后在 docker info中可查看。
"graph": "/mnt/ssd/0/docker", #docker 文件存放地址
}