介绍

  • 我需要盖一个房子,于是我搬石头、砍木头、画图纸、盖房子。一顿操作,终于把这个房子盖好了。
  • 结果,住了一段时间,心血来潮想搬到海边去。这时候按以往的办法,我只能去海边,再次搬石头、砍木头、画图纸、盖房子。
  • 烦恼之际,跑来一个魔法师教会我一种魔法。这种魔法可以把我盖好的房子复制一份,做成「镜像」,放在我的背包里。 等我到了海边,就用这个「镜像」,复制一套房子,拎包入住。
  • 对应到我们的项目中来,房子就是项目本身,镜像就是项目的复制,背包就是镜像仓库。如果要动态扩容,从仓库中取出项目镜像,随便复制就可以了。
  • Build once,Run anywhere! 不用再关注版本、兼容、部署等问题,彻底解决了「上线即崩,无休止构建」的尴尬。

虚拟机与容器

虚拟机:虚拟化硬件

  • 虚拟机 Virtual Machine 指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。在实体计算机中能够完成的工作在虚拟机中都能够实现。

  • 虚拟机(VM)是共享一个服务器的物理资源的操作系统。它是主机硬件上的Guest,因此也被称为Guest虚拟机。

  • 虚拟机由几层组成。支持虚拟化的层是hypervisorhypervisor是一种虚拟化服务器的软件。

  • 运行应用程序所需的一切(虚拟化的硬件,操作系统以及任何所需的二进制文件和库)都包含在虚拟机里。因此,虚拟机具有自己独立的基础架构。

虚拟机的优势

  • 虚拟机可减少在服务器设备上的支出,可以利用一个物理服务器资源切分成多个独立的虚拟机来完成许多工作。

  • 由于只有一台主机,因此可以利用虚拟机管理程序的集中功能高效地管理所有虚拟环境。这些系统完全相互独立,这意味着你可以在不同的虚拟机里安装不同的系统环境。

  • 最重要的是,虚拟机与主机操作系统隔离,是进行实验和开发应用程序的安全场所。

虚拟机的劣势

  • 虚拟机可能占用主机的大量系统资源,虚拟机的大小为数GB。在虚拟服务器上运行单个应用程序意味着还要运行Guest OS以及Guest OS运行所需的所有硬件的虚拟副本。这样很快就增加了很多RAMCPU资源消耗。

  • 迁移虚拟机上运行的应用程序的过程也可能很复杂,因为它始终附加在操作系统上。因此,必须同时迁移应用程序和操作系统。同样,在创建虚拟机时,系统管理程序会分配专用于VM的硬件资源。不过与运行单独的实体服务器相比,这仍然是经济的。

容器:软件单元

  • 容器是一个不依赖于操作系统,运行应用程序的环境。它通过Linux的NamespacesCgroups技术对应用程序进程进行隔离和限制的,Namespace的作用是隔离,它让应用进程只能看到该Namespace内的世界;而Cgroups 的作用是限制分配给进程的宿主机资源。但对于宿主机来说,这些被“隔离”了的进程跟其他进程并没有太大区别。
  • 容器只是运行在宿主机上的一种特殊的进程,多个容器之间使用的还是同一个宿主机的操作系统内核。

容器是怎么工作的

  • Namespace的作用是隔离,它让应用进程只能看到该Namespace内的世界

  • Cgroups的作用是限制,它给这个世界围上了一圈看不见的墙。

  • 通过Mount Namespace可以修改容器进程对自己的文件系统 **”挂载点”的认知。在容器进程启动之前重新挂载它的整个根目录“/“**(通过pivot_root系统调用改变进程的文件系统,如果系统不支持,则使用chroot),而由于Mount Namespace的存在,这个挂载对宿主机不可见的。

  • 这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫做:rootfs(根文件系统)。rootfs只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。

  • 所以说,rootfs 只包括了操作系统的 “躯壳”,并没有包括操作系统的内核。同一台机器上的所有容器,都会共享宿主机操作系统的内核。这就意味着,如果容器里的应用程序需要配置内核参数、跟内核进行直接的交互,这些都是操作的宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。

  • 这也是容器相比于虚拟机的主要缺陷之一:毕竟虚拟机有模拟出来的硬件机器充当沙盒,而且每个虚拟机里还运行着一个完整Guest OS让应用随便折腾。

  • 不过由于rootfs里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。这就赋予了容器所谓的一致性:无论在本地、云端,还是在一台任何地方的机器上,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就能被重现出来。

  • 出于完整的隔离和安全性考虑 ,虚拟机通常用于要求苛刻的应用程序,网络基础结构以及能消耗VM大部分资源的应用程序。而容器通常用于Web应用,微服务。

容器的优势

  • 容器占用的大小比虚拟机小很多,甚至可以小到10MB,可以轻松限制容器的内存和CPU使用率。与部署应用需要部署整个操作系统的虚拟机相比,容器非常轻巧且启动迅速。这样让我们可以快速扩展容器并添加相同的容器。

  • 同样,容器对于持续集成和持续部署(CI / CD)实施也是极好的选择。他们通过在开发人员之间分发和合并镜像来促进协作开发。

容器的劣势

  • 容器仍无法提供与虚拟机相同的安全性和稳定性。由于它们共享主机的内核,因此不能像虚拟机一样完全隔离。

  • 容器是进程级的隔离,一个容器可以通过影响宿主机内核的稳定性来影响其他容器。

  • 一旦容器执行了任务,它就会关闭并删除其中的所有数据。如果希望数据保留下来,则必须使用”数据卷”进行保存,这需要在主机上进行手动配置。

核心概念

Docker镜像

  • Docker镜像类似于虚拟机镜像,可以将它理解为一个只读的模板
  • 例如,一个镜像可以包含一个基本的操作系统环境,里面仅安装了Apache应用程序(或用户需要的其他软件)。可以把它称为Apache镜像。

Docker容器

  • Docker容器类似于一个轻量级的沙箱,Docker利用容器来运行和隔离应用。容器是基于镜像创建的应用运行实例。可以对容器执行启动、开始、停止、删除操作,各个容器之间是彼此隔离的、互不可见的。

Docker仓库

  • Docker仓库类似于代码仓库,它是Docker集中存放镜像文件的场所。

  • Docker仓库可以分为公共仓库和私有仓库。Docker Hub是官方的、最大的公共仓库。

  • 从严格意义上来讲,应当区分仓库和仓库注册服务器(registry)。

    • 仓库注册服务器是存放仓库的服务器。一个服务器上会存放大量仓库。
    • 每个仓库存储某一类镜像,往往包含多个镜像文件,通过tag加以区分。例如存放Ubuntu操作系统的仓库称为Ubuntu仓库,其中包含不同版本的镜像。

安装Docker

配置Docker服务

  • 为了避免每次使用docker命令都要权限,可以将当前用户加入安装docker时自动创建的docker用户组。

    1
    sudo usermod -aG docker USER_NAME

win10专业版安装Docker

  • win10 开始内置了一个轻量级虚拟机,经过不断的优化,这个虚拟机实现了与 windows 的高度集成,实现了虚拟机的高性能运行,WSL2 便是运行在虚拟机上的一个完整的 linux 内核。

  • 因此WSL2给了在windows更接近原生linux的体验,同时wsl2 的开启速度有了非常明显的提升,几乎不需要再等待。

  • 在“控制面板\所有控制面板项\程序和功能”中选择“启用或者关闭Windows功能”,勾选选项: 打开系统虚拟机平台,适用于Linux 的 Windows 子系统.也可在powershell中使用命令方式打开

1
2
3
4
5
# 虚拟机平台
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

# 适用于Linux 的 Windows 子系统
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
  • 下载 wsl2 需要使用的 linux 内核, 在 https://docs.microsoft.com/zh-cn/windows/wsl/wsl2-kernel 页面点击下载 linux 内核更新包,下载完点击安装

  • 重启系统,并设置WSL 2 设置为默认版本

    1
    2
    3
    wsl --set-default-version 2
    # 查看是不是WSL2
    wsl -l -v
  • 打开 Microsoft Store,搜索 Terminal,安装 Windows Terminal,

  • 搜索 Ubuntu,选择安装。 第一次打开 Ubuntu 的时候,将打开一个控制台窗口,会等待几分钟来进行配置,启动完成后为 Ubuntu 创建一个用户和密码(**如果第一次启动ubuntu失败,可以重启windows10系统再次试下**)。

  • 由于默认情况下我们不知道 root 用户的密码,所以如果我们想要使用 root 用户的话可以使用 passwd 命令为 root 用户设置一个新的密码,同时为了避免sudo切换root是需要输入密码,把自己配置的用户名加到sudo免密中,命令如下:

    1
    2
    # 替换leap为自己单独配置的用户名
    sudo echo "leap ALL=(ALL:ALL) NOPASSWD: ALL" >>/etc/sudoers
  • 更换ubuntu的apt安装源, 阿里源修改方式, 并执行更新apt update && apt upgrade -y

原生linux安装docker方式

  • 因为wsl2已经完整使用了linux内核了,此种方式和先前在linux虚拟机安装docker类似,步骤如下:

    1
    2
    3
    $ curl -fsSL https://get.docker.com -o get-docker.sh
    $ sudo sh get-docker.sh
    $ sudo service docker start
  • 执行脚本安装过程中,脚本提示“建议使用Docker Desktop for windows”,20s内按Ctrl+C会退出安装,所以需要等待20s,另外此种方式需要访问外网。

  • 检查docker安装正常

    1
    2
    3
    4
    5
    6
    # 检查dockerd进程启动
    service docker status
    ps aux|grep docker
    # 检查拉取镜像等正常
    docker pull busybox
    docker images
  • 注意:不同于完全linux虚拟机方式,WLS2下通过apt install docker-ce命令安装的docker无法启动,因为WSL2方式的ubuntu里面没有systemd。

    • 上述官方get-docker.sh安装的docker,dockerd进程是用ubuntu传统的init方式而非systemd启动的。

Docker Desktop for windows方式

  • Docker 也专门开发了可以使用 WSL2 中的 Docker 守护进程的桌面管理程序, 打开 Docker Desktop WSL2 backend 页面,下载最新的 Docker Desktop for Windows 程序 ,建议下载stable版本。下载地址:https://www.docker.com/products/docker-desktop

  • 启动Docker Desktop for Windows,点击“设置”按钮,启用基于WSL2的引擎复选框(Use the WSL 2 based engine)

  • 这个时候在 WSL 里面执行 docker 命令还是找不到的, 在 Resources 的WSL Integration中设置要从哪个 WSL2 发行版中访问

  • 重启 Docker desktop for Windows,重启完成后我们就可以在 WSL2里面使用 docker 命令了

安装方式总结

  • WSL2下原生linux安装docker方式和完全linux虚拟机安装docker类似,区别在于WSL2下的linux不支持systemd。
  • Docker Desktop for windows方式,其实质是利用docker的C/S架构
    • 将windows模式下的docker对应docker.sock,docker客户端二进制和docker的数据目录挂载到WSL2里面的linux机器。
    • 在此linux机器下执行docker命令,实质为客户端通过 挂载的/var/run/docker.sock文件与windows里面的dockerd服务端进程通信。
  • 要使用哪个模式下的docker,重启下该模式下的docker服务端即可,本质都是修改/var/run/docker.sock文件。理论可以同时使用,但是需要修改docker配置,通过-H参数增加dockerd的tcp监听,执行具体docker命令时指定dockerd监听的对应IP和端口即可。

