# 创建容器<a name="ZH-CN_TOPIC_0184808226"></a>

## 下载镜像<a name="zh-cn_topic_0182302200_section820441134510"></a>

运行docker命令需要root权限，当你使用普通用户登录时，需要用sudo权限执行docker命令。

```
[root@localhost ~]# docker pull ubuntu
```

该命令行将在docker官方的镜像库中下载ubunt:latest（命令行中没指定TAG，所以使用默认的TAG名latest），镜像在下载过程中将检测所依赖的层本地是否存在，如果存在就跳过。从私有镜像库下载镜像时，请带上registry描述，例如：假如建立了一个私有镜像库，地址为192.168.1.110:5000，里面有一些常用镜像。使用下面命令行从私有镜像库中下载镜像。

```
[root@localhost ~]# docker pull 192.168.1.110:5000/ubuntu
```

从私有镜像库中下载下来的image名字带有镜像库地址的信息名字比较长，可以用docker tag命令生成一个名字简单点的image。

```
[root@localhost ~]# docker tag 192.168.1.110:5000/ubuntu ubuntu
```

可以通过docker images命令查看本地镜像列表。

## 运行一个简单的应用<a name="zh-cn_topic_0182302200_section9658816124511"></a>

```
[root@localhost ~]# docker run ubuntu /bin/echo "Hello world"
Hello world
```

该命令行使用ubuntu:latest（命令行中没有指定tag，所以使用默认的tag名latest）镜像创建了一个容器，在容器内执行了echo "Hello world"。使用下面命令行可以查看刚才创建的这个容器。

```
[root@localhost ~]# docker ps -l
CONTAINER ID        IMAGE               COMMAND                   CREATED             STATUS                     PORTS               NAMES
d8c0a3315bc0        ubuntu              "/bin/echo 'Hello wo…"   5 seconds ago       Exited (0) 3 seconds ago                       practical_franklin
```

## 创建一个交互式的容器<a name="zh-cn_topic_0182302200_section148331720164510"></a>

```
[root@localhost ~]# docker run -it ubuntu /bin/bash
root@bf22919af2cf:/# ls 
bin  boot  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var 
root@bf22919af2cf:/# pwd 
/
```

-ti选项分配一个伪终端给容器并可以使用STDIN进行交互，可以看到这时可以在容器内执行一些命令。这时的容器看起来完全是一个独立的linux虚拟机。使用exit命令退出容器。

## 后台运行容器<a name="zh-cn_topic_0182302200_section208036271451"></a>

执行下面命令行，-d指示这个容器在后台运行，--name=container1 指定容器的名字为container1。

```
[root@localhost ~]# docker run -d --name=container1 ubuntu /bin/sh -c "while true;do echo hello world;sleep 1;done"
7804d3e16d69b41aac5f9bf20d5f263e2da081b1de50044105b1e3f536b6db1c
```

命令行的执行结果是返回了这个容器的ID，没有返回命令的执行结果hello world，此时容器在后台运行，可以用docker ps命令查看正在运行的容器:

```
[root@localhost ~]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
7804d3e16d69        ubuntu              "/bin/sh -c 'while tr"   11 seconds ago      Up 10 seconds                           container1
```

用docker logs查看容器运行的输出：

```
[root@localhost ~]# docker logs container1
hello world
hello world
hello world
...
```

## 容器网络连接<a name="zh-cn_topic_0182302200_section10769033124516"></a>

默认情况下，容器可以访问外部网络，而外部网络访问容器时需要通过端口映射，下面以在docker中运行私有镜像库服务registry为例。下面的命令行中-P使registry镜像中开放的端口暴露给主机。

```
[root@localhost ~]# docker run --name=container_registry -d -P registry 
cb883f6216c2b08a8c439b3957fb396c847a99079448ca741cc90724de4e4731 
```

container\_registry这个容器已经启动了，但是并不知道容器中的服务映射到主机的哪个端口，通过docker port查看端口映射。