使用Docker镜像

获取镜像

  • 命令:

    1
    2
    3
    4
    5
    docker pull NAME[:TAG]

    # 命令选项:
    # 是否获取仓库中的所有镜像。默认为false
    -a or --all-tags = true|false
    • NAME 是镜像的名称;
    • TAG 是镜像的标签,常用于表示版本。如果不显示指定 TAG ,则默认为 latest
  • 例:

    1
    2
    docker pull ubuntu:14.04
    docker pull ubuntu
  • 下载过程中通过输出信息可以看出,镜像文件一般由若干层(layer)组成。类似 6c8sdf8908df 这样的串是层的唯一id。

  • 完整的id包括256 bit,由64个十六进制字符组成。使用 docker pull 命令下载时会获取并输出镜像的各层信息。当不同的镜像包含相同的层时,本地仅存储一份该层的文件,从而减小了存储空间需求。

  • 严格来讲,完整的镜像名称需要添加仓库地址前缀。比如上面的命令,完整版为:

    1
    docer pull registry.hub.docker.com/ubuntu:14.04
    • 当使用的是Docker Hub服务时,镜像仓库前缀可以省略。但使用其他仓库时,需要指定。

列出镜像

  • 命令:

    1
    docker images
  • 列出的信息有:

    • REPOSITORY :仓库名称,或者说镜像名称,
    • TAG :标签。注意标签只是一个标记,不能通过标签判断两个镜像是否内容相同。
    • IMAGE ID镜像的ID,是镜像的唯一标识。两个Tag不同的镜像,可能ID相同,说明它们指向同一个镜像。
    • CREATED :创建时间。
    • SIZE :镜像大小。注意这里的大小只是逻辑大小,并不代表该镜像实际占用的空间。因为Docker采用分层文件系统,因此实际大小通常会小于逻辑大小。
  • 该命令支持通过镜像名和tag来过滤镜像。当指定了镜像名时,只有镜像名与指定的镜像名完全匹配的镜像才会列出来。当同时指定了镜像名和tag时,只有两者都完全匹配的镜像才会列出来。

    • 注意,对于非docker hub的镜像,需要指定仓库前缀才能匹配。

  • 选项:参考链接:https://docs.docker.com/engine/reference/commandline/images/

    Name, shorthand Default Description
    --all , -a Show all images (default hides intermediate images)
    --digests Show digests
    --filter , -f Filter output based on conditions provided
    --format Pretty-print images using a Go template
    --no-trunc Don’t truncate output
    --quiet , -q Only show numeric IDs

创建镜像标签

  • 命令:

    1
    docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]
  • 功能:创建一个带tag的新镜像,它相当于一个“快捷方式”,或者说链接,指向原始镜像。

查看底层信息

1
docker inspect [OPTIONS] NAME|ID [NAME|ID...]

查看镜像历史

1
docker history [OPTIONS] IMAGE

选项:

Name, shorthand Default Description
--format 使用GO的模板来格式化输出
--human , -H true 以人类可读的方式显示时间和大小
--no-trunc 不要截断输出
--quiet , -q Only show numeric IDs

搜寻镜像

1
docker search [OPTIONS] TERM

选项:

Name, shorthand Default Description
--automated deprecated Only show automated builds
--filter , -f Filter output based on conditions provided
--format Pretty-print search using a Go template
--limit 25 Max number of search results
--no-trunc Don’t truncate output
--stars , -s deprecated Only displays with at least x stars

例:

1
docker search busybox

删除镜像

1
2
3
docker rmi [OPTIONS] IMAGE [IMAGE...]
# 一次性删除所有的镜像
docker rmi $(docker images -q)

选项:

Name, shorthand Default Description
--force , -f 强制删除。用于删除正在运行中的容器的镜像。
--no-prune Do not delete untagged parents

可以使用以下两种方式来指定删除的目标镜像:

  • 镜像名+Tag:
    • 当某个镜像文件关联多个Tag时,只会删除指定的tag,而不是删除镜像文件本身。
    • 如果某个镜像文件只有一个tag,那么删除该tag也会删除镜像本身。
  • 镜像ID
    • 会删除这个镜像文件及其关联的所有Tag。

创建镜像

基于已有镜像

  • 先启动一个容器,在容器中进行修改,然后将该容器保存为一个新的镜像。

  • 关键命令:

    1
    docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
  • 选项:

    Name, shorthand Default Description
    --author , -a Author (e.g., “John Hannibal Smith hannibal@a-team.com”)
    --change , -c Apply Dockerfile instruction to the created image
    --message , -m Commit message
    --pause , -p true Pause container during commit
  • 示例:

    1. 以交互模式启动基础镜像,创建一个容器:

      1
      docker run -it centos /bin/bash
    2. 在容器中执行修改命令,然后退出:

      1
      2
      yum install -y vim
      exit
    3. 查询上一步创建的容器ID

      1
      $ docker ps -a
    4. 使用 docker commit 命令将容器提交为一个新镜像:

      1
      docker commit -m "Add vim" -a "fulsun" 4e23efcc3044 mycentos:0.1
    5. 查看本地镜像:

      1
      2
      docker images
      docker history mycentos:0.1

基于本地模板导入

  • Export命令用于持久化容器(不是镜像)

    1
    2
    docker export <CONTAINER ID> > /home/export.tar
    docker import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]
  • 选项:

    Name, shorthand Default Description
    --change , -c Apply Dockerfile instruction to the created image
    --message , -m Set commit message for imported image
    --platform experimental (daemon)API 1.32+ Set platform if server is multi-platform capable

存出和载入镜像

存出镜像

  • 将一个或多个镜像保存为一个tar文件。默认情况下输出流输出到STDOUT。通常都需要指定-o参数,来输出到指定的文件中。否则需要使用输出重定向。

    1
    docker save [OPTIONS] IMAGE [IMAGE...]
  • 选项:

    Name, shorthand Default Description
    --output , -o Write to a file, instead of STDOUT

载入镜像

  • 从一个tar文件或STDIN载入镜像。通常都需要-i参数,来指定载入的目标文件。否则需要使用输入重定向。

    1
    docker load [OPTIONS]
  • 选项:

    Name, shorthand Default Description
    --input , -i Read from tar archive file, instead of STDIN
    --quiet , -q Suppress the load output

上传镜像

  • 命令

    1
    2
    3
    4
    5
    # 上传到docker hub
    docker push [OPTIONS] NAME[:TAG]

    # 上传到私有仓库
    docker push REGISTRY_HOST:[PORT]/NAME[:TAG]
  • 选项:

    Name, shorthand Default Description
    --disable-content-trust true Skip image signing
  • 例:上传到私有仓库:

    1
    2
    3
    4
    5
    # 为镜像创建一个新标签,使其带上私有仓库地址
    docker tag test:0.1 docker.neg/po/test:0.1

    # 推送
    docker push docker.neg/po/test:0.1

export和save的区别

  • 大小比较: export后的版本会比save的版本稍微小一些

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    $ docker export 4e23efcc3044 > ./export.tar
    $ docker save mycentos:0.1 > ./save.tar

    -rwxrwxrwx 1 sun sun 253M Feb 15 15:50 export.tar
    -rwxrwxrwx 1 sun sun 264M Feb 15 15:50 save.tar

    # 删除镜像和容器
    docker rm $(docker ps -q -a)
    docker rmi $(docker images -q)

    # 导入
    cat export.tar | docker import - mycentos:latest
    docker load < save.tar

    sun@DESKTOP-6CMI5TB:~/desktop$ docker images
    REPOSITORY TAG IMAGE ID CREATED SIZE
    mycentos latest 77d84c6582bb 32 seconds ago 256MB
    mycentos 0.1 2d78f7e58229 24 minutes ago 267MB
    • export导出后,会丢失历史和元数据。执行inspect命令就知道了:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ docker history mycentos
    IMAGE CREATED CREATED BY SIZE COMMENT
    77d84c6582bb 4 minutes ago 256MB Imported from -
    $ docker history mycentos:0.1
    IMAGE CREATED CREATED BY SIZE COMMENT
    2d78f7e58229 29 minutes ago /bin/bash 58.1MB Add vim
    <missing> 2 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
    <missing> 2 months ago /bin/sh -c #(nop) LABEL org.label-schema.sc… 0B
    <missing> 2 months ago /bin/sh -c #(nop) ADD file:bd7a2aed6ede423b7… 209MB

操作Docker容器

  • 容器是镜像的一个运行实例。 镜像是静态的只读文件,而容器带有运行时需要的可写文件层。

创建容器

  • 命令

    1
    docker create [OPTIONS] IMAGE [COMMAND] [ARG...]
  • 选项:主要分为以下几类

    • 与容器运行模式相关
    • 与容器和环境配置相关
    • 与容器资源限制和安全保护相关
  • 使用 docker create 命令新建的容器初始时处于停止状态,可使用 docker start 命令来启动它。

启动容器

1
docker start [OPTIONS] CONTAINER [CONTAINER...]
  • 选项:

    Name, shorthand Default Description
    --attach , -a Attach STDOUT/STDERR and forward signals
    --checkpoint experimental (daemon) Restore from this checkpoint
    --checkpoint-dir experimental (daemon) Use a custom checkpoint storage directory
    --detach-keys Override the key sequence for detaching a container
    --interactive , -i Attach container’s STDIN
  • 可通过 docker ps 命令来查看运行中的容器。

新建并启动容器

1
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
  • 等价于 docker create + docker start 。因此其选项也是两者的结合体。

  • 当执行 docker run 时,Docker后台运行的操作包括:

    • 检查本地是否存在指定的镜像,如果不存在就下载。
    • 利用镜像创建一个容器,并启动该容器。
    • 分配一个文件系统给容器,并在只读的镜像层外面挂载一层可读可写层。
    • 从宿主主机配置的网桥接口中桥接一个虚拟接口道容器中。
    • 从网桥的地址池配置一个IP地址给容器。
    • 执行用户指定的应用程序。
    • 执行完毕后容器被自动终止。
  • 例1:启动容器,执行命令,然后关闭容器:

    1
    docker run ubuntu /bin/echo 'Hello World'
  • 例2:启动容器,进入交互模式:

    • -i 表示让容器的标准输入保持打开。
    • -t 表示让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上。
    • 交互模式下,退出容器:按 Ctrl + d 或输入 exit 命令。退出后容器自动处于退出( Exited )状态。
    • 不停止容器类型的情况下退出: CTRL+P+Q
    1
    docker run -it ubuntu /bin/bash

守护态运行

  • 以守护态(Daemonized)运行于后台。添加 -d 参数。

  • 例:守护态运行的容器状态是 up

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

终止容器

渐进终止:docker stop

  • 先向容器发送SIGTERM信号,等待一段时间(默认10秒)后,再发送SIGKILL信号。

    1
    docker stop [OPTIONS] CONTAINER [CONTAINER...]
  • 选项:

    Name, shorthand Default Description
    --time , -t 10 Seconds to wait for stop before killing it

直接终止:docker kill

  • 直接向容器发送SIGKILL信号。

    1
    docker kill [OPTIONS] CONTAINER [CONTAINER...]
  • 选项:

    Name, shorthand Default Description
    --signal , -s KILL Signal to send to the container

重启

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

  • docker restart 命令可作用与运行中的容器,先终止,再启动。

进入容器

  • 在使用 -d 参数以守护态启动容器时,用户无法看到容器中的信息,也无法进行操作。如果想要进入容器进行操作,有以下方式。

attach命令

  • 功能:将本地标准输入、标准输出、标准错误流与一个运行中的容器关联。

    1
    docker attach [OPTIONS] CONTAINER
  • 选项:

    Name, shorthand Default Description
    --detach-keys 指定推出attach模式的快捷键序列。默认是CTRL+p。注意是推出attach模式,不是终止容器。
    --no-stdin false 是否不关联标准输入。默认是false。用于一些只读场景。
    --sig-proxy true 是否代理所有收到的系统信号给应用进程。

exec命令

  • 功能:在容器中执行一条命令。

    1
    docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
  • 选项:

    Name, shorthand Default Description
    --detach , -d Detached mode: run command in the background
    --detach-keys Override the key sequence for detaching a container
    --env , -e API 1.25+ Set environment variables
    --interactive , -i Keep STDIN open even if not attached
    --privileged Give extended privileges to the command
    --tty , -t Allocate a pseudo-TTY
    --user , -u Username or UID (format: <name|uid>[:<group|gid>])
    --workdir , -w API 1.35+ Working directory inside the container
  • 例:执行打开终端命令:

    • –rm表示退出容器后删除容器
    1
    2
    $ docker run -dit --name test01 centos /bin/bash
    $ docker exec -it test01 /bin/bash

nsente工具

  • nsenter命令是一个可以在指定进程的命令空间下运行指定程序的命令。它位于util-linux包中。
  • 一个最典型的用途就是进入容器的网络命令空间。相当多的容器为了轻量级,是不包含较为基础的命令的,比如说ip addresspingtelnetsstcpdump 等等命令,这就给调试容器网络带来相当大的困扰:只能通过 docker inspect ContainerID 命令获取到容器IP,以及无法测试和其他网络的连通性。
  • 这时就可以使用nsenter命令仅进入该容器的网络命名空间,使用宿主机的命令调试容器网络。此外,nsenter也可以进入 mnt, uts, ipc, pid, user 命令空间,以及指定根目录和工作目录。

namespace是Linux中一些进程的属性的作用域,使用命名空间,可以隔离不同的进程。

Linux的每个进程都具有命名空间,可以在/proc/PID/ns目录中看到命名空间的文件描述符。

Linux在不断的添加命名空间,目前有:

  • mount:挂载命名空间,使进程有一个独立的挂载文件系统,始于Linux 2.4.19

  • ipc:ipc命名空间,使进程有一个独立的ipc,包括消息队列,共享内存和信号量,始于Linux 2.6.19

  • uts:uts命名空间,使进程有一个独立的hostname和domainname,始于Linux 2.6.19

  • net:network命令空间,使进程有一个独立的网络栈,始于Linux 2.6.24

  • pid:pid命名空间,使进程有一个独立的pid空间,始于Linux 2.6.24

  • user:user命名空间,是进程有一个独立的user空间,始于Linux 2.6.23,结束于Linux 3.8

  • cgroup:cgroup命名空间,使进程有一个独立的cgroup控制组,始于Linux 4.6

  • 命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    nsenter [options] [program [arguments]]

    options:
    -t, --target pid:指定被进入命名空间的目标进程的pid
    -m, --mount[=file]:进入mount命令空间。如果指定了file,则进入file的命令空间
    -u, --uts[=file]:进入uts命令空间。如果指定了file,则进入file的命令空间
    -i, --ipc[=file]:进入ipc命令空间。如果指定了file,则进入file的命令空间
    -n, --net[=file]:进入net命令空间。如果指定了file,则进入file的命令空间
    -p, --pid[=file]:进入pid命令空间。如果指定了file,则进入file的命令空间
    -U, --user[=file]:进入user命令空间。如果指定了file,则进入file的命令空间
    -G, --setgid gid:设置运行程序的gid
    -S, --setuid uid:设置运行程序的uid
    -r, --root[=directory]:设置根目录
    -w, --wd[=directory]:设置工作目录

    如果没有给出program,则默认执行$SHELL
  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 在Kubernetes中,在得到容器pid之前还需获取容器的ID,可以使用如下命令获取:
    [root@Wshile test]# kubectl get pod test -oyaml|grep containerID
    - containerID: docker://cf0873782d587dbca6aa32f49605229da3748600a9926e85b36916141597ec85
    # 运行了一个容器并查看容器pid
    $ docker inspect -f {{.State.Pid}} 4e8a3e983dc3
    5645
    # 使用nsenter命令进入该容器的网络命令空间:
    [root@Wshile ~]# nsenter -n -t5645
    [root@Wshile ~]# ip addr

删除容器

  • 命令

    1
    docker rm [OPTIONS] CONTAINER [CONTAINER...]
  • 选项:

    Name, shorthand Default Description
    --force , -f false 强制终止一个运行中的容器 (uses SIGKILL)
    --link , -l false 删除容器的连接,但保留容器
    --volumes , -v false 删除容器关联的数据卷
  • 对于foreground容器,由于其只是在开发调试过程中短期运行,其用户数据并无保留的必要,因而可以在容器启动时设置–rm选项,这样在容器退出时就能够自动清理容器内部的文件系统。示例如下:

    1
    docker run --rm  -it --name test02 centos /bin/bash
    • -rm选项不能与-d同时使用,即只能自动清理foreground容器,不能自动清理detached容器

    • 注意,–rm选项也会清理容器的匿名data volumes。

      所以,执行docker run命令带–rm命令选项,等价于在容器退出后,执行docker rm -v。

导入和导出容器

  • 某些时候,需要将容器从一个系统迁移到另外一个系统,此时可以使用Docker导入和导出功能。

导出容器

导入容器

  • 功能:从一个文件导入一个镜像

    1
    docker import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]
  • 选项:

    Name, shorthand Default Description
    --change , -c Apply Dockerfile instruction to the created image
    --message , -m Set commit message for imported image
  • 注意区别 docker save/loaddocker export/import 。前者操作的目标是镜像,后者操作的目标是容器。不过 docker import 通过容器快照导入的依然是镜像,而非直接启动一个容器。

Docker的数据管理

  • 使用Docker的过程中,往往需要对数据进行持久化,或者需要在多个容器之间进行数据共享,这必然涉及容器的数据管理操作。

  • 容器中管理数据主要有两种方式:

    • 数据卷(Data Volumes):容器内数据直接映射到本地主机环境。
    • 数据卷容器(Data Volume Containers):使用专门容器维护数据卷。

数据卷

  • 数据卷是一个可供 一个或多个容器使用的特殊目录, 它将主机操作系统目录直接映射进容器,类似于Linux中的mount操作。
  • 数据卷可以提供很多有用的特性:
    • 数据卷可以在多个容器之间共享和重用,方便容器间数据传递和共享。
    • 对数据卷内数据的修改会立马生效,无论是容器内操作还是本地操作。
    • 对数据卷更新不会影响镜像,解耦了应用和数据。
    • 数据卷会一直存在,直到没有容器使用,可以安全地卸载它。