```
[root@localhost ~]# docker port container_registry 
5000/tcp -> 0.0.0.0:49155 
```

从输出可以看出，容器内的5000端口映射到了主机的49155端口。通过主机IP:49155就可以访问registry服务了，在浏览器中输入http://localhost:49155就可以返回registry的版本信息。

在运行registry镜像的时候还可以直接指定端口映射如：

```
docker run --name=container_registry -d -p 5000:5000 registry 
```

通过-p 5000:5000指定容器的5000端口映射到主机的5000端口。

## 注意事项<a name="zh-cn_topic_0182302200_section2261716204719"></a>

-   **启动容器不能单独加-a stdin**

    启动容器时，不能单独加-a stdin，必须要同时加上-a stdout或者-a stderr，否则会导致终端即使在容器退出后也会卡住。


-   **避免使用已有容器的长id、短id作为新容器的name**

    创建容器时，避免使用已有容器A的长id或短id作为新容器B的name。若使用容器A的长id作为容器B的name，当使用容器B的name作为指定容器进行操作时，docker匹配到的是容器A。若使用容器A的短id作为容器B的name，当使用容器A的短id作为指定容器进行相关操作时，docker匹配到的是容器B。这是因为，docker在匹配容器时，先精确匹配所有容器的长id。若未匹配成功，再根据container\_name进行精确匹配；若还未匹配成功，直接对容器id进行模糊匹配。