volumes 挂载

  • Docker管理宿主机文件系统的一部分,默认位于 /var/lib/docker/volumes 目录中;(最常用的方式

  • 所有Container的数据都保存在了这个目录下边,由于没有在创建时指定卷,所以Docker帮我们默认创建许多匿名(一堆很长ID的名字)卷。

bind mounts

  • 可以存储在宿主机系统的任意位置;(比较常用的方式
  • 在不同的宿主机系统时不可移植的,比如Windows和Linux的目录结构是不一样的,bind mount所指向的host目录也不能一样。这也是为什么bind mount不能出现在Dockerfile中的原因,因为这样Dockerfile就不可移植了。

管理卷

  • 创建卷

    1
    2
    3
    # docker volume create nginxData // 创建一个自定义容器卷
    # docker volume ls // 查看所有容器卷
    # docker volume inspect [OPTIONS] VOLUME [VOLUME...] // 查看指定容器卷详情信息
  • 清理卷

    1
    2
    3
    # docker stop edc-nginx // 暂停容器实例
    # docker rm edc-nginx // 移除容器实例
    # docker volume rm edc-nginx-vol // 删除自定义数据卷

挂载卷

  • 自定义容器卷,我们可以创建一个使用这个数据卷的容器,这里我们以nginx为例:

  • -v代表挂载数据卷,这里使用自定数据卷 nginxData,并且将数据卷挂载到 /usr/share/nginx/html (这个目录是yum安装nginx的默认网页目录)。

  • 如果没有通过-v指定,那么Docker会默认帮我们创建匿名数据卷进行映射和挂载。

1
2
# docker run -d -it --name=nginx01 -p 8800:80 -v nginxData:/usr/share/nginx/html nginx

  • 创建好容器之后,我们可以进入容器里面看看内容:

    1
    $ docker exec -it nginx01 bash
  • 宿主机去到刚刚创建的数据卷里边看到容器里面的两个默认页面,由此可知,volume帮我们做的类似于一个软链接的功能。在容器里边的改动,我们可以在宿主机里感知,而在宿主机里面的改动,在容器里边可以感知到。

  • 如果我们手动stop并且remove当前nginx容器,我们会发现容器卷里面的文件还在,并没有被删除掉。

  • 由此可以验证,在数据卷里边的东西是可以持久化的。如果下次还需要创建一个nginx容器,那么还是复用当前数据卷里面的文件。

Bind Mounts挂载

  • 将宿主机上的 /mnt/d/app/nginx/htmlt 目录(如果没有会自动创建)挂载到 /usr/share/nginx/html (这个目录是yum安装nginx的默认网页目录)。

    1
    docker run -d -it --name=nginx02 -v /mnt/d/app/nginx/html:/usr/share/nginx/html -p 8800:80 nginx
  • 进入容器, 与volumes不同,bind mounts的方式会隐藏掉被挂载目录里面的内容(如果非空的话),这里是/usr/share/nginx/html 目录下的内容被隐藏掉了,因此我们看不到。

    1
    2
    3
    4
    $ docker exec -it nginx02 bash
    root@617e5a7fa198:/# cd /usr/share/nginx/html/
    root@617e5a7fa198:/usr/share/nginx/html# ls
    root@617e5a7fa198:/usr/share/nginx/html#
  • 我们可以将宿主机上的文件随时挂载到容器中,新建 index.html

1
2
3
# 在容器中查看
root@617e5a7fa198:/usr/share/nginx/html# ls
index.html
  • 验证绑定
1
2
3
4
5
docker inspect nginx02
"HostConfig": {
"Binds": [
"/mnt/d/app/nginx/html:/usr/share/nginx/html"
],
  • 清理掉容器之后, 挂载目录里面的文件仍然还在,不会随着容器的结束而消失,从而实现数据持久化。

    1
    2
    docker stop nginx02
    docker rm edc-nginx
  • 挂载一个本地主机文件作为数据卷(不推荐)

    • 将 index.html 文件挂在到容器中
    • 其中 --rm 参数表示容器退出后自动删除。
    • 这种方式在修改挂载文件时,可能造成文件inode改变,进而导致容器报错。所以不推荐。
    1
    2
    $ docker run -it --name=nginx03 --rm -v /mnt/d/app/nginx/html/index.html:/usr/share/nginx/h
    tml/index.html -p 8800:80 nginx

数据卷容器

  • 如果用户需要在多个容器之间共享一些持续更新的数据,最简单的方式是使用数据卷容器。

  • 数据卷容器也是一个容器,但是它的目的是专门用来提供数据卷供其它容器挂载

  • --volumes-from参数指定的目标容器,并不需要处于运行状态。这说明volume-from只是复制目标容器的volume配置而已,并不依赖于目标容器提供什么服务。

  • 使用示例:

    1. 创建一个容器dbdata,并在其中创建一个数据卷:

      1
      docker run -it -v /dbdata --name dbdata ubuntu
    2. 创建另外的容器,从dbdata容器导入数据卷:

      1
      2
      docker run -it --volumes-from dbdata --name db1 ubuntu
      docker run -it --volumes-from dbdata --name db2 ubuntu
    3. 此时dbdata、db1、db2三个容器共享同一个/dbdata数据卷。任意一个容器修改了该目录下的内容,其他容器都能同步获取到。

    4. 可以多次使用--volumes-from参数来从多个容器挂载多个数据卷。还可以从其他已经挂载了别的容器数据卷的容器来挂载数据卷。

      1
      docker run -it --volumes-from db1 --name db3 ubuntu
    5. 如果删除了挂载了数据卷的容器,数据卷并不会自动删除。如果要删除一个数据卷,必须在删除最后一个还挂载着它的容器时显式指定-v参数

      1
      2
      3
      4
      docker rm bd3
      docker rm bd2
      docker rm bd1
      docker rm -v dbdata

利用数据卷容器迁移数据

  • 可以利用数据卷容器对其中的数据进行备份、恢复,以实现数据的迁移。其原理就是找到数据卷目录所对应的实际本机目录,然后备份其中的数据。

备份

  • 命令详解:

    • 首先利用ubuntu镜像创建了一个容器,名为worker。
    • 使用–volumes-from dbdata参数让worker容器挂在dbdata容器的数据卷(/dbdata)。
    • 使用-v ${pwd}:/backup参数来把本地的当前目录挂载到worker容器的/backup目录。(注意,此时worker容器中有两个数据卷,一个是/dbdata,和容器数据卷共享,实际关联的本机目录未知。另一个是/backup,实际关联的本机目录就是当前目录。)
    • worker容器启动后,使用tar cvf /backup/backup.tar /dbdata命令,将/dbdata下的内容打包到/backup/backup.tar。此时我们本机当前目录就有了backup.tar。至此完成了备份。
    1
    2
    3
    4
    5
    6
    docker run \
    --volume-from dbdata
    -v ${pwd}:/backup \
    --name worker \
    ubuntu \
    tar cvf /backup/backup.tar /dbdata

恢复

  1. 将上一步备份的数据恢复到一个数据卷容器中的步骤:

  2. 创建一个带有数据卷的容器,作为数据卷容器:

    1
    docker run -v /dbdata --name dbdata2 ubuntu /bin/bash
  3. 创建另一个容器,它从dbdata2导入数据卷,同时再将本机当前目录挂载到它的/backup数据卷上。此时该容器有两个数据卷,一个是与dbdata2共享的/dbdata,另一个是与本机当前目录关联的/backup。由于是与本机当前目录关联,自然/backup目录下就有backup.tar这个文件。因此在容器中执行解压命令,将其从/backup目录解压到/dbdata目录,即完成了恢复。

    1
    2
    3
    4
    5
    docker run \
    --volumes-from dbdata2 \
    -v ${pwd}:/backup \
    busybox \
    tar xvf /backup/backup.tar

端口映射

从外部访问容器应用

  • 在启动容器时(run或create),如果不指定对应的参数,在容器外部是无法通过网络来访问容器内的网络应用和服务的。

  • 当容器中运行着一些网络应用时,若想要外部访问这些应用,需要通过 -P-p 参数来指定容器到宿主主机的端口映射。 -P (大写)参数表示将容器暴露的端口随机映射到本机49000~49900之间的一个端口上。

  • 例:

    1
    2
    3
    4
    docker run -d -P nginx
    docker ps
    # PORTS -> 0.0.0.0:49153->80/tcp
    # 访问 127.0.0.1:49153 即可打开Nginx的页面
  • -p (小写)可以指定本机映射到容器的端口,且在一个指定端口上只能绑定一个容器端口。支持以下格式:

    • HostPort:ContainerPort:将本地端口映射到容器端口。如果本机有多个IP地址,则所有地址的这个端口都会映射到该容器端口。
    • IP:HostPort:ContainerPort:将指定IP地址的指定端口映射到容器端口。
    • IP::ContainerPort:将指定IP地址的端口随机挑选一个映射到容器端口。
  • 查看容器端口映射的方法:

    • docker port CONTAINER
    • docker ps -l
    • docker logs CONTAINER

容器的互联

  • 容器的互联(linking)是一种让多个容器中的应用进行快速交互的方式。它会在源和接收容器之间创建连接关系,接收容器可以通过容器名快速访问到源容器,而不用指定具体的IP地址。

自定义容器名

  • 创建容器时docker会自动为容器分配一个名字。但最好手动指定名字。 容器名必须是(本机)全局唯一的。

    1
    docker run -d -P --name web training/webapp python app.py

容器互联

  • 实现web应用容器与数据库容器互联:

    1. 创建一个数据库容器,指定容器名:

      1
      docker run -d --name db training/postgres
    2. 创建一个应用容器,并将它连接到db容器:

      1
      docker run -d -P --name web --link db:db training/webapp python app.py
    3. --link 参数的格式为 --link name:alias ,其中name是要连接的容器名称,alias是这个连接的别名。

    4. Docker相当于在两个互联的容器之间创建了一个私密的虚拟管道,不需要容器将自己的端口映射到宿主主机上。这种方式效率更高,也更安全。

  • 当容器A主动link到容器B时,A中会发生以下变化:

    1. 其环境变量中会多出与容器B相关的值

    2. 其/etc/hosts文件中会添加到容器B的域名和IP地址。

  • 因此,在容器A中,有两种方式访问容器B的服务:

    • 读取环境变量,获取服务地址,然后访问。
    • 依赖DNS服务,使用容器B的域名直接访问。
  • 需要注意

  • 使用link选项建立的容器所链接的主机需要在运行状态

  • 使用link选项建立的容器运行时需要所链接的容器也必须是运行状态

  • 使用link选项链接的主机ip不需要固定,因为每次新建容器都会检查所链接容器的ip,在/etc/hosts里生成新的alias 名称对应的ip

Dockerfile

  • Dockerfile是一个文本格式的配置文件,用户可以使用Dockerfile来快速创建自定义的镜像。

基本结构

  • Dockerfile由一行行命令语句组成,并且支持以 # 开头的注释行。

  • 一般而言,Dockerfile从上到下分为四部分:

    • 基础镜像信息
    • 维护者信息(Maintainer)
    • 镜像操作指令(典型的是RUN指令。RUN指令将对镜像执行紧随其后的命令。每运行完一条RUN指令,镜像就添加新的一层,并提交)
    • 容器启动时执行的指令(不一定有)
  • 例:

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

    MAINTAINER NGINX Docker Maintainers "docker-maint@nginx.com"

    ENV NGINX_VERSION 1.10.1-1~jessie

    RUN apt-key adv .......

    RUN ln -sf .......

    EXPOSE 80 443

    CMD ["nginx","-g","daemon off;"]

指令说明

  • 官方文档:https://docs.docker.com/engine/reference/builder/

    指令 作用
    FROM 指定所创建镜像的基础镜像
    MAINTAINER 指定维护者信息
    RUN 指定构建镜像时需执行的命令
    CMD 指定启动容器时默认执行的命令
    LABEL 指定生成镜像的元数据标签信息
    EXPOSE 声明镜像内服务所监听的端口
    ENV 指定环境变量
    ADD 复制指定的路径下的内容到镜像下的路径下,可以为URL;如果为tar文件,会自动解压到路径下。
    COPY 复制本地主机的路径下的内容到镜像下的路径下。通常我们都是拷贝本机文件,所以推荐使用COPY,因为更简单、更明确。
    ENTRYPOINT 指定容器的默认入口
    VOLUME 创建数据卷挂载点
    USER 指定运行容器时的用户名或UID
    WORKDIR 配置工作目录
    ARG 指定镜像内使用的参数(例如版本号信息等)
    ONBUILD 配置当所创建的镜像作为其他镜像的基础镜像时,所执行的创建操作指令。
    STOPSIGNAL 指定容器退出的信号值
    HEALTHCHECK 指定如何进行健康检查
    SHELL 指定使用shell时的默认shell类型

FROM

  • 指定所创建镜像的基础镜像,如果本地本存在,则默认回去Docker Hub下载指定镜像。一个合法的Dockerfile一定是以FROM指令开头。如果要在同一个Dockerfile中创建多个镜像,可以使用多个FROM指令,每个镜像一个。

  • 格式:

    1
    2
    3
    4
    5
    FROM <image> [AS <name>]

    FROM <image>[:<tag>] [AS <name>]

    FROM <image>[@<digest>] [AS <name>]

MAINTAINER

  • 指定维护者信息。该信息会写入生成镜像的Author属性域中。

  • 格式:

    1
    MAINTAINER <name>
  • 根据官方文档,该指令已过时。应该使用 LABEL 指令代替,因为它更灵活。

    • LABEL指令添加元数据到镜像中。如果要使用包含有空格的元数据,可以给key-value加上引号。
    1
    2
    3
    LABEL <key>=<value> <key>=<value> <key>=<value> ...

    LABEL maintainer="SvenDowideit@home.org.au"

RUN

  • 指定构建镜像时运行的指令。每条RUN指令都将在当前镜像基础上创建一个新层,执行指定命令,然后提交,从而产生一个新镜像。这个新镜像也将成为下一个RUN命令的的当前镜像。

  • 有两种格式:

    1
    2
    3
    4
    5
    6
    7
    RUN <command>
    # shell形式。将在默认的shell终端中运行命令。在linux下是/bin/sh -c。
    # 命令太长时可用\换行

    RUN ["executable", "param1", "param2"]
    # exec形式。使用exec执行,不会启动shell。
    # 注意这里数组将被解析为JSON数组,因此必须用双引号。

CMD

  • 指定从生成的镜像启动容器时,默认执行的命令

  • 有三种形式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    CMD ["executable","param1","param2"]
    # exec形式。使用exec执行,不会启动shell。
    # 是官方推荐的形式。

    CMD command param1 param2
    # shell形式。将在默认的shell终端中运行命令。在linux下是/bin/sh -c。

    CMD ["param1","param2"]
    # 这种形式不是指定默认命令,而是为ENTRYPOINT指定的命令提供参数。
    # 所有数组中的值都将作为ENTRYPOINT指定的命令的参数。
  • 每个Dockerfile只能有一条CMD命令。如果指定了多条,则只有最后一条会被执行。

  • 如果用户在启动容器时手动指定了运行的命令(作为run或create的参数),则会覆盖掉CMD。

LABLE

  • 指定生成的镜像的元数据信息。

  • 格式:

    1
    LABEL <key>=<value> <key>=<value> <key>=<value> ...
  • 可以指定多个LABLE。

  • 父镜像的LABEL会被子镜像继承。如果子镜像指定了同名LABEL,则会覆盖。

  • 可通过 docker inspect 指令来查看label。

EXPOSE

  • 声明容器运行时将监听的端口。

  • 格式:

    1
    2
    3
    4
    5
    6
    EXPOSE <port> [<port>/<protocol>...]

    # 例:
    EXPOSE 80
    EXPOSE 80/tcp
    EXPOSE 80/udp
  • 可指定监听TCP或UDP端口。如果不指定协议,则默认监听TCP端口。

  • 使用EXPOSE指令并不会真的发布该端口(即监听宿主主机的相应端口)。为了真的发布该端口,需要在docke run时使用-p或-P参数指定发布端口。

ENV

  • 指定环境变量。指定的环境变量在后续的构建过程中生效,也会存在于启动后的容器中。

  • 格式:

    1
    2
    ENV <key> <value>
    ENV <key>=<value> ...
  • 指定的环境变量可在启动容器时覆盖:

    1
    docker run --env <key>=<value> image

ADD

  • 该指令将指定的文件、目录或URL指定的远程文件拷贝到镜像中的指定目录下。

  • 格式:

    1
    2
    ADD [--chown=<user>:<group>] <src>... <dest>
    ADD [--chown=<user>:<group>] ["<src>",... "<dest>"] # 当路径中有空格时,使用该形式
  • 其中<src>可以是以下三种形式:

    • Dockerfile所在目录的一个相对路径(文件或目录)。注意只能访问该目录及其子目录。

    • 一个URL

    • 一个tar文件

  • <dest>可以是镜像内的绝对路径,或者相对于工作目录(WORKDIR指令指定)的相对路径。路径支持正则表达式。

COPY

  • 该指令将指定的文件、目录拷贝到镜像中的指定目录下。

  • 格式:

    1
    2
    COPY [--chown=<user>:<group>] <src>... <dest>
    COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
  • <src>可以是Dockerfile所在目录的一个相对路径(文件或目录)。注意只能访问该目录及其子目录。

  • <dest>可以是镜像内的绝对路径,或者相对于工作目录(WORKDIR指令指定)的相对路径。路径支持正则表达式。

COPY和ADD对比

  • COPY和ADD的功能基本一致。ADD可视为增强版的COPY。

  • COPY指令只能复制当前构建上下文中的文件(即当前Dockerfile所在目录下的文件)。

  • ADD指令除了可复制当前构建上下文中的文件外,还支持其他两种数据源:通过URL访问远程文件和指定一个需要自动解压的tar文件。

  • 大部分情况下,我们都只是复制本机文件到镜像,因此应该使用COPY。而对于拷贝远程文件、解压文件等情形,虽然ADD可以完成,但不如直接使用RUN CURL等命令直接和清晰。因此绝大部分情况下,都应该使用COPY,不要使用ADD。

  • 参考:

ENTRYPOINT

  • 指定镜像的默认入口命令。

  • 格式:

    1
    2
    3
    4
    5
    6
    ENTRYPOINT ["executable", "param1", "param2"]
    # exec形式。使用exec执行,不会启动shell。
    # 是官方推荐的形式。

    ENTRYPOINT command param1 param2
    # shell形式。将在默认的shell终端中运行命令。在linux下是/bin/sh -c。
  • 当指定了ENTRYPOINT时,CMD指令只能用于提供参数。

  • 每个Dockerfile中只能有一个ENTRYPOINT,当指定多个时,只有最后一个生效。

  • 在启动容器时,必须使用**–entrypoint**参数才能覆盖掉Dockerfile中的配置。镜像名后跟命令的形式不行。

CMD和ENTRYPOINT对比

  • 两者的最终目的都是为了给基于该镜像启动的容器提供默认执行项。 如果直接启动一个不带CMD或ENTRYPOINT的镜像,将会报错:

    1
    2
    $ docker run alpine
    FATA[0000] Error response from daemon: No command specified
  • 因此,通常情况下CMD和ENTRYPOINT至少应该指定一个。

  • 另一方面,CMD相比于ENTRYPOINT来说,更加容器被覆盖(docker run最后的参数即会覆盖CMD,不需要专门指定参数)。如果你的镜像用途并不明确,没有预期用户会怎么使用,那么推荐使用CMD。这样你的镜像使用上更加灵活。

  • 如果你的镜像明确是一个“可执行应用”,期望用户就是把它当成一个应用来用,那么就应该使用ENTRYPOINT。并且此时推荐ENTRYPOINT和CMD同时使用。ENTRYPOINT负责指定执行的命令,而CMD负责该命令的提供默认参数。当用户启动容器、不指定任何参数时,将使用CMD指定的参数。而当用户指定了参数时,则使用用户提供的参数。

  • 参考资料:https://www.ctl.io/developers/blog/post/dockerfile-entrypoint-vs-cmd/

VOLUME

  • 创建一个挂载点。

  • 格式:

    1
    2
    VOLUME ["<路径1>", "<路径2>"...]
    VOLUME <路径>
  • 容器在运行时,由于采用的是联合文件系统,在其中进行写操作很慢,并且容器关闭后数据就丢失了。为了提高性能,需要绕过联合文件系统,直接访问宿主主机的文件系统。这便是VOLUME的作用。

  • VOLUME既可以在容器启动时指定,也可以在Dockerfile中指定,从而镜像构建时便确定。

  • 对于数据库一类的应用,VOLUME很重要,需要将产生的数据放到一个VOLUME中。为了避免用户忘记声明VOLUME,于是需要在Dockerfile中指定。

USER

  • 该指令指定一个用户名(或UID),也可指定一个用户组(或GID)。在镜像构建阶段,Dockerfile中位于USER指令之后的RUN、CMD、ENTRYPOINT指令的执行将使用USER指定的用户名和用户组。

  • 格式:

    1
    2
    USER <user>[:<group>] or
    USER <UID>[:<GID>]

WORKDIR

  • 该指令为后续的RUN, CMD, ENTRYPOINT, COPYADD 指令指定工作目录。

  • 格式:

    1
    WORKDIR /path/to/workdir
  • 如果指定的工作目录不存在,将自动创建一个新目录。

  • 一个Dockerfile中可以使用多个WORKDIR指令,根据需要不断切换工作目录。如果使用的路径是相对路径,则会相对于前一个工作目录进行计算。

  • WORKDIR指令可以解析它前面通过ENV设置的环境变量:

    1
    2
    3
    ENV DIRPATH /path
    WORKDIR $DIRPATH/$DIRNAME
    RUN pwd # 这里pwd输出为/path/$DIRNAME

ARG

  • 该指令用于定义一些构建参数,用户可以在执行 docker build 命令时传入这些参数值。典型的,比如版本号。

  • 注意,ARG用于定于构建参数,准确点说应该叫BUILD_ARG。ARG只会存在于构建时,不会存在于容器启动时。

  • 另外,CMD和ENTRYPOINT是在启动时进行解析的。因此,ARG不能用于CMD和ENTRYPOINT命令中。因为那个时候ARG已经不存在了。一个解决方案是,把ARG赋值给环境变量ENV,ENV是会保留到运行时的。

  • 格式:

    1
    ARG <name>[=<default value>]
  • 用户指定参数值:

    1
    docker build --build-arg <varname>=<value>
  • 如果指定的参数不存在,则会参数如下警告信息:

    1
    [Warning] One or more build-args [foo] were not consumed.
  • 可以多次使用ARG指令,指定多个参数。

  • 注意:不要通过ARG指令传递敏感信息,因为传递的构建参数值是可以通过 docker history 命令看到的。

ONBUILD

  • 该指令指定一些其他指令,这些指令将在当前镜像作为别的镜像的基础镜像时,在其构建阶段执行。

  • 格式:

    1
    ONBUILD [INSTRUCTION]
  • 用ONBUILD指定的指令,在本镜像的构建过程中不会执行,而是存储在镜像中。当创建另一个Dockerfile,并且用FROM指令指定本镜像作为基础镜像时,在子镜像的构建阶段,读取到FROM指令时,Docker会寻找基础镜像的ONBUILD指令,并按顺序执行其指定的指令。

  • 注意ONBUILD指令只会影响直接继承的子镜像的构建过程,对于孙子镜像没有影响。

  • 该指令常用于制作“执行环境”类型的镜像,且镜像名通常会添加“onbuild”后缀。

STOPSIGNAL

  • 指定所创建的镜像启动的容器所接受的退出信号值。

  • 格式:

    1
    STOPSIGNAL signal # signal可以为无符号整数,如9,或信号名,如SIGKILL

HEALTHCHECK

  • 该指令告诉Docker如何检查启动的容器是否处于正常工作状态。

  • 有两种格式:

    1
    2
    HEALTHCHECK [OPTIONS] CMD command (通过在容器内执行指定的命令来检查容器是否健康)
    HEALTHCHECK NONE (禁用从基础镜像继承的健康检查)
  • 选项:

    • --interval=DURATION (default: 30s)
    • --timeout=DURATION (default: 30s)
    • --start-period=DURATION (default: 0s)
    • --retries=N (default: 3)
  • 一个Dockerfile中只能指定一个HEALTHCHECK指令。指定多个时只有最后一个生效。

SHELL

  • 为后续的RUN、CMD、ENTRYPOINT的shell形式命令指定默认shell。

  • 格式:

    1
    SHELL ["executable", "parameters"]

创建镜像

  • 当创建了一个Dockerfile之后,即可通过docker build指令来创建镜像。

    1
    docker build [OPTIONS] PATH | URL | -
  • 选项:https://docs.docker.com/engine/reference/commandline/build/

  • 常用选项:

    • –file , -f,指定dockerfile所在路径。
    • –tag , -t,指定生成镜像的标签信息。
  • docker build命令依靠上下文dockerfile文件来构建镜像。上下文是PATH或URL参数指定的目录及其子目录。

  • Dockerfile默认也在上下文中找,但是可以通过-f参数指定上下文以外的其他文件。该命令将上下文和dockerfile发送给Docker服务端,由服务端来创建镜像

使用.dockerignore文件

  • 在使用docker build命令时,在docker CLI将上下文发送给docker守护进程之前,它会在上下文的根目录中寻找.dockerignore文件。

  • 如果该文件存在,CLI将使用该文件中指定的模式对context目录下的文件进行匹配,并将匹配成功的文件从上下文中移除,然后再发送。这样可以避免发送大文件和敏感文件。

  • .dockerignore文件中每一行代表一个模式。

  • 例:

    1
    2
    3
    4
    */temp*
    */*/temp*
    temp?
    # 注释

实践编写标准

  • 精简镜像用途。
  • 选用合适的基础镜像。
  • 提供足够清晰的命令注释和维护者信息。
  • 正确使用版本号。
  • 减少容器层数。需要尽量合并指令。
  • 及时删除临时文件和缓存文件。
  • 合理使用缓存,减少内容目录下文件,使用.dockerignore,以提高生产速度。
  • 合理指令顺序。在开启缓存的情况下,内容不变的指令尽量放在前面。
  • 减少外部源干扰。

操作系统镜像

BusyBox

  • BusyBox是一个集成了一百多个最常用Linux命令和工具(如cat、echo、grep、mount、telnet)的精简工具箱,它只有几MB的大小,非常适合进行各种快速验证,被誉为“Linux系统中的瑞士军刀”。

  • 使用Busybox镜像启动一个容器,可以Linux命令的快速试验。

Alpine

  • Alpine操作系统是一个面向安全的轻型Linux发行版。

  • 特点:

    • 精简、安全、高性能、占用资源少。
    • 体积小(5MB)。
    • 提供友好的包管理工具apk,可以方便地安装软件。
  • 官方推荐使用Alpine作为基础镜像。

Debian/Ubuntu

CentOS/Fedora

为镜像添加SSH服务

  • 进入容器中的办法,比如attach、exec等命令。但是,以上这些命令只能进入本机容器,要求用户先登录到容器的宿主主机。
  • 为了能够远程登录到容器中进行操作,需要容器本身支持SSH服务。

基于commit命令创建

  1. 利用ubuntu镜像创建一个容器:
1
2
3
# $ docker run -it ubuntu /bin/bash
$docker run -it --name centos7ssh centos bash
[root@9f2fb07b9fb7 /]#
  1. 安装SSH服务

    1
    2
    3
    4
    5
    6
    # 更新软件下载源 apt-get update
    [root@9f2fb07b9fb7 /]# yum -y update
    # 选择主流的openssh-server作为服务端:apt-get install openssh-server -y
    [root@9f2fb07b9fb7 /]# yum install openssh-server -y
    [root@9f2fb07b9fb7 /]# yum -y install passwd openssl openssh-clients
    Complete!
  2. 如果需要正常启动SSH服务,则目录/var/run/sshd必须存在。手动创建并启动SSH服务:

1
[root@9f2fb07b9fb7 /]# mkdir -p /var/run/sshd
  • ubuntu容器启动

    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
    root@161f67ccad50:/# /usr/sbin/sshd -D &
    [1] 3020
    # 查看容器的22端口:
    root@161f67ccad50:/# netstat -lnutp|grep 22
    tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 3020/sshd
    tcp6 0 0 :::22 :::* LISTEN 3020/sshd
    root@161f67ccad50:/#
    # 修改SSH服务的安全登录配置,取消pam登陆限制:
    root@161f67ccad50:/# sed -ri 's#session required pam_loginuid.so#session required pam_loginuid.so#g' /etc/pam.d/sshd
    root@161f67ccad50:/#
    # 在root用户家目录创建.ssh目录,并复制需要登录的公钥信息到.ssh目录下的authorized_keys中:
    root@161f67ccad50:/# mkdir /root/.ssh
    root@161f67ccad50:/# cd /root/.ssh
    root@161f67ccad50:~/.ssh# ls
    root@161f67ccad50:~/.ssh# vi /root/.ssh/authorized_keys
    # 创建自启动的SSH服务可执行文件run.sh,并添加可执行权限:
    root@161f67ccad50:/# cat run.sh
    #!/bin/bash
    /usr/sbin/sshd -D &
    root@161f67ccad50:/# chmod +x run.sh
    root@161f67ccad50:/#
    # 修改root密码
    root@161f67ccad50:/# passwd root
    #  退出容器:
    root@161f67ccad50:/# exit
    exit
  • centos启动容器

    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
    # 0.创建 /var/run/sshd/目录,要不然sshd服务启动会报错
    [root@9f2fb07b9fb7 /]# mkdir -p /var/run/sshd
    # 1. 编辑sshd的配置文件/etc/ssh/sshd_config,将其中的UsePAM yes改为UsePAM no
    [root@9f2fb07b9fb7 /]# sed -i "s/UsePAM.*/UsePAM no/g" /etc/ssh/sshd_config
    # 2. 创建公私密钥,输入命令后,按两次enter键确认就行
    ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
    ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key
    ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key
    # 3. 开启ssh服务
    [root@9f2fb07b9fb7 /]# /usr/sbin/sshd -D &
    [1] 88
    # 4. 检查服务 yum install -y net-tools
    [root@9f2fb07b9fb7 /]# ps -ef | grep sshd
    root 114 1 0 03:02 pts/0 00:00:00 /usr/sbin/sshd -D
    root 116 1 0 03:02 pts/0 00:00:00 grep --color=auto sshd
    [root@9f2fb07b9fb7 /]# netstat -lnutp|grep 22
    tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 114/sshd
    tcp6 0 0 :::22 :::* LISTEN 114/sshd
    # 5. 修改密码
    [root@9f2fb07b9fb7 /]# passwd
    Changing password for user root.
    New password:
    BAD PASSWORD: The password is shorter than 8 characters
    Retype new password:
    passwd: all authentication tokens updated successfully.

    # 6. 本地ssh登录
    [root@9f2fb07b9fb7 /]# ssh localhost
    The authenticity of host 'localhost (127.0.0.1)' can't be established.
    ECDSA key fingerprint is SHA256:Jd4716Zgpv238UOTswKZsTo0asc4gszB2CfhpD35TvI.
    Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
    Warning: Permanently added 'localhost' (ECDSA) to the list of known hosts.
    root@localhost's password:
  • 保存镜像

  • 将退出的容器用docker commit命令保存为一个新的sshd:centos镜像:

    1
    docker commit 9f2fb07b9fb7 sshd:centos
  • 使用镜像

    • 启动容器,并添加端口映射到容器的22端口:

      1
      2
      3
      $ docker run -it -p 10086:22 sshd:centos
      [root@8f6ce5d8cf8c /]# /usr/sbin/sshd -D &
      [root@8f6ce5d8cf8c /]# netstat -lnutp|grep 22
    • 宿主机登录(容器中的主机)

      1
      2
      3
      4
      C:\Users\Administrator>ssh root@127.0.0.1 -p 10086
      root@127.0.0.1's password:
      Last login: Tue Feb 16 03:18:10 2021 from 127.0.0.1
      Last login: Tue Feb 16 03:18:10 2021 from 127.0.0.1

使用DockerFile

  1. 创建工作目录

    1
    2
    3
    4
    5
    [root@docker ~]# mkdir -p sshd_ubuntu
    # 在其中创建Dockerfile和run.sh文件:
    [root@docker ~]# cd sshd_ubuntu/ && touch Dockerfile run.sh
    [root@docker sshd_ubuntu]# ls
    Dockerfile run.sh
  2. 编写run.sh

    1
    2
    3
    4
    [root@docker sshd_ubuntu]# vim run.sh
    [root@docker sshd_ubuntu]# cat run.sh
    #!/bin/bash
    /usr/sbin/sshd -D &
  3. ssh免密登录

    1
    2
    3
    # ssh-keygen -t rsa -f /mnt/c/Users/Administrator/.ssh/id_rsa
    [root@docker sshd_ubuntu]# cat /mnt/c/Users/Administrator/.ssh/id_rsa.pub > ~/sshd_ubuntu/authorized_keys
    [root@docker sshd_ubuntu]#
  4. 编写DockerFile

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    [root@docker sshd_ubuntu]# cat Dockerfile
    # 基础镜像信息
    FROM ubuntu:14.04

    # 维护者信息
    MAINTAINER staryjie staryjie@163.com

    # 更新apt缓存、安装ssh服务
    RUN apt-get update && apt-get install -y openssh-server
    RUN mkdir -p /var/run/sshd /root/.ssh
    RUN sed -ri 's#session required pam_loginuid.so#session required pam_loginuid.so#g' /etc/pam.d/sshd

    # 配置免密要和自启动脚本
    ADD authorized_keys /root/.ssh/authorized_keys
    ADD run.sh /run.sh
    RUN chmod 755 /run.sh

    # 暴露22端口
    EXPOSE 22

    # 设置脚本自启动
    CMD ["/run.sh"]
    [root@docker sshd_ubuntu]#
  5. 创建镜像

    1
    2
    3
    4
    5
    6
    7
    [root@docker ~]# cd ~/sshd_ubuntu/ && docker build -t sshd02:centos  .
    Removing intermediate container e86118d7da77
    Successfully built 12abdcc3350f
    Successfully tagged sshd:ubuntu2
    [root@docker sshd_ubuntu]# docker images
    REPOSITORY TAG IMAGE ID CREATED SIZE
    sshd ubuntu2 12abdcc3350f 7 seconds ago 284MB
  6. 运行容器

    1
    2
    3
    4
    5
    [root@docker sshd_ubuntu]# docker run -it --name ssh_test -p 10122:22 sshd02:centos bash
    root@c03d5c93ec84:/# netstat -lnutp|grep 22
    tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 17/sshd
    tcp6 0 0 :::22 :::* LISTEN 17/sshd
    root@c03d5c93ec84:/#
  7. 宿主机ssh连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@docker ~]# ssh root@127.0.0.1 -p 10122
The authenticity of host '[10.0.0.31]:10122 ([10.0.0.31]:10122)' can't be established.
ECDSA key fingerprint is 13:3a:46:78:aa:b0:ac:9b:75:1f:ba:99:82:c6:8b:76.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[10.0.0.31]:10122' (ECDSA) to the list of known hosts.
Welcome to Ubuntu 14.04 LTS (GNU/Linux 4.4.0-98-generic x86_64)

* Documentation: https://help.ubuntu.com/

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

root@c03d5c93ec84:~#

DockerFIle Cetos版本

  • 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
    # 基础镜像信息
    FROM centos
    # 维护者信息
    MAINTAINER fulsun fl_6145@163.com

    # 更新apt缓存、安装ssh服务
    RUN yum -y update && yum -y install passwd openssl openssh-clients openssh-server
    RUN mkdir -p /var/run/sshd /root/.ssh
    RUN sed -i "s/UsePAM.*/UsePAM no/g" /etc/ssh/sshd_config
    RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
    RUN ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key
    RUN ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key

    RUN echo 'root' | passwd --stdin root

    # 添加测试用户admin,密码admin,并且将此用户添加到sudoers里
    RUN useradd admin
    RUN echo "admin:admin" | chpasswd
    RUN echo "admin ALL=(ALL) ALL" >> /etc/sudoers

    # 配置免密要和自启动脚本
    ADD authorized_keys /root/.ssh/authorized_keys
    ADD run.sh /run.sh

    RUN chmod 755 run.sh

    # 暴露22端口
    EXPOSE 22

    CMD ip addr ls eth0 | awk '{print $2}' | egrep -o '([0-9]+\.){3}[0-9]+';/usr/sbin/sshd -D
    • 构建镜像

      1
      2
      $ docker rmi sshcentos:0.1 -f
      $ docker build -t sshcentos:0.1 .
    • 运行镜像,暴露容器的22端口

      1
      2
      docker rm test01 -f
      docker run -dit --name test01 -p 10086:22 sshcentos:0.1
    • 宿主机SSH方式访问

      1
      > ssh admin@127.0.0.1 -p 10086
    • 检查容器sshd服务

      1
      2
      $ docker exec -it test01 bash
      [root@dc302489e459 /]# ps -ef |grep sshd

Docker核心实现技术

  • 作为一种容器虚拟化技术,Docker深度应用了操作系统的多项底层支持技术。

  • 早期版本的Docker是基于已经成熟的Linux Container(LXC)技术实现的。自Docker 0.9版本起,Docker逐渐从LXC转移到新的libcontainer上,并且积极推动开放容器规范runc,视图打造更通用的底层容器虚拟化库。

  • 从操作系统功能上看,目前Docker底层依赖的核心技术主要包括Linux操作系统的:

    • 命名空间(Namespace)
    • 控制组(Control Group)
    • 联合文件系统(Union File System)
    • Linux网络虚拟化支持

基本架构

  • Docker目前采用了标准的C/S架构。客户端和服务端既可以运行在一个机器上,也可以运行在不同机器上,通过socket或RESTful API来进行通信。

服务端

  • Docker Daemon一般在宿主主机后台运行,作为服务端接受来自客户的请求,并处理这些请求(创建、运行、分发容器)。

  • 在设计上,Docker Daemon是一个模块化的架构,通过专门的Engine模块来分发管理各个来自客户端的任务。Docker服务端默认监听本地的unix:///var/run/docker.sock套接字,只允许本地的root用户或docker用户组成员访问。可以通过 -H 选项来修改docker daemon监听的目标。

  • 例如,让服务端监听本地的TCP连接1234端口:

    1
    docker daemon -H 0.0.0.0:1234
  • 此外,Docker也支持通过HTTPS认证方式来验证访问。

    • unix:///var/run/docker.sock是Unix域套接字,是本机进程间通信的一种方式。
    • 创建一个Unix域套接字时,需要指定一个文件地址,对于docker来说,这个地址就是/var/run/docker.sock。
    • 由于docker daemon监听这个套接字,因此直接向这个套接字写入命令即可操作Docker。
    • 因此在本机启动用于Docker管理的Container时,都会看到这样的参数:-v /var/run/docker.sock:/var/run/docker.sock,即将本机的docker.sock挂在到容器中,从而使容器可以直接向本机Docker发送命令。
    • 对于不同OS,docker daemon启动时的初始配置文件位置不同,对于使用systemd管理启动服务的系统,配置文件在 /etc/systemd/system/docker.service.d/docker.conf
  • 参考:

客户端

  • Docker客户端为用户提供一系列可执行命令,用户用这些命令与Docker Daemon交互。
  • 客户端默认通过本地的unix:///var/run/docker.sock套接字向服务端发送命令。
  • 如果服务端没有监听在默认的地址,则需要客户端在执行命令时显式指定服务端地址。

新的架构设计

  • 原架构的缺点:Docker Daemon责任太重,既然响应API请求,又要管理容器。

  • 新架构:在1.11.0+之后的版本中,管理容器的任务被放到了一个单独的组件containerd中,减少了daemon的工作。

命名空间

  • 命名空间(namespace)是Linux内核的一个强大特性。
  • 每个容器都拥有自己单独的命令控件,运行在其中的应用都像是在独立的操作系统环境中一样。
  • 命名空间机制保证了容器之间资源隔离,彼此互不影响。

控制组

  • 控制组(CGroups)是Linux内核的一个特性,主要用来对共享资源进行隔离、限制、审计等。

  • 控制组可以提供对容器的内存、CPU、磁盘IO等资源进行限制和计费管理。

  • 具体来看,控制组提供:

    • 资源限制:比如容器能使用的内存限制。
    • 优先级:CPU优先级。
    • 资源审计:用来统计系统实际上把多少资源用到适合的目的上。
    • 隔离:为组隔离命名空间,这样一个组不会看到另一个组的进程、网络连接和文件系统。
    • 控制:挂起、恢复和重启动等操作。
  • Docker的控制组相关信息保存在 /sys/fs/cgroup/memory/docker/ 目录下。如下图:

    • 其中白色的文件为全局配置,修改这些文件会影响所有Container。
    • 的文件夹代表每个容器,其中包含各个容器的配置。但是不应修改其中的文件,只是查看容器状态时可以读取其中文件。
  • 可以在创建或启动容器时为每个容器指定资源限制,例如:

    • -c | --cpu-shares=XXX 调整容器使用CPU的权重。
    • -m | --memory=XXX 调整容器使用内存的大小。

联合文件系统

  • 联合文件系统(UnionFS)是一种轻量级的高性能分层文件系统,它支持将文件系统中的修改信息作为一次提交,并层层叠加,同时可以将不同目录挂载到同一个虚拟文件系统下,应用看到的是挂载的最终结果。

  • 联合文件系统是实现Docker镜像的技术基础。Docker镜像可以通过分层来继承,不同镜像可共享相同的层,从而减小镜像大小。

Docker存储

  • 一个Docker镜像自身是由多个文件层组成的,每一层有唯一的编号(层ID)。

  • 可以通过 docker history 查看一个本地镜像由哪些层组成。例:

  • 构成Docker镜像的各个文件层内容都是只读的、不可修改的。而当Docker利用镜像启动一个容器时,将在镜像文件系统的最顶端再挂在一个新的可读可写层给容器

  • 容器中的内容更新只会发生在该读写层。当需要修改底层文件层中的某个文件时,需要先将该文件复制到读写层,然后在读写层修改(写时复制,copy on write)。

  • 当修改的目标文件较大时,性能比较差。因此,一般推荐将容器修改的数据通过Volume方式挂在,而不是直接修改镜像内的数据。

  • 另外,对于频繁启停的Docker容器,文件系统本身的IO性能也十分关键。

  • 具体来说,Docker相关的所有文件都存储在 var/lib/docker 目录下,里面包含Docker镜像和容器运行相关的所有文件。

  • 而镜像文件系统相关的内容,则根据使用的文件系统的不同,存储在不同的文件夹下,即/var/lib/docker/{driver-name}。

    • 如果使用的是aufs(ubuntu常用),则在aufs文件夹下;
    • 如果是deveice mapper,则在devicemapper文件系统下。
    • 不同文件系统内,存储镜像文件系统的方式不同。
    • 但通常都有一个/mnt/目录,代表各个容器运行时的最终挂载点。

多种文件系统比较

  • Docker支持多种联合文件系统,在启动时按照以下优先级选择:
    • overlay2
    • aufs
    • devicemapper
    • btrfs
    • vfs

Linux网络虚拟化

基本原理

  • 直观上看,要实现网络通信,机器需要至少一个网络接口(物理接口或虚拟接口)与外界相通,并可以收发数据包;此外,如果不同子网之间要进行通信,需要额外的路由机制。

  • Docker中的网络接口默认都是虚拟的接口。虚拟接口的最大优势就是转发效率极高。这是因为Linux通过在内核中进行数据复制来实现虚拟接口之间的数据转发,即发送接口的发送缓存中的数据包将被直接复制到接收接口的接收缓存中,而无须通过外部物理网络设备进行交换。

  • 本地系统和容器内系统来看,虚拟接口跟一个正常的以太网网卡相比并无区别,只是它速度要快得多。

  • Docker的容器网络就很好地利用了Linux虚拟网络技术

    • Docker启动时会在主机上自动创建一个docker0虚拟网桥,实际上是一个Linux网桥,可以理解为一个软件交换机,它会在挂载其上的接口之间进行数据转发。 同时Docker随机分配一个本地未占用的私有网段(在RFC1918中定义)中的一个地址给docker0接口

    • 当创建一个Docker容器时,同时会创建一对veth pair接口当数据包发送到一个接口时,另外一个接口也可以收到相同的数据包

    • 这对接口一端在容器内,即eth0,另一端在本地并被挂载到docker0网桥
      名称以veth开头,通过这种方式,主机可以跟容器通讯,容器之间也可以相互通信。
      如此一来,Docker就创建了在主机和所有容器之间的一个虚拟共享网络。

网络创建过程

  • 在使用docker run命令启动容器的时候,可以通过 --net 参数来指定容器的网络配置:
    • --net=bridge :默认值,在Docker网桥docker0上位容器创建新的网络栈。(网桥docker0相当于一台交换机,每个容器相当于一台电脑,各个容器通过交换机连接在一起)
    • --net=container:NAME_or_ID :让Docker将新建容器的进程放到一个已存在的容器的网络栈中,新容器进程有自己的文件系统、进程列表和资源列表,但会和已存在的容器共享IP地址和端口等网络资源,两者进程可以直接通过lo环回接口通信。
    • --net=host :告诉Docker不要将容器网络放到隔离的命名空间中,即不要容器化容器内的网络。此时容器使用本地主机的网络,它拥有完全的本地主机接口访问权限。这样做存在很大的安全风险。
    • --net=none :让Docker将新容器放到隔离的网络栈中,但是不进行网络配置。之后用户可以自行进行配置。
    • --net=user_defined_network :用户先自行用network相关命令创建一个网络,然后通过这种方式将容器链接到指定的已创建网络上。

手动配置网络

  • 下面是Docker网络相关的参数命令,其中有些命令选项只有在Docker服务启动的时候才能配置,而且不能马上生效。

    1
    2
    3
    4
    5
    6
    7
    -b BRIDGE or --bridge=BRIDGE 指定容器挂载的网桥
    --bip=CIDR 定制docker0的掩码
    -H SOCKET ... or --host=SOCKET... docker服务端接收命令的通道
    --icc=true|false 是否支持容器之间进行通信
    --ip-forward=true|false 启用net.ipv4.ip_forward
    --iptables=true|false 禁止Docker添加iptables规则
    --mtu=BYTRS 容器网络中的MTU
  • 下面两个命令既可以在启动服务时指定,也可以Docker容器启动时(docker run)指定。
    在docker服务启动的时候指定则会成为默认值,后续执行docker run时可以覆盖设置的默认值。

    1
    2
    --dns=IP_ADDRESS      使用指定的DNS服务器
    --dns=search=DOMAIN 指定DNS搜索域
  • 最后这些选项只能在docker run执行时使用,因为他是针对容器的特性内容:

    1
    2
    3
    4
    5
    h HOSTNAME or --hostname=HOSTNAME    配置容器主机名
    --link=CONTAINER_NAME:ALIAS 添加到另一个容器的连接
    -net=bridge|none|container:NAME_or_ID|host|user_defined_network 配置容器的桥接模式
    -p SPEC or --publish=SPEC 映射容器端口到宿主主机
    -P or --publish-all=true|false 映射容器所有端口到宿主主机
    • 其中 –net选项支持五种模式,如下所示:
      1. –net=bridge 默认选项,为容器创建一个独立的网络命名空间,分配网卡、ip地址等网络配置。并通过veth接口对将容器挂载到一个虚拟网桥(默认为docker0)上。
      2. –net=none 为容器创建一个独立的网络命名空间,但不进行网络配置,即容器内没有创建网卡、IP等
      3. –net=container:NAME_or_ID 意味着新创建的容器共享指定的已存在容器的网络命名空间,两个容器内的网络配置共享,但其它资源(进程空间、文件系统等)还是隔离的。
      4. –net=host 意味着不为容器创建独立的网络命名空间,容器内看到的网络配置均与主机保持一致。
      5. –net=user_defined_network 用户自行用network相关命令创建一个网络,同一个网络内的容器彼此可见,可以采用更多类型的网络插件。

配置容器DNC和主机名

  • Docker支持自定义容器的主机名和DNS配置。

  • 实际上,容器中主机名和DNS配置信息都是通过三个系统配置文件来维护的:**/etc/resolv.conf/etc/hostname/etc/hosts**。

  • 启动一个容器,在容器中使用mount命令可以看到这三个文件的挂载信息。

    • 其中,/etc/resolv.conf文件在创建容器的时候,默认会与宿主机/etc/resolv.conf文件内容保持一致。

  • 容器内修改配置文件

    • Docker1.2.0开始支持在运行中的容器里直接编辑/etc/hosts、/etc/hostname和/etc/resolv.conf文件。
    • 但是这些修改只是临时的,只是在运行的容器中保留,容器终止或重启后并不会被保存下来。也不会被docker commit提交。
  • 通过参数指定

    • 如果用户想要自定义容器的配置,可以在创建或启动容器的时候利用下面的参数指定。

    • 指定主机名: 这个主机名只有在容器内才能看到,在容器外看不到,既不会在docker ps中显示,也不会在其它容器的/etc/hosts中看到。

      1
      2
      3
      4
      # 设定容器的主机名,它会被写到容器内的/etc/hosts和/etc/hostname文件中。
      -h HOSTNAME
      # 或
      --hostname=HOSTNAME。
    • 记录其它容器主机名: 在创建容器的时候,添加一个所连接容器的主机名到容器内/etc/hosts文件中。这样新创建容器可以直接使用主机名来与所连接容器通信。

      1
      --link=CONTAINER_NAME:ALLAS
    • 指定DNS服务器: 添加DNS服务器到容器的/etc/resolv.con中,容器会用指定的服务器来解析所有不在/etc/hosts中的主机名。

      1
      --dns=IP_ADDRESS
    • 指定DNS搜索域: 设定容器的搜索域,当设定搜索域为.example.com时,在搜索一个名为host的主机时,DNS不仅搜索host,还会搜索host.example.com。

      1
      --dns-search=DOMAIN。

容器访问控制

  • 容器的访问控制主要通过Linux上的iptables防火墙软件来进行管理和实现。
  • iptables是系统流行的防火墙软件,在大部分发行版本中都会自带。

容器访问外部网络

  • 容器默认指定了网关为docker0网桥上的docker0内部接口。

  • docker0内部接口也是宿主机的一个本地接口,因此默认情况下是可以访问到宿主机本地的。

  • 容器想要通过宿主机访问到外部网络,需要宿主机进行转发。在Linux系统中,检查转发是否打开(默认是打开的):

    1
    2
    3
    4
    5
    6
    [root@042cfbfd8647 /]# sysctl net.ipv4.ip_forward
    net.ipv4.ip_forward = 1
    # 如果为0,说明没有开启转发,则需要手动打开:
    sysctl -w net.ipv4.ip_forward=
    # 简单的,在启动Docker服务的时候设定,docker服务会自动打开宿主机系统的转发服务。
    ip-forward=true

容器之间的访问

  • 容器之间相互访问,需要两方面的支持:

    • 网络拓朴是否已经连通。默认情况下,所有容器都会连接到docker0网桥上,这意味着默认情况下拓扑是互通的;
    • 本地系统的防火墙软件iptables是否允许访问通过。这取决于防火墙的默认规则是允许还是禁止。
  • 访问所有端口

    • 当启动Docker服务的时候,默认会添加一条“允许”转发策略到iptables的FORWARD链上。通过配置--icc=true|false(默认为true)参数可以控制默认的策略。
    • 为了安全考虑,可以在Docker配置文件中配置DOCKER_OPTS=--icc=false来默认禁止容器之间的相互访问。
    • 同时,如果启动Docker服务时手动指定--iptables=false参数则不会修改宿主机系统上的iptables规则。
  • 访问指定端口

    • 在通过--icc=false禁止容器间相互访问后,仍可以通过--link=CONTAINER_NAME:ALIAS选项来允许访问指定容器的开放端口。
    • --link=CONTAINER_NAME:ALIAS中的CONTAINER_NAME必须是docker自动分配的主机名或使用–name指定的主机名,不能使用-h参数配置的主机名。

映射容器端口到宿主机

  • 默认情况下,容器可以主动访问到外部网络的连接,但是外部网络无法访问到容器。

容器访问外部实现

  • 假设容器内部的网络地址为172.12.0.2,本地网络地址为10.0.2.2。

  • 容器要能访问外部网络,源地址不能为172.12.0.2,需要进行源地址映射(Source NAT,SNAT),
    修改为本地系统的IP地址10.0.2.2。

  • 映射是通过iptables的源地址伪装操作实现的。

  • 查看主机nat表上POSTROUTING链的规则。该链负责网包离开主机之前,还写其源地址。规则将所有源地址为172.12.0.0/16网段,而不是从docker0接口发出的流量,动态伪装为系统网卡发出。

    1
    iptables -t nat -nvL POSTROUTING

外部访问容器实现

  • 容器允许外部访问,可以在docker run时候通过-p或-P参数来启用。
  • 不管用那种方法,其实也是在本地的iptable的nat表中添加相应的规则,将访问外部IP地址的网包进行目标地址DANT,将目标地址修改为容器的IP地址
  • 以一个开放的8080端口为例,使用-P会自动映射本地的一个随即端口到容器的8080端口。

安全防护与配置

Docker是基于Linux操作系统实现的应用虚拟化。运行在容器内的进程,跟运行在本地系统中的进程本质上并无区别,配置不合适的安全策略将可能给本地系统带来安全风险。

命名空间隔离的安全

  • 当用户用docker run命令启动一个容器时,Docker将在后台为容器创建一个独立的命名空间。命名空间提供了最基础也是最直接的隔离,在容器中运行的进程不会被运行在本地主机上的进程和其他容器通过正常渠道发现和影响。

  • 与虚拟机方式相比,通过命名空间来实现的隔离并不是那么绝对。运行在容器中的应用可以直接访问系统内核和部分系统文件。因此,用户必须保证容器中应用是安全可信的(这跟保证运行在系统中的软件是可信的一样),否则本地系统将可能受到威胁。总之,必须保证镜像的来源和自身可靠

  • Docker自1.3.0版本起对镜像管理引入了签名系统,加强了对镜像安全性的防护。用户可以通过签名来验证镜像的完整性和正确性。

控制组资源控制的安全

  • 当用户执行docker run命令启动一个Docker容器时,Docker将通过Linux相关的调用,在后台为容器创建一个独立的控制组策略集合,该集合将限制容器内应用对资源的消耗。

  • 控制组提供了很多有用的特性。它确保各个容器可以公平地分享主机的内存、CPU、磁盘IO等资源。当然,更重要的是,通过控制组,可以限制容器对资源的占用,确保了当某个容器对资源消耗过大时,不会影响到本地主机系统和其他容器。

  • 尽管控制组不负责隔离容器之间相互访问、处理数据和进程,但是它在防止恶意攻击、特别是DDoS方面是是否有效的。

  • 对于支持多用户的服务平台(比如公有的各种Paas、容器云),控制组尤其重要。例如,当个别应用容器出现异常时,可以保证本地系统和其他容器正常运行而不受影响,从而避免引发“雪崩”灾难。

内核的能力机制

  • 能力机制(Capability)是Linux内核一个强大的特性,可以提供细粒度的权限访问控制。

  • 以前Linux系统对进程权限只有根权限和非根权限两种粗粒度的区别。Linux内核自2.2版本起支持能力机制,它将权限划分为更加细粒度的操作能力,既可以作用在进程上,也可以作用在文件上。

  • 默认情况下,Docker启动的容器被严格限制只允许使用内核的一部分能力。用户可以根据自生需求来为Docker容器启动额外的权限。

Docker服务端的防护

  • 使用Docker容器的核心是Docker服务端。Docker服务的运行目前还需要root权限的支持,因此服务端安全性十分关键。

  • 首先,必须确保只有可信的用户才可以访问到Docker服务。Docker允许用户在主机和容器间共享文件夹,同时不需要限制容器的访问权限,这就容易让容器突破资源限制。

  • 容器中的进程和宿主机器上的其进程是没有区别的,他们使用的是同一个内核,受同一个内核管理权限,如果说容器中的进程是root权限,那么也就意味这这个进程在宿主机器上其实也是root权限。如果资源被挂载到容器中,那么意味着容器中的进程能够获取,修改,删除这些资源,因为它是root权限。

    • 例如,恶意用户可以在启动容器时将本地根目录挂载到容器中,那么就可以在容器中任意修改主机内容了。因此,当提供容器创建服务时(例如通过一个Web服务器),要注意参数的安全检查,防止恶意用户用特定的参数来创建一些有破坏性的容器。
  • 用户可以通过基于HTTP的REST API来访问Docker服务端。建议使用安全机制,确保只有可信的网络或VPN网络,或证书保护机制下的访问可以进行。

  • 最近改进的Linux命名空间机制将可以实现使用非root用户来运行全功能的容器。

  • 目前,Docker滋生改进安全防护的目标是实现以下两个特性:

    • 将容器的root用户映射到本地主机上的非root用户。
    • 允许Docker服务端在非root权限下运行。
  • 参考:你不应该在容器中使用root用户

  • 如果你要创建一个镜像,那么你就应该在Dockerfile中创建一个默认的用户,而不是使用默认的root。这样更加容器控制权限。我们举一个root容器的例子,看一下他的危害:

  • 在宿主机器的root目录下创建一个隐私文件,我使用root用户创建,只有root权限的用户才能访问这个文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    root@srv:~# cd /root
    root@srv:~# echo "top secret stuff" >> ./secrets.txt
    root@srv:~# chmod 0600 secrets.txt
    root@srv:/root# ls -l
    total 4
    -rw------- 1 root root 17 Sep 26 20:29 secrets.txt

    # 使用非root用户访问
    marc@srv:~$ cat /root/secrets.txt
    cat: /root/secrets.txt: Permission denied
  • 退出root,使用一个普通的用户来创建一个Dockerfile:

    1
    2
    FROM debian:stretch
    CMD ["cat", "/tmp/secrets.txt"]
  • 然后编译运行这个镜像,并且将我们的隐私文件挂载进去:

    1
    marc@srv:~$ docker run -v /root/secrets.txt:/tmp/secrets.txt <img>
  • 即便我在宿主机器上的容器是marc,但是在容器中,我已经拥有root权限了,能够访问宿主机上root权限才能访问的资源文件。这也就是说任何人在DockerHub上下载这镜像,都会暴露自己的特权文件(当然,还取决于你怎么它)。

  • 建议在创建Dockerfile的时候创建一个普通用户,并且指定uid和gid。使用这个普通用户来启动容器。

    1
    2
    3
    4
    5
    FROM debian:stretch
    RUN groupadd -g 999 appuser && \
    useradd -r -u 999 -g appuser appuser
    USER appuser
    CMD ["cat", "/tmp/secrets.txt"]
  • 再次上之前那样运行容器

    1
    2
    marc@srv:~$ docker run -v /root/secrets.txt:/tmp/secrets.txt <img>
    cat: /tmp/secrets.txt: Permission denied

Docker Compose

简介

  • Compose是Docker官方的开源项目,用于实现对Docker容器集群的快速编排。

  • 我们已经知道,使用一个Dockerfile模板文件,可以很方便地定义一个单独的应用容器。然而,在日常工作中,经常会碰到需要多个容器互相配合来完成某项任务的情况。

  • 例如要实现一个Web项目,除了Web服务容器本身,还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。

    • Compose恰好满足了这样的需求。它允许用户通过一个单独的docker-compose.yml模板文件(yaml格式)来定义一组相关联的应用容器为一个项目(project)。
  • Compose中有两个重要的概念:

    • 服务(service):一个应用容器,实际上可以包括若干运行相同镜像的容器实例。
    • 项目(project):由一组关联的应用容器组成的一个完整业务单元。
  • Compose的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷的生命周期管理。

  • 使用docker-compose的基础是docker,image和container,像积木玩具中的一个个单元一样。简单来说就是一个容器组装的工具,能将image和container组装成我们期待的样子

  • Compose项目是由Python编写的,实际上调用了Docker服务提供的API来对容器进行管理。

docker-compose安装