-   **使用sh/bash等依赖标准输入输出的容器应该使用\`-ti\`参数，避免出现异常**

    正常情况：不用\`-ti\`参数启动sh/bash等进程容器，容器会马上退出。

    出现这种问题的原因在于，docker会先创建一个匹配用于容器内业务的stdin，在不设置-ti等交互式参数时，docker会在容器启动后关闭该pipe，而业务容器进程sh/bash在检测到stdin被关闭后会直接退出。

    异常情况：如果在上述过程中的特定阶段（关闭该pipe之前）强制杀死docker daemon，会导致该pipe的daemon端没有被及时关闭，这样即使不带\`-ti\`的sh/bash进程也不会退出，导致异常场景，这种容器就需要手动清理。

    Daemon重启后会接管原有的容器stream，而不带\`-ti\`参数的容器可能就无法处理（因为正常情况下这些容器不存在stream需要接管）；真实业务下几乎不存在这种使用方式\(不带 \`-ti\`的sh/bash没有任何作用\)，为了避免这类问题发生，限制交互类容器应该使用 \`-ti\`参数。

-   **容器存储卷**

    启动容器时如果通过\`-v\`参数将主机上的文件挂载到容器中，在主机或容器中使用vi或sed命令修改文件可能会使文件inode发生改变，从而导致主机和容器内的文件不同步。容器中挂载文件时应该尽量避免使用这种文件挂载的方式（或不与vi和sed同时使用），也可以通过挂载文件上层目录来避免该问题。在docker挂载卷时“nocopy”选项可以避免将容器内挂载点目录下原有的文件拷贝到主机源目录下，但是这个选项只能在挂载匿名卷时使用，不能在bind mount的场景下使用。

-   **避免使用可能会对host造成影响的选项**

    --privileged 选项会让容器获得所有权限，容器可以做挂载操作和修改/proc、/sys等目录，可能会对host造成影响，普通容器需要避免使用该选项。

    共享host的namespace，比如--pid host/--ipc host/--net host等选项可以让容器跟host共享命名空间，同样会导致容器影响host的结果，需要避免使用。

-   **kernel memory cgroup不稳定，禁止使用**

    kernel memory cgroup在小于4.0版本的Linux内核上仍属于实验阶段，运行起来不稳定，虽然Docker的Warning说是小于4.0就可以，但是我们评估认为，kmemcg在高版本内核仍然不稳定，所以不管是低版本还是高版本，均禁止使用。

    当docker run --kernel-memory时，会产生如下告警：

    ```
    WARNING: You specified a kernel memory limit on a kernel older than 4.0. Kernel memory limits are experimental on older kernels, it won't work as expected as expected and can cause your system to be unstable.
    ```

-   **blkio-weight参数在支持blkio精确控制的内核下不可用**

    --blkio-weight-device 可以实现容器内更为精确的blkio控制，该控制需要指定磁盘设备，可以通过docker --blkio-weight-device参数实现。同时在这种内核下docker不再提供--blkio-weight方式限制容器blkio，使用该参数创建容器将会报错:

    ```
    docker: Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "process_linux.go:398: container init caused \"process_linux.go:369: setting cgroup config for ready process caused \\\"blkio.weight not supported, use weight_device instead\\\"\""
    ```

-   **使用--blkio-weight-device需要磁盘支持CFQ调度策略**

    --blkio-weight-device参数需要磁盘工作于完全公平队列调度（CFQ：Completely Fair Queuing）的策略时才能工作。

    通过查看磁盘scheduler文件（/sys/block/<磁盘\>/queue/scheduler）可以获知磁盘支持的策略以及当前所采用的策略，如查看sda：

    ```
    # cat /sys/block/sda/queue/scheduler noop [deadline] cfq 
    ```

    当前sda支持三种调度策略：noop, deadline, cfq，并且正在使用deadline策略。通过echo修改策略为cfq：

    ```
    # echo cfq > /sys/block/sda/queue/scheduler
    ```


-   **容器基础镜像中systemd使用限制**

    通过基础镜像创建的容器在使用过程中，容器基础镜像中的systemd仅用于系统容器，普通容器不支持使用。


## 并发性能<a name="zh-cn_topic_0182302200_section4124524171619"></a>

-   docker内部的消息缓冲有一个上限，超过这个上限就会将消息丢弃，因此在并发执行命令时建议不要超过1000条命令，否则有可能会造成docker内部消息丢失，从而造成容器无法启动等严重问题。
-   并发创建容器并对容器执行restart时会偶现“oci runtime error: container init still running”报错，这是因为containerd对事件等待队列进行了性能优化，容器stop过程中执行runc delete，尝试在1s内kill掉容器的init进程，如果1s内init进程还没有被kill掉的话runc会返回该错误。由于containerd的GC（垃圾回收机制）每隔10s会回收之前runc delete的残留资源， 所以并不影响下次对容器的操作，一般出现上述报错的话等待4\~5s之后再次启动容器即可。

## 安全特性解读<a name="zh-cn_topic_0182302200_section85671246468"></a>

1.  docker默认的权能配置分析

    原生的docker默认配置如下，默认进程携带的Cap如下:

    ```
    "CAP_CHOWN", 
    "CAP_DAC_OVERRIDE", 
    "CAP_FSETID", 
    "CAP_FOWNER", 
    "CAP_MKNOD", 
    "CAP_NET_RAW", 
    "CAP_SETGID", 
    "CAP_SETUID", 
    "CAP_SETFCAP", 
    "CAP_SETPCAP", 
    "CAP_NET_BIND_SERVICE", 
    "CAP_SYS_CHROOT", 
    "CAP_KILL", 
    "CAP_AUDIT_WRITE",
    ```

    默认的seccomp配置是白名单，不在白名单的syscall默认会返回SCMP\_ACT\_ERRNO，根据给docker不同的Cap开放不同的系统调用，不在上面的权限，默认docker都不会给到容器。

2.  CAP\_SYS\_MODULE

    CAP\_SYS\_MODULE这个Cap是让容器可以插入ko，增加该Cap可以让容器逃逸，甚至破坏内核。因为容器最大的隔离是Namespace，在ko中只要把他的Namespace指向init\_nsproxy即可。

3.  CAP\_SYS\_ADMIN

    sys\_admin权限给容器带来的能力有：

    -   文件系统（mount，umount，quotactl）
    -   namespace设置相关的（setns，unshare，clone new namespace）
    -   driver ioctl
    -   对pci的控制，pciconfig\_read, pciconfig\_write, pciconfig\_iobase
    -   sethostname

4.  CAP\_NET\_ADMIN

    容器中有访问网络接口的和sniff网络流量的权限，容器可以获取到所有容器包括host的网络流量，对网络隔离破坏极大。

5.  CAP\_DAC\_READ\_SEARCH

    该权限开放了，两个系统调用open\_by\_handle\_at，name\_to\_handle\_at，如果host上没有selinux保护，容器中可通过暴力搜索file\_handle结构的inode号，进而可以打开host上的任意文件，影响文件系统的隔离性。

6.  CAP\_SYS\_RAWIO

    容器中可对host写入io端口，可造成host内核崩溃。

7.  CAP\_SYS\_PTRACE

    容器中有ptrace权限，可对容器的进程进行ptrace调试。现runc已经修补该漏洞，但有些工具比如nsenter和docker-enter并没有改保护，容器中可对这些工具执行的进程进行调试，获取这些工具带入的资源信息（Namespace、fd等），另外， ptrace可以绕过seccomp，极大增加内核攻击面。

8.  Docker Cap接口 --cap-add all

    --cap-add all表示赋予容器所有的权能，包括本节提到的比较危险的权能，使得容器可以逃逸。

9.  不要禁用docker的seccomp特性

    默认的docker有一个seccomp的配置，配置中使用的是白名单，不在配置的sys\_call会被seccomp禁掉，使用接口--security-opt 'seccomp:unconfined'可以禁止使用seccomp特性。如果禁用seccomp或使用自定义seccomp配置但过滤名单不全，都会增加容器对内核的攻击面。

10. 不要配置/sys和/proc目录可写

    /sys和/proc目录包含了linux维护内核参数、设备管理的接口，容器中配置该目录可写可能会导致容器逃逸。

11. Docker开放Cap  --CAP\_AUDIT\_CONTROL

    容器可以通过控制系统audit系统，并且通过AUDIT\_TTY\_GET/AUDIT\_TTY\_SET等命令可以获取审计系统中记录的tty执行输入记录，包括root密码。

12. CAP\_BLOCK\_SUSPEND和CAP\_WAKE\_ALARM

    容器可拥有阻塞系统挂起\(epoll\)的能力。

13. CAP\_IPC\_LOCK

    容器拥有该权限后，可以突破ulimit中的max locked memory限制，任意mlock超大内存块，造成一定意义的DoS攻击。

14. CAP\_SYS\_LOG

    容器拥有该权限后，可以dmesg读取系统内核日志，突破内核kaslr防护。

15. CAP\_SYS\_NICE

    容器拥有该权限后，可以改变进程的调度策略和优先级，造成一定意义的DoS攻击。

16. CAP\_SYS\_RESOURCE

    容器可以绕过对其的一些资源限制，比如磁盘空间资源限制、keymaps数量限制、pipe-size-max限制等，造成一定意义的DoS攻击。

17. CAP\_SYS\_TIME

    容器可以改变host上的时间。

18. Docker默认Cap风险分析

    Docker默认的Cap，包含了CAP\_SETUID和CAP\_FSETID，如host和容器共享目录，容器可对共享目录的二进制文件进行+s设置，host上的普通用户可使用其进行提权CAP\_AUDIT\_WRITE，容器可以对host写入，容器可以对host写入日志，host需配置日志防爆措施。

19. Docker和host共享namespace参数，比如--pid，--ipc, --uts

    该参数为容器和host共享namespace空间，容器和host的namespace隔离没有了，容器可对host进行攻击。比如，使用--pid 和host共享pid namespace，容器中可以看到host上的进程pid号，可以随意杀死host的进程。

20. --device 把host的敏感目录或者设备，映射到容器中

    Docker管理面有接口可以把host上的目录或者设备映射到容器中，比如--device，-v等参数，不要把host上的敏感目录或者设备映射到容器中。


