容器化技术 - docker - 1

简介

docker是什么:

  Docker 最初是 dotCloud 公司创始人 Solomon Hykes 在法国期间发起的一个公司内部项目, 它是基于 dotCloud 公司多年云服务技术的一次革新, 并于 2013 年 3 月以 Apache 2.0 授权协议开源, 主要项目代码在 GitHub 上进行维护. Docker 项目后来还加入了 Linux 基金会, 并成立推动 开放容器联盟(OCI).

  Docker 自开源后受到广泛的关注和讨论, 至今其 GitHub 项目已经超过 4 万 6 千个星标和一万多个 fork. 甚至由于 Docker 项目的火爆, 在 2013 年底, dotCloud 公司决定改名为 Docker. Docker 最初是在 Ubuntu 12.04 上开发实现的;Red Hat 则从 RHEL 6.5 开始对 Docker 进行支持;Google 也在其 PaaS 产品中广泛应用 Docker.

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

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

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

docker_1

docker_2

为什么要使用 docker

作为一种新兴的虚拟化方式, Docker 跟传统的虚拟化方式相比具有众多的优势.

更高效的利用系统资源

  由于容器不需要进行硬件虚拟以及运行完整操作系统等额外开销, Docker 对系统资源的利用率更高. 无论是应用执行速度、内存损耗或者文件存储速度, 都要比传统虚拟机技术更高效. 因此, 相比虚拟机技术, 一个相同配置的主机, 往往可以运行更多数量的应用.

更快速的启动时间

  传统的虚拟机技术启动应用服务往往需要数分钟, 而 Docker 容器应用, 由于直接运行于宿主内核, 无需启动完整的操作系统, 因此可以做到秒级、甚至毫秒级的启动时间. 大大的节约了开发、测试、部署的时间.

一致的运行环境

  开发过程中一个常见的问题是环境一致性问题. 由于开发环境、测试环境、生产环境不一致, 导致有些 bug 并未在开发过程中被发现. 而 Docker 的镜像提供了除内核外完整的运行时环境, 确保了应用运行环境一致性, 从而不会再出现 「这段代码在我机器上没问题啊」 这类问题.

持续交付和部署

  对开发和运维(DevOps)人员来说, 最希望的就是一次创建或配置, 可以在任意地方正常运行.

  使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署. 开发人员可以通过 Dockerfile 来进行镜像构建, 并结合 持续集成(Continuous Integration) 系统进行集成测试, 而运维人员则可以直接在生产环境中快速部署该镜像, 甚至结合 持续部署(Continuous Delivery/Deployment) 系统进行自动部署.

  而且使用 Dockerfile 使镜像构建透明化, 不仅仅开发团队可以理解应用运行环境, 也方便运维团队理解应用运行所需条件, 帮助更好的生产环境中部署该镜像.

更轻松的迁移

  由于 Docker 确保了执行环境的一致性, 使得应用的迁移更加容易. Docker 可以在很多平台上运行, 无论是物理机、虚拟机、公有云、私有云, 甚至是笔记本, 其运行结果是一致的. 因此用户可以很轻易的将在一个平台上运行的应用, 迁移到另一个平台上, 而不用担心运行环境的变化导致应用无法正常运行的情况.

更轻松的维护和扩展

  Docker 使用的分层存储以及镜像的技术, 使得应用重复部分的复用更为容易, 也使得应用的维护更新更加简单, 基于基础镜像进一步扩展镜像也变得非常简单. 此外, Docker 团队同各个开源项目团队一起维护了一大批高质量的 官方镜像, 既可以直接在生产环境使用, 又可以作为基础进一步定制, 大大的降低了应用服务的镜像制作成本.

对比传统虚拟机

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

docker 基本概念

  Docker 包括三个基本概念

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

  理解了这三个概念, 就理解了 Docker 的整个生命周期.

docker 引擎

  Docker 引擎是一个包含一下主要组件的客户端服务器应用程序.

  • 一种服务器, 它是一种成为守护进程并长时间运行的程序.
  • REST API 用于指定程序可以用来与守护进程通信的接口, 并指示它做什么.
  • 一个有命令行界面(CLI)工具的客户端.

Docker 引擎组件的流程如下图所示:

docker_3

docker 系统架构

  Docker 使用客户端-服务器(C/S)架构模式, 使用远程API来管理和创建Docker容器.

  Docker 容器通过 Docker 镜像来创建.

  容器与镜像的关系类似于面向对象编程中的对象与类. 容器 - 对象 ; 镜像 - 类 .

docker_4

标题 说明
镜像(Images) Docker 镜像是用于创建 Docker 容器的模板.
容器(Container) 容器是独立运行的一个或一组应用.
客户端(Client) Docker 客户端通过命令行或者其他工具使用 Docker API (https://docs.docker.com/reference/api/docker_remote_api) 与 Docker 的守护进程通信.
主机(Host) 一个物理或者虚拟的机器用于执行 Docker 守护进程和容器.
仓库(Registry) Docker 仓库用来保存镜像, 可以理解为代码控制中的代码仓库. Docker Hub(https://hub.docker.com) 提供了庞大的镜像集合供使用.
Docker Machine Docker Machine是一个简化Docker安装的命令行工具, 通过一个简单的命令行即可在相应的平台上安装Docker, 比如VirtualBox、 Digital Ocean、Microsoft Azure.

docker 镜像

   操作系统分为内核和用户空间. 对于 Linux 而言, 内核启动后, 会挂在 root 文件系统为其提供用户空间支持. 而 docker 镜像(Image), 就相当于是一个 root 文件系统. 比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu 16.04 最小系统的 root 文件系统.

  Docker 镜像是一个特殊的文件系统, 除了提供容器运行时所需的程序、库、资源配置等文件外, 还包含了一些为运行时准备的一些配置参数(匿名卷、环境变量、用户等等). 镜像不包含任何动态数据, 其内容在构建之后也不会被改变.

分层存储

  因为镜像包含操作系统完整的 root 文件系统, 其体积往往是庞大的, 因为在 Docker 设计时, 就充分利用 Union FS 的技术, 将其设计为分层存储的架构. 所以严格来说, 镜像并非是想一个 ISO 那样的打包文件, 镜像只是一个虚拟的概念, 其实际体现并非由一个文件组成, 而是由一组文件系统组成, 或者说, 由多层文件系统联合组成.

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

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

docker容器

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

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

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

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

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

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

docker 仓库

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

  一个 Docker Registry 中可以包含多个仓库 (Repository) ; 每个仓库可以包含多个标签 (Tag) ; 每个标签对应一个镜像.

  通常, 一个仓库会包含同一个软件不同版本的镜像, 而标签就常用于对应该软件的各个版本. 我们可以通过<仓库名>:<标签>的格式来指定具体是这个软件哪个版本的镜像. 如果不给出标签, 将以 latest 作为默认标签.

  以Ubuntu镜像为例, ubuntn 是仓库的名字, 其内包含有不同的版本标签, 如: 14.06, 16.04. 我们可以通过 ubuntu:14.04, 或者 ubuntu:16.04 来具体指定所需哪个版本的镜像. 如果忽略了标签, 比如 ubuntu, 那将视为 ubuntu:latest.

  仓库名经常以两段式路径形式出现, 比如jwilder/nginx-proxy, 前者往往意味着 Docker Registry 多用户环境下的用户名, 后者则往往是对应的软件名. 但这并非绝对. 取决于所使用的具体 Docker Registry 的软件或服务.

公有 Docker Registry

  Docker Registry 公开服务是开放给用户使用、允许用户管理镜像的 Registry 服务. 一般这类公开服务允许用户免费上传、下载公开的镜像, 并可能提供收费服务供用户管理私有镜像.

  最常使用的 Registry 公开服务是官方的 Docker Hub , 这也是默认的 Registry, 并拥有大量的高质量的官方镜像. 除此以外, 还有 CoreOS 的 Quay.io, CoreOS 相关的镜像存储在这里; Google 的 Google Container Registry, Kubernetes 的镜像使用的就是这个服务.

  由于某些原因, 在国内访问这些服务可能会比较慢. 国内的一些云服务商提供了针对 Docker Hub 的镜像服务 (Registry Mirror) , 这些镜像服务被称为加速器. 常见的有 阿里云加速器、DaoCloud 加速器 等. 使用加速器会直接从国内的地址下载 Docker Hub 的镜像, 比直接从 Docker Hub 下载速度会提高很多.

国内也有一些云服务商提供类似于 Docker Hub 的公开服务. 比如 时速云镜像仓库、网易云镜像服务、DaoCloud 镜像市场、阿里云镜像库 等.

P.S. 亲测直接用官方加速器也不会慢很多.

私有 Docker Registry

  除了使用公开服务外, 用户还可以在本地搭建私有 Docker Registry. Docker 官方提供了 Docker Registry 镜像, 可以直接使用做为私有 Registry 服务.

  开源的 Docker Registry 镜像只提供了 Docker Registry API 的服务端实现, 足以支持 docker 命令, 不影响使用. 但不包含图形界面, 以及镜像维护、用户管理、访问控制等高级功能. 在官方的商业化版本 Docker Trusted Registry 中, 提供了这些高级功能.

  除了官方的 Docker Registry 外, 还有第三方软件实现了 Docker Registry API, 甚至提供了用户界面以及一些高级功能. 比如 VMWare Harbor 和 Sonatype Nexus.

Docker 安装

  Docker 在 1.13 版本之后, 从 2017 年的 3 月 1 日开始, 版本命名规则变为如下:

项目 说明
版本格式 YY.MM
Stable 版本 每个季度发行
Edge 版本 每个月发行
  同时 Docker 划分为 CE 和 EE. CE 即社区版 (免费, 支持周期三个月) , EE 即企业版, 强调安全, 付费使用.

  Docker CE 每月发布一个 Edge 版本 (17.03, 17.04, 17.05...), 每三个月发布一个 Stable 版本 (17.03, 17.06, 17.09...), Docker EE 和 Stable 版本号保持一致, 但每个版本提供一年维护.

  官方网站上有各种环境下的安装指南, 这里主要介绍 Docker CE 在 Linux 、Windows 10 (PC) 上的安装.

centOS 安装 docker

系统要求

  • Docker CE 支持 64 位版本 CentOS 7
  • 内核版本不低于 3.10

  CentOS 7 满足最低内核的要求, 但由于内核版本比较低, 部分功能 (如 overlay2 存储层驱动) 无法使用, 并且部分功能可能不太稳定.

卸载旧版本

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

sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-selinux \
                  docker-engine-selinux \
                  docker-engine

第一种方式: 使用 yum 安装

  执行以下命令安装依赖包:

sudo yum install -y yum-utils \
           device-mapper-persistent-data lvm2

  鉴于国内网络问题, 强烈建议使用国内源, 官方源请在注释中查看.

  执行下面的命令添加 yum 软件源:

sudo yum-config-manager \
    --add-repo \
    https://mirrors.ustc.edu.cn/docker-ce/linux/centos/docker-ce.repo

# 官方源
# sudo yum-config-manager \
#   --add-repo \
#   https://download.docker.com/linux/centos/docker-ce.repo

设置 Docker CE版本:

  如果需要最新版本的 Docker CE 请使用以下命令:

sudo yum-config-manager --enable docker-ce-edge

  如果需要测试版本的 Docker CE 请使用以下命令:

sudo yum-config-manager --enable docker-ce-test

  更新 yum 软件源缓存, 并安装 docker-ce:

sudo yum makecache fast

sudo yum install docker-ce

第二种方式: 使用脚本自动安装

  在测试或开发环境中 Docker 官方为了简化安装流程, 提供了一套便捷的安装脚本, CentOS 系统上可以使用这套脚本安装:

curl -fsSL get.docker.com -o get-docker.sh

sudo sh get-docker.sh --mirror Aliyun

  执行这两个命令后, 脚本就会自动的将一切准备工作做好, 并且把 Docker CE 的 Edge 版本安装在系统中.

第三种方式: 完全离线安装

  与第一种方式类似, 先在类似的环境下执行命令,获取所有需要安装的环境依赖及docker-ce的安装包.

yum install -y yum-utils device-mapper-persistent-data \
lvm2 --downloadonly --downloaddir=./

yum-config-manager     --add-repo     https://mirrors.ustc.edu.cn/docker-ce/linux/centos/docker-ce.repo

yum-config-manager --enable docker-ce-edge

yum makecache fast

yum install docker-ce --downloadonly --downloaddir=./

  离线安装过程中,经常会遇到循环依赖的问题,可以选择全部强制安装.

rpm -ivh ./initfile/npmfile/audit-2.8.5-4.el7.x86_64.rpm --nodeps
rpm -ivh ./initfile/npmfile/audit-libs-2.8.5-4.el7.x86_64.rpm --nodeps
rpm -ivh ./initfile/npmfile/audit-libs-python-2.8.5-4.el7.x86_64.rpm --nodeps
rpm -ivh ./initfile/npmfile/checkpolicy-2.5-8.el7.x86_64.rpm --nodeps
rpm -ivh ./initfile/npmfile/containerd.io-1.3.7-3.1.el7.x86_64.rpm --nodeps
rpm -ivh ./initfile/npmfile/container-selinux-2.119.2-1.911c772.el7_8.noarch.rpm --nodeps
rpm -ivh ./initfile/npmfile/device-mapper-1.02.164-7.el7_8.2.x86_64.rpm --nodeps
......

启动 Docker CE

# 设置开机自启
sudo systemctl enable docker
# 启动docker
sudo systemctl start docker

建立 docker 用户组

  默认情况下, docker 命令会使用 Unix socket 与 Docker 引擎通讯. 而只有 root 用户和 docker 组的用户才可以访问 Docker 引擎的 Unix socket. 出于安全考虑, 一般 Linux 系统上不会直接使用 root 用户. 因此, 更好地做法是将需要使用 docker 的用户加入 docker 用户组.

  建立 docker 组:
sudo groupadd docker

  将当前用户加入 docker 组:
sudo usermod -aG docker $USER

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

测试 Docker 是否安装正确

# 查看docker信息
docker info
# 从线上库拉取并执行hello-world容器
docker run hello-world

docker_5

  若能正常输出以上信息, 则说明安装成功.

镜像加速

  鉴于国内网络问题, 如果后续拉取 Docker 镜像十分缓慢, 建议安装 Docker 之后配置 国内镜像加速.

# 默认是没有这个文件的
vim /etc/docker/daemon.json

# 文件中添加以下内容(docker官方加速)
{
  "registry-mirrors": ["https://registry.docker-cn.com"]
}

# 保存配置, 重启docker
systemctl daemon-reload
systemctl restart docker

  重新利用docker info命令查看docker信息可以看到配置的加速已经生效了.

docker_6

添加内核参数

  默认配置下, 如果在 CentOS 使用 Docker CE 看到下面的这些警告信息:

WARNING: bridge-nf-call-iptables is disabled
WARNING: bridge-nf-call-ip6tables is disabled

  请添加内核配置参数以启用这些功能.

# 编辑sysctl.conf
vim /etc/sysctl.conf

# 添加下面两行参数配置
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1

# 重新加载 sysctl.conf
sysctl -p

参考文档

Docker 官方 CentOS 安装文档

windos 安装 docker

系统要求

  Docker for Windows 支持 64 位版本的 Windows 10 Pro, 且必须开启 Hyper-V.

开启Hyper-V

如果是专业版win10:

  直接去控制面板中开启Hyper-V:

docker_7

如果是家庭版win10:

  新建hyperv.cmd文件, 内容如下:

pushd "%~dp0"
dir /b %SystemRoot%\servicing\Packages\*Hyper-V*.mum >hyper-v.txt
for /f %%i in ('findstr /i . hyper-v.txt 2^>nul') do dism /online /norestart /add-package:"%SystemRoot%\servicing\Packages\%%i"
del hyper-v.txt
Dism /online /enable-feature /featurename:Microsoft-Hyper-V-All /LimitAccess /ALL

  以管理员身份执行hyperv.cmd文件.

  等一会儿执行完后,系统会让重启服务器, 直接重启.

  重启后就可以在控制面板 -> 程序 -> 启用或关闭Windows功能 中开启Hyper-V.

docker_9

伪装成win10专业版

  以管理员身份打开cmd.
执行如下命令:

REG ADD "HKEY_LOCAL_MACHINE\software\Microsoft\Windows NT\CurrentVersion" /v EditionId /T REG_EXPAND_SZ /d Professional /F

安装Docker

  点击以下链接下载 Stable 或 Edge 版本的 Docker for Windows.

https://store.docker.com/editions/community/docker-ce-desktop-windows

  下载好之后双击 Docker for Windows Installer.exe 开始安装.

运行

  在 Windows 搜索栏输入 Docker 点击 Docker for Windows 开始运行.

  可以在cmd / powerShell 中直接执行docker命令.

P.S. 会默认安装在C盘,记得更改docker images本地存储位置

docker_10

配置镜像加速

  单击右下角图标选择settings,之后配置就行.

docker_11

docker 镜像

  Docker 运行容器前需要本地存在对应的镜像, 如果本地不存在该镜像, Docker 会从镜像仓库下载该镜像.

Docker 获取镜像

  Docker Hub 上有大量的高质量的镜像可以用, 从 Docker 镜像仓库获取镜像的命令是 docker pull.

  其命令格式为:

docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]

  具体的选项可以通过 docker pull --help 命令看到, 这里说一下镜像名称的格式.

  • Docker 镜像仓库地址: 地址的格式一般是 <域名/IP>[:端口号]. 默认地址是 Docker Hub.
  • 仓库名: 如之前所说, 这里的仓库名是两段式名称, 即 <用户名>/<软件名>. 对于 Docker Hub, 如果不给出用户名, 则默认为 library, 也就是官方镜像.

  比如: docker pull ubuntu:16.04

docker_12

  上面的命令中没有给出 Docker 镜像仓库地址, 因此将会从 Docker Hub 获取镜像. 而镜像名称是 ubuntu:16.04, 因此将会获取官方镜像 library/ubuntu 仓库中标签为 16.04 的镜像.

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

  在使用上面命令的时候, 你可能会发现, 你所看到的层 ID 以及 sha256 的摘要和这里的不一样. 这是因为官方镜像是一直在维护的, 有任何新的 bug, 或者版本更新, 都会进行修复再以原来的标签发布, 这样可以确保任何使用这个标签的用户可以获得更安全、更稳定的镜像.

测试运行

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

docker run -it --rm  ubuntu:16.04  bash

docker_13

docker run就是运行容器的命令, 这里简要的说明一下上面用到的参数:

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

最后通过键入exit, 退出容器.

docker 列出镜像

  要想列出已经下载下来的镜像, 可以使用docker image ls命令查看:

REPOSITORY(仓库名) TAG(标签) IMAGE ID(镜像ID) CREATED(创建时间) SIZE(占用空间)
ubuntu 16.04 096efd74bb89 3 days ago 127MB
goharbor/redis-photon v2.1.0 45fa455a8eeb 13 days ago 68.7MB
goharbor/harbor-registryctl v2.1.0 98f466a61ebb 13 days ago 132MB
goharbor/registry-photon v2.1.0 09c818fabdd3 13 days ago 80.1MB
goharbor/nginx-photon v2.1.0 470ffa4a837e 13 days ago 40.1MB
goharbor/harbor-log v2.1.0 402802990707 13 days ago 82.1MB
goharbor/harbor-jobservice v2.1.0 ff65bef832b4 13 days ago 165MB
goharbor/harbor-core v2.1.0 26047bcb9ff5 13 days ago 147MB
goharbor/harbor-portal v2.1.0 5e97d5e230b9 13 days ago 49.5MB
goharbor/harbor-db v2.1.0 44c0be92f223 13 days ago 164MB
goharbor/prepare v2.1.0 58d0e7cee8cf 13 days ago 160MB
192.168.2.72/tomcat/tomcat 8-jdk8 d5ef56581444 13 days ago 530MB

镜像体积

  如果仔细观察, 会注意到, 这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同. 比如, ubuntu:16.04 镜像大小, 在这里是 127 MB, 但是在 Docker Hub 显示的却是 50 MB. 这是因为 Docker Hub 中显示的体积是压缩后的体积. 在镜像下载和上传过程中镜像是保持着压缩状态的, 因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小. 而 docker image ls 显示的是镜像下载到本地后, 展开的大小, 准确说, 是展开后的各层所占空间的总和, 因为镜像到本地后, 查看空间的时候, 更关心的是本地磁盘空间占用的大小.

  另外一个需要注意的问题是, docker image ls 列表中的镜像体积总和并非是所有镜像实际硬盘消耗. 由于 Docker 镜像是多层存储结构, 并且可以继承、复用, 因此不同镜像可能会因为使用相同的基础镜像, 从而拥有共同的层. 由于 Docker 使用 Union FS, 相同的层只需要保存一份即可, 因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多.

可以通过以下命令来便捷的查看镜像、容器、数据卷所占用的空间.

docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 14 11 1.761GB 817.1MB (46%)
Containers 11 1 9.707MB 9.706MB (99%)
Local Volumes 11 9 374.9MB 374.9MB (99%)
Build Cache 0 0 0B 0B

虚悬镜像

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

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

  这种镜像一般是被新版本镜像替换了名称, 或是docker build时由于新旧镜像同名, 旧镜像名称被取消, 从而出现仓库名、标签均为<none>.
  这类无标签镜像也被称为 虚悬镜像(dangling image) , 可以用下面的命令专门显示这类镜像:

docker image ls -f dangling=true

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

docker image prune

中间层镜像

  为了加速镜像构建、重复利用资源, Docker 会利用 中间层镜像. 所以在使用一段时间后, 可能会看到一些依赖的中间层镜像. 默认的 docker image ls 列表中只会显示顶层镜像, 如果希望显示包括中间层镜像在内的所有镜像的话, 需要加 -a 参数:

docker image ls -a

  这样会看到很多无标签的镜像, 与之前的虚悬镜像不同, 这些无标签的镜像很多都是中间层镜像, 是其它镜像所依赖的镜像. 这些无标签镜像不应该删除, 否则会导致上层镜像因为依赖丢失而出错. 实际上, 这些镜像也没必要删除, 因为之前说过, 相同的层只会存一遍, 而这些镜像是别的镜像的依赖, 因此并不会因为它们被列出来而多存了一份, 无论如何你也会需要它们. 只要删除那些依赖它们的镜像后, 这些依赖的中间层镜像也会被连带删除.

列出部分镜像

  不加任何参数的情况下, docker image ls会列出所有的顶级镜像, 但是有时候只需要列出部分镜像,docker image ls中的几个参数可以帮助做到这件事儿:

[docker@ptsjzx2 ~]$ docker image ls --help

Usage:	docker image ls [OPTIONS] [REPOSITORY[:TAG]]

List images

Aliases:
  ls, images, list

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

  例如:

  docker image ls ubuntu - 查询仓库名为ubuntu的镜像

  docker image ls ubuntu:16.04 - 查询具体到版本号的镜像

  docker image ls --digests - 展示完整id

删除本地镜像

  如果要删除本地的镜像, 可以使用docker image rm命令,格式如下:

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

用 ID、镜像名、摘要删除镜像

使用ID删除镜像:

  删除命令中的<镜像> 可以是 镜像短 ID、镜像长 ID、镜像名 或者 镜像摘要.

docker_14

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

使用景象名删除镜像:

  也可以用镜像名, 也就是 <仓库名>:<标签>, 来删除镜像.
eg: docker image rm ubuntu:16.04

使用景象摘要删除镜像:

  最精确的是使用镜像摘要删除镜像. 首先需要通过docker image ls --digests命令查询镜像的摘要.之后利用docker image rm [摘要]删除镜像.

Untagged 和 Deleted

  如果观察上面这几个命令的运行输出信息的话, 你会注意到删除行为分为两类, 一类是 Untagged, 另一类是 Deleted. 之前介绍过, 镜像的唯一标识是其 ID 和摘要, 而一个镜像可以有多个标签.

  因此当我们使用上面命令删除镜像的时候, 实际上是在要求删除某个标签的镜像. 所以首先需要做的是将满足我们要求的所有镜像标签都取消, 这就是我们看到的 Untagged 的信息. 因为一个镜像可以对应多个标签, 因此当我们删除了所指定的标签后, 可能还有别的标签指向了这个镜像, 如果是这种情况, 那么 Delete 行为就不会发生. 所以并非所有的 docker image rm 都会产生删除镜像的行为, 有可能仅仅是取消了某个标签而已.

  当该镜像所有的标签都被取消了, 该镜像很可能会失去了存在的意义, 因此会触发删除行为. 镜像是多层存储结构, 因此在删除的时候也是从上层向基础层方向依次进行判断删除. 镜像的多层结构让镜像复用变动非常容易, 因此很有可能某个其它镜像正依赖于当前镜像的某一层. 这种情况, 依旧不会触发删除该层的行为. 直到没有任何层依赖当前层时, 才会真实的删除当前层. 这就是为什么, 有时候会奇怪, 为什么明明没有别的标签指向这个镜像, 但是它还是存在的原因, 也是为什么有时候会发现所删除的层数和自己 docker pull 看到的层数不一样的源.

  除了镜像依赖以外, 还需要注意的是容器对镜像的依赖. 如果有用这个镜像启动的容器存在(即使容器没有运行), 那么同样不可以删除这个镜像. 之前讲过, 容器是以镜像为基础, 再加一层容器存储层, 组成这样的多层存储结构去运行的. 因此该镜像如果被这个容器所依赖的, 那么删除必然会导致故障. 如果这些容器是不需要的, 应该先将它们删除, 然后再来删除镜像.

用 docker image ls 命令来配合

  像其它可以承接多个实体的命令一样, 可以使用 docker image ls -q 来配合使用 docker image rm, 这样可以成批的删除希望删除的镜像. 我们在“镜像列表”章节介绍过很多过滤镜像列表的方式都可以拿过来使用.

  比如, 需要删除所有仓库名为redis的镜像:

docker image rm $(docker image ls -q redis)

  或者删除所有在 mongo:3.2 之前的镜像:

docker image rm $(docker image ls -q -f before=mongo:3.2)

  充分利用想象力和 Linux 命令行的强大可以完成很多非常赞的功能.

利用 commit 理解镜像构成

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

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

  镜像是多层存储, 每一层是在前一层的基础上进行的修改;而容器同样也是多层存储, 是在以镜像为基础层, 在其基础上加一层作为容器运行时的存储层.

  现在以定制一个 Web 服务器为例子, 来展示镜像是如何构建的.

  首先用 nginx 镜像启动一个容器, 命名为 nginx, 并且映射了 80 端口.

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

  启动成功后用浏览器访问服务器对应的ip地址, 就可以看到默认的 Nginx 欢迎页面.

docker_15

  现在如果需要改成欢迎 Docker 的文字, 我们可以使用 docker exec 命令进入容器, 修改其内容:

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

  这样就可以以交互式终端方式进入nginx容器, 并执行了 bash 命令, 也就是获得一个可操作的 Shell.

  然后, 用 <h1>Hello, Docker!</h1> 覆盖了 /usr/share/nginx/html/index.html 的内容.

  现在再刷新浏览器的话, 会发现内容被改变了.

docker_16

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

docker diff webserver
C /root
A /root/.bash_history
C /run
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
C /usr/share/nginx/html/index.html
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp

  现在做好了定制需求, 希望能将其保存下来形成镜像.

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

  docker commit 的语法格式为:

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

  可以用下面的命令将容器保存为镜像:

docker commit \
    --author "Test autor" \
    --message "修改了默认网页" \
    nginx \
    nginx:v2

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

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

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

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

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

  新的镜像定制好后, 可以来运行这个镜像.

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

  这里将定制nginx镜像启动的docker容器命名为 web2, 并且映射到 81 端口. 再访问http://[服务器ip]:81就可以看到更改后的效果.

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

慎用 docker commit

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

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

  此外, 使用 docker commit 意味着所有对镜像的操作都是黑箱操作, 生成的镜像也被称为黑箱镜像, 换句话说, 就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像, 别人根本无从得知. 而且, 即使是这个制作镜像的人, 过一段时间后也无法记清具体在操作的. 虽然 docker diff 或许可以告诉得到一些线索, 但是远远不到可以确保生成一致镜像的地步. 这种黑箱镜像的维护工作是非常痛苦的.

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

使用 Dockerfile 定制镜像

Dockerfile 定制镜像

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

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

  还以之前定制 nginx 镜像为例, 这次使用 Dockerfile 来定制.

  在一个空白目录中, 建立一个文本文件(touch Dockerfile), 并命名为 Dockerfile, 内容如下:

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

FROM 指定基础镜像

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

  在 Docker Store 上有非常多的高质量的官方镜像, 有可以直接拿来使用的服务类的镜像, 如 nginx、redis、mongo、mysql、httpd、php、tomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像, 如 node、openjdk、python、ruby、golang 等. 可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制.

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

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

FROM scratch
...

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

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

RUN 执行命令

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

shell 格式: RUN <命令>

  就像直接在命令行中输入的命令一样. 刚才写的 Dockerfile 中的 RUN 指令就是这种格式.

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

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

  比如这样:

FROM debian:jessie

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

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

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

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

  上面的 Dockerfile 正确的写法应该是这样:

FROM debian:jessie

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

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

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

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

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

构建镜像

  编辑好Dockerfile后, 就可以开始构建镜像了.在Dockerfile目录下, 执行:
docker build -t nginx:v3 .

[docker@ptsjzx2 test]$ docker build -t nginx:v3 .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM nginx
latest: Pulling from library/nginx
d121f8d1c412: Pull complete 
ebd81fc8c071: Pull complete 
655316c160af: Pull complete 
d15953c0e0f8: Pull complete 
2ee525c5c3cc: Pull complete 
Digest: sha256:c628b67d21744fce822d22fdcc0389f6bd763daac23a6b77147d0712ea7102d0
Status: Downloaded newer image for nginx:latest
 ---> 7e4d58f0e5f3
Step 2/2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in 9dd3850cc6ac
Removing intermediate container 9dd3850cc6ac
 ---> 18d7d26fa744
Successfully built 18d7d26fa744
Successfully tagged nginx:v3

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

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

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

镜像构建上下文(Context)

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

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

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

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

  如果在 Dockerfile 中这么写:
COPY ./package.json /app/
这并不是要复制执行docker build命令所在的目录下的 package.json, 也不是复制 Dockerfile 所在目录下的 package.json, 而是复制 上下文(context) 目录下的 package.json.

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

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

  如果观察 docker build 输出, 我们其实已经看到了这个发送上下文的过程:
docker build -t nginx:v3 .
理解构建上下文对于镜像构建是很重要的, 避免犯一些不应该的错误.

  比如有些初学者在发现 COPY /opt/xxxx /app 不工作后, 于是干脆将 Dockerfile 放到了硬盘根目录去构建, 结果发现 docker build 执行后, 在发送一个几十 GB 的东西, 极为缓慢而且很容易构建失败. 那是因为这种做法是在让 docker build 打包整个硬盘, 这显然是使用错误.

  一般来说, 应该会将 Dockerfile 置于一个空目录下, 或者项目根目录下. 如果该目录下没有所需文件, 那么应该把所需文件复制一份过来. 如果目录下有些东西确实不希望构建时传给 Docker 引擎, 那么可以用 .gitignore 一样的语法写一个 .dockerignore, 该文件是用于剔除不需要作为上下文传递给 Docker 引擎的.

  那么为什么会有人误以为 . 是指定 Dockerfile 所在目录呢?这是因为在默认情况下, 如果不额外指定 Dockerfile 的话, 会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile.

  这只是默认行为, 实际上 Dockerfile 的文件名并不要求必须为 Dockerfile, 而且并不要求必须位于上下文目录中, 比如可以用 -f ../Dockerfile.php 参数指定某个文件作为 Dockerfile.

  当然, 一般大家习惯性的会使用默认的文件名 Dockerfile, 以及会将其置于镜像构建上下文目录中.

其它 docker build 的用法

直接用 Git repo 进行构建

  docker build 还支持从 URL 构建, 比如可以直接从 Git repo 中构建:

docker build https://github.com/twang2218/gitlab-ce-zh.git#:8.14

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

用给定的 tar 压缩包构建

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

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

从标准输入中读取 Dockerfile 进行构建

docker build - < Dockerfile

cat Dockerfile | docker build -

  如果标准输入传入的是文本文件, 则将其视为 Dockerfile, 并开始构建. 这种形式由于直接从标准输入中读取 Dockerfile 的内容, 它没有上下文, 因此不可以像其他方法那样可以将本地文件 COPY 进镜像之类的事情.

从标准输入中读取上下文压缩包进行构建

docker build - < context.tar.gz

  如果发现标准输入的文件格式是 gzip、bzip2 以及 xz 的话, 将会使其为上下文压缩包, 直接将其展开, 将里面视为上下文, 并开始构建.

Dockerfile 指令详解

  上文已经介绍了 FROM, RUN, 还提及了 COPY, ADD, 其实 Dockerfile 功能很强大, 它提供了十多个指令. 下面继续讲解其他的指令.

COPY 复制文件

  格式:

COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]

  和 RUN 指令一样, 也有两种格式, 一种类似于命令行, 一种类似于函数调用.

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

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

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

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

ADD 更高级的复制文件

  ADD 指令和 COPY 的格式和性质基本一致. 但是在 COPY 基础上增加了一些功能.

  比如 <源路径> 可以是一个 URL, 这种情况下, Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去. 下载后的文件权限自动设置为 600, 如果这并不是想要的权限, 那么还需要增加额外的一层 RUN 进行权限调整, 另外, 如果下载的是个压缩包, 需要解压缩, 也一样还需要额外的一层 RUN 指令进行解压缩. 所以不如直接使用 RUN 指令, 然后使用 wget 或者 curl 工具下载, 处理权限、解压缩、然后清理无用文件更合理. 因此, 这个功能其实并不实用, 而且不推荐使用.

  如果 <源路径> 为一个 tar 压缩文件的话, 压缩格式为 gzip, bzip2 以及 xz 的情况下, ADD 指令将会自动解压缩这个压缩文件到 <目标路径>去.

  在某些情况下, 这个自动解压缩的功能非常有用, 比如官方镜像 ubuntu 中:

FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...

  但在某些情况下, 如果我们真的是希望复制个压缩文件进去, 而不解压缩, 这时就不可以使用 ADD 命令了.

  在 Docker 官方的 Dockerfile 最佳实践文档 中要求, 尽可能的使用 COPY, 因为 COPY 的语义很明确, 就是复制文件而已, 而 ADD 则包含了更复杂的功能, 其行为也不一定很清晰. 最适合使用 ADD 的场合, 就是所提及的需要自动解压缩的场合.

  另外需要注意的是, ADD 指令会令镜像构建缓存失效, 从而可能会令镜像构建变得比较缓慢.

  因此在 COPYADD 指令中选择的时候, 可以遵循这样的原则, 所有的文件复制均使用 COPY 指令, 仅在需要自动解压缩的场合使用 ADD.

CMD 容器启动命令

  CMD 指令的格式和 RUN 相似, 也是两种格式:

  • shell 格式: CMD <命令>
  • exec 格式: CMD ["可执行文件", "参数1", "参数2"...]
  • 参数列表格式: CMD ["参数1", "参数2"...]. 在指定了 ENTRYPOINT 指令后, 用 CMD 指定具体的参数.

  之前介绍容器的时候曾经说过, Docker 不是虚拟机, 容器就是进程. 既然是进程, 那么在启动容器的时候, 需要指定所运行的程序及参数. CMD 指令就是用于指定默认的容器主进程的启动命令的.

  在运行时可以指定新的命令来替代镜像设置中的这个默认命令, 比如, ubuntu 镜像默认的 CMD 是 /bin/bash, 如果我们直接 docker run -it ubuntu 的话, 会直接进入 bash. 我们也可以在运行时指定运行别的命令, 如 docker run -it ubuntu cat /etc/os-release. 这就是用 cat /etc/os-release 命令替换了默认的 /bin/bash 命令了, 输出了系统版本信息.

  在指令格式上, 一般推荐使用 exec 格式, 这类格式在解析时会被解析为 JSON 数组, 因此一定要使用双引号 ", 而不要使用单引号.

  如果使用 shell 格式的话, 实际的命令会被包装为 sh -c 的参数的形式进行执行. 比如:

CMD echo $HOME

  在实际执行中, 会将其变更为:

CMD [ "sh", "-c", "echo $HOME" ]

  这就是为什么我们可以使用环境变量的原因, 因为这些环境变量会被 shell 进行解析处理.

  提到CMD就不得不提容器中应用在前台执行和后台执行的问题. 这是初学者常出现的一个混淆.

  Docker 不是虚拟机, 容器中的应用都应该以前台执行, 而不是像虚拟机、物理机里面那样, 用upstart/systemd去启动后台服务, 容器内没有后台服务的概念.

  一些初学者将 CMD 写为:

CMD service nginx start

  然后发现容器执行后就立即退出了. 甚至在容器内去使用systemctl命令结果却发现根本执行不了. 这就是因为没有搞明白前台、后台的概念, 没有区分容器和虚拟机的差异, 依旧在以传统虚拟机的角度去理解容器.

  对于容器而言, 其启动程序就是容器应用进程, 容器就是为了主进程而存在的, 主进程退出, 容器就失去了存在的意义, 从而退出, 其它辅助进程不是它需要关心的东西.

  而使用service nginx start命令, 则是希望upstart 来以后台守护进程形式启动 nginx 服务. 而刚才说了 CMD service nginx start会被理解为CMD [ "sh", "-c", "service nginx start"], 因此主进程实际上是 sh. 那么当 service nginx start 命令结束后, sh 也就结束了, sh 作为主进程退出了, 自然就会令容器退出.

  正确的做法是直接执行 nginx 可执行文件, 并且要求以前台形式运行. 比如:

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

ENTRYPOINT 入口点

  ENTRYPOINT 的格式和 RUN 指令格式一样, 分为 exec 格式和 shell 格式.

  ENTRYPOINT 的目的和CMD一样, 都是在指定容器启动程序及参数. ENTRYPOINT在运行时也可以替代, 不过比CMD要略显繁琐, 需要通过docker run的参数 --entrypoint 来指定.

  当指定了 ENTRYPOINT后, CMD的含义就发生了改变, 不再是直接的运行其命令, 而是将 CMD的内容作为参数传给ENTRYPOINT指令, 换句话说实际执行时, 将变为:

<ENTRYPOINT> "<CMD>"

  那么有了CMD后, 为什么还要有ENTRYPOINT呢?这种<ENTRYPOINT> "<CMD>"有什么好处么?让我们来看几个场景.

场景一: 让镜像变成像命令一样使用

  假设我们需要一个得知自己当前公网 IP 的镜像, 那么可以先用 CMD 来实现:

FROM ubuntu:16.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://ip.cn" ]

  假如我们使用 docker build -t myip . 来构建镜像的话, 如果我们需要查询当前公网 IP.

  只需要执行: docker run myip

  输出: 当前 IP: 61.148.226.66 来自: 北京市 联通

  这么看起来好像可以直接把镜像当做命令使用了, 不过命令总有参数, 如果我们希望加参数呢?比如从上面的 CMD 中可以看到实质的命令是 curl, 那么如果我们希望显示 HTTP 头信息, 就需要加上 -i 参数. 那么我们可以直接加 -i 参数给 docker run myip 么?

docker run myip -i
docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".

  可以看到可执行文件找不到的报错, executable file not found. 之前我们说过, 跟在镜像名后面的是 command, 运行时会替换 CMD 的默认值. 因此这里的 -i 替换了原来的 CMD, 而不是添加在原来的 curl -s http://ip.cn 后面. 而 -i 根本不是命令, 所以自然找不到.

  那么如果我们希望加入 -i 这参数, 我们就必须重新完整的输入这个命令:
docker run myip curl -s http://ip.cn -i
这显然不是很好的解决方案, 而使用 ENTRYPOINT 就可以解决这个问题. 现在我们重新用 ENTRYPOINT 来实现这个镜像:

FROM ubuntu:16.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]

  这次我们再来尝试直接使用 docker run myip -i:

docker run myip
当前 IP: 61.148.226.66 来自: 北京市 联通
docker run myip -i
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 22 Nov 2016 05:12:40 GMT
Content-Type: text/html; charset=UTF-8
Vary: Accept-Encoding
X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
X-Cache: MISS from cache-2
X-Cache-Lookup: MISS from cache-2:80
X-Cache: MISS from proxy-2_6
Transfer-Encoding: chunked
Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
Connection: keep-alive
当前 IP: 61.148.226.66 来自: 北京市 联通

  可以看到, 这次成功了. 这是因为当存在 ENTRYPOINT 后, CMD 的内容将会作为参数传给 ENTRYPOINT, 而这里 -i 就是新的 CMD, 因此会作为参数传给 curl, 从而达到了我们预期的效果.

场景二: 应用运行前的准备工作:

  启动容器就是启动主进程, 但有些时候, 启动主进程前, 需要一些准备工作.

  比如 mysql 类的数据库, 可能需要一些数据库配置、初始化的工作, 这些工作要在最终的 mysql 服务器运行之前解决.

  此外, 可能希望避免使用 root 用户去启动服务, 从而提高安全性, 而在启动服务前还需要以 root 身份执行一些必要的准备工作, 最后切换到服务用户身份启动服务. 或者除了服务外, 其它命令依旧可以使用 root 身份执行, 方便调试等.

  这些准备工作是和容器 CMD 无关的, 无论 CMD 为什么, 都需要事先进行一个预处理的工作. 这种情况下, 可以写一个脚本, 然后放入 ENTRYPOINT 中去执行, 而这个脚本会将接到的参数(也就是 <CMD>)作为命令, 在脚本最后执行. 比如官方镜像 redis 中就是这么做的:

FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD [ "redis-server" ]

  可以看到其中为了 redis 服务创建了 redis 用户, 并在最后指定了 ENTRYPOINT 为 docker-entrypoint.sh 脚本.

#!/bin/sh
...
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
	chown -R redis .
	exec su-exec redis "$0" "$@"
fi

exec "$@"

  该脚本的内容就是根据 CMD 的内容来判断, 如果是 redis-server 的话, 则切换到 redis 用户身份启动服务器, 否则依旧使用 root 身份执行. 比如:

docker run -it redis id
uid=0(root) gid=0(root) groups=0(root)

ENV 设置环境变量

  格式有两种:

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...

  这个指令很简单, 就是设置环境变量而已, 无论是后面的其它指令, 如 RUN, 还是运行时的应用, 都可以直接使用这里定义的环境变量.

ENV VERSION=1.0 DEBUG=on NAME="Happy Feet"

  这个例子中演示了如何换行, 以及对含有空格的值用双引号括起来的办法, 这和 Shell 下的行为是一致的.

  定义了环境变量, 那么在后续的指令中, 就可以使用这个环境变量. 比如在官方 node 镜像 Dockerfile 中, 就有类似这样的代码:

ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
  && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
  && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
  && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
  && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
  && ln -s /usr/local/bin/node /usr/local/bin/nodejs

  在这里先定义了环境变量 NODE_VERSION, 其后的 RUN 这层里, 多次使用 $NODE_VERSION 来进行操作定制. 可以看到, 将来升级镜像构建版本的时候, 只需要更新 7.2.0 即可, Dockerfile 构建维护变得更轻松了.

  下列指令可以支持环境变量展开:

ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD. 

  可以从这个指令列表里感觉到, 环境变量可以使用的地方很多, 很强大. 通过环境变量, 我们可以让一份 Dockerfile 制作更多的镜像, 只需使用不同的环境变量即可.

ARG 构建参数

格式: ARG <参数名>[=<默认值>]

  构建参数和ENV的效果一样, 都是设置环境变量. 所不同的是, ARG所设置的构建环境的环境变量, 在将来容器运行时是不会存在这些环境变量的. 但是不要因此就使用ARG保存密码之类的信息, 因为docker history还是可以看到所有值的.

  Dockerfile 中的ARG指令是定义参数名称, 以及定义其默认值. 该默认值可以在构建命令docker build中用--build-arg <参数名>=<值>来覆盖.

  在 1.13 之前的版本, 要求--build-arg中的参数名, 必须在 Dockerfile 中用ARG定义过了, 换句话说, 就是--build-arg指定的参数, 必须在 Dockerfile 中使用了. 如果对应参数没有被使用, 则会报错退出构建. 从 1.13 开始, 这种严格的限制被放开, 不再报错退出, 而是显示警告信息, 并继续构建. 这对于使用 CI 系统, 用同样的构建流程构建不同的 Dockerfile 的时候比较有帮助, 避免构建命令必须根据每个 Dockerfile 的内容修改.

VOLUME 定义匿名卷

  格式为:

  • VOLUME ["<路径1>", "<路径2>"...]
  • VOLUME <路径>

  之前说过, 容器运行时应该尽量保持容器存储层不发生写操作, 对于数据库类需要保存动态数据的应用, 其数据库文件应该保存于卷(volume)中, 后面会进一步介绍 Docker 卷的概念.

  为了防止运行时用户忘记将动态文件所保存目录挂载为卷, 在 Dockerfile 中, 我们可以事先指定某些目录挂载为匿名卷, 这样在运行时如果用户不指定挂载, 其应用也可以正常运行, 不会向容器存储层写入大量数据.

VOLUME /data

  这里的 /data 目录就会在运行时自动挂载为匿名卷, 任何向 /data 中写入的信息都不会记录进容器存储层, 从而保证了容器存储层的无状态化. 当然, 运行时可以覆盖这个挂载设置. 比如:

docker run -d -v mydata:/data xxxx

  在这行命令中, 就使用了 mydata 这个命名卷挂载到了 /data 这个位置, 替代了 Dockerfile 中定义的匿名卷的挂载配置.

EXPOSE 暴露端口

格式: EXPOSE <端口1> [<端口2>...]

  EXPOSE 指令是声明运行时容器提供服务端口, 这只是一个声明, 在运行时并不会因为这个声明应用就会开启这个端口的服务. 在 Dockerfile 中写入这样的声明有两个好处, 一个是帮助镜像使用者理解这个镜像服务的守护端口, 以方便配置映射;另一个用处则是在运行时使用随机端口映射时, 也就是 docker run -P时, 会自动随机映射 EXPOSE 的端口.

  此外, 在早期 Docker 版本中还有一个特殊的用处. 以前所有容器都运行于默认桥接网络中, 因此所有容器互相之间都可以直接访问, 这样存在一定的安全性问题. 于是有了一个 Docker 引擎参数--icc=false, 当指定该参数后, 容器间将默认无法互访, 除非互相间使用了--links参数的容器才可以互通, 并且只有镜像中 EXPOSE 所声明的端口才可以被访问. 这个--icc=false的用法, 在引入了 docker network 后已经基本不用了, 通过自定义网络可以很轻松的实现容器间的互联与隔离.

  要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来. -p是映射宿主端口和容器端口, 换句话说, 就是将容器的对应端口服务公开给外界访问, 而 EXPOSE 仅仅是声明容器打算使用什么端口而已, 并不会自动在宿主进行端口映射.

WORKDIR 指定工作目录

格式: WORKDIR <工作目录路径>

  使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录), 以后各层的当前目录就被改为指定的目录, 如该目录不存在, WORKDIR 会帮你建立目录.

  之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写, 这种错误的理解还可能会导致出现下面这样的错误:

RUN cd /app
RUN echo "hello" > world.txt

  如果将这个 Dockerfile 进行构建镜像运行后, 会发现找不到 /app/world.txt 文件, 或者其内容不是 hello. 原因其实很简单, 在 Shell 中, 连续两行是同一个进程执行环境, 因此前一个命令修改的内存状态, 会直接影响后一个命令;而在 Dockerfile 中, 这两行 RUN 命令的执行环境根本不同, 是两个完全不同的容器. 这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误.

  之前说过每一个RUN都是启动一个容器、执行命令、然后提交存储层文件变更. 第一层 RUN cd /app的执行仅仅是当前进程的工作目录变更, 一个内存上的变化而已, 其结果不会造成任何文件变更. 而到第二层的时候, 启动的是一个全新的容器, 跟第一层的容器更完全没关系, 自然不可能继承前一层构建过程中的内存变化.

  因此如果需要改变以后各层的工作目录的位置, 那么应该使用WORKDIR指令.

USER 指定当前用户

格式: USER <用户名>

  USER 指令和WORKDIR相似, 都是改变环境状态并影响以后的层. WORKDIR是改变工作目录, USER则是改变之后层的执行RUN,CMD以及ENTRYPOINT这类命令的身份.

  当然, 和WORKDIR一样, USER只是帮助你切换到指定用户而已, 这个用户必须是事先建立好的, 否则无法切换.

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

  如果以 root 执行的脚本, 在执行期间希望改变身份, 比如希望以某个已经建立好的用户来运行某个服务进程, 不要使用 su 或者 sudo, 这些都需要比较麻烦的配置, 而且在 TTY 缺失的环境下经常出错. 建议使用 gosu.

# 建立 redis 用户, 并使用 gosu 换另一个用户执行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.7/gosu-amd64" \
    && chmod +x /usr/local/bin/gosu \
    && gosu nobody true
# 设置 CMD, 并以另外的用户执行
CMD [ "exec", "gosu", "redis", "redis-server" ]

HEALTHCHECK 健康检查

格式:

  • HEALTHCHECK [选项] CMD <命令>: 设置检查容器健康状况的命令
  • HEALTHCHECK NONE: 如果基础镜像有健康检查指令, 使用这行可以屏蔽掉其健康检查指令
    HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常, 这是 Docker 1.12 引入的新指令.

  在没有HEALTHCHECK指令前, Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常. 很多情况下这没问题, 但是如果程序进入死锁状态, 或者死循环状态, 应用进程并不退出, 但是该容器已经无法提供服务了. 在 1.12 以前, Docker 不会检测到容器的这种状态, 从而不会重新调度, 导致可能会有部分容器已经无法提供服务了却还在接受用户请求.

  而自 1.12 之后, Docker 提供了HEALTHCHECK指令, 通过该指令指定一行命令, 用这行命令来判断容器主进程的服务状态是否还正常, 从而比较真实的反应容器实际状态.

  当在一个镜像指定了HEALTHCHECK指令后, 用其启动容器, 初始状态会为starting, 在HEALTHCHECK指令检查成功后变为 healthy, 如果连续一定次数失败, 则会变为 unhealthy.

HEALTHCHECK 支持下列选项:

  • --interval=<间隔>: 两次健康检查的间隔, 默认为 30 秒;
  • --timeout=<时长>: 健康检查命令运行超时时间, 如果超过这个时间, 本次健康检查就被视为失败, 默认 30 秒;
  • --retries=<次数>: 当连续失败指定次数后, 则将容器状态视为 unhealthy, 默认 3 次.

  和 CMD, ENTRYPOINT 一样, HEALTHCHECK 只可以出现一次, 如果写了多个, 只有最后一个生效.

  在 HEALTHCHECK [选项] CMD后面的命令, 格式和 ENTRYPOINT 一样, 分为 shell 格式, 和 exec 格式. 命令的返回值决定了该次健康检查的成功与否: 0: 成功;1: 失败;2: 保留, 不要使用这个值.

  假设有个镜像是个最简单的 Web 服务, 我们希望增加健康检查来判断其 Web 服务是否在正常工作, 我们可以用 curl 来帮助判断, 其 Dockerfile 的 HEALTHCHECK 可以这么写:

FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
  CMD curl -fs http://localhost/ || exit 1

  这里我们设置了每 5 秒检查一次(这里为了试验所以间隔非常短, 实际应该相对较长), 如果健康检查命令超过 3 秒没响应就视为失败, 并且使用 curl -fs http://localhost/ || exit 1 作为健康检查命令.

  使用 docker build 来构建这个镜像:

docker build -t myweb:v1 .

  构建好了后, 我们启动一个容器:

docker run -d --name web -p 80:80 myweb:v1

  当运行该镜像后, 可以通过 docker container ls 看到最初的状态为 (health: starting):

docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   3 seconds ago       Up 2 seconds (health: starting)   80/tcp, 443/tcp     web
在等待几秒钟后, 再次 docker container ls, 就会看到健康状态变化为了 (healthy): 
docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   18 seconds ago      Up 16 seconds (healthy)   80/tcp, 443/tcp     web

  如果健康检查连续失败超过了重试次数, 状态就会变为 (unhealthy).

  为了帮助排障, 健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里, 可以用 docker inspect来查看.

docker inspect --format '{{json .State.Health}}' web | python -m json.tool
{
    "FailingStreak": 0,
    "Log": [
        {
            "End": "2016-11-25T14:35:37.940957051Z",
            "ExitCode": 0,
            "Output": "<!DOCTYPE html>\n<html>…… </html>\n",
            "Start": "2019-11-25T14:35:37.780192565Z"
        }
    ],
    "Status": "healthy"
}

ONBUILD 为他人作嫁衣

格式: ONBUILD <其它指令>

  ONBUILD是一个特殊的指令, 它后面跟的是其它指令, 比如 RUN, COPY等, 而这些指令, 在当前镜像构建时并不会被执行. 只有当以当前镜像为基础镜像, 去构建下一级镜像的时候才会被执行.

  Dockerfile 中的其它指令都是为了定制当前镜像而准备的, 唯有 ONBUILD是为了帮助别人定制自己而准备的.

  假设我们要制作 Node.js 所写的应用的镜像. Node.js 使用 npm 进行包管理, 所有依赖、配置、启动信息等会放到 package.json 文件里. 在拿到程序代码后, 需要先进行 npm install 才可以获得所有需要的依赖. 然后就可以通过 npm start 来启动应用. 因此, 一般来说会这样写 Dockerfile:

FROM node:slim
RUN mkdir /app
WORKDIR /app
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/
CMD [ "npm", "start" ]

  把这个 Dockerfile 放到 Node.js 项目的根目录, 构建好镜像后, 就可以直接拿来启动容器运行. 但是如果我们还有第二个 Node.js 项目也差不多呢?好吧, 那就再把这个 Dockerfile 复制到第二个项目里. 那如果有第三个项目呢?再复制么?文件的副本越多, 版本控制就越困难, 让我们继续看这样的场景维护的问题.

  如果第一个 Node.js 项目在开发过程中, 发现这个 Dockerfile 里存在问题, 比如敲错字了、或者需要安装额外的包, 然后开发人员修复了这个 Dockerfile, 再次构建, 问题解决. 第一个项目没问题了, 但是第二个项目呢?虽然最初 Dockerfile 是复制、粘贴自第一个项目的, 但是并不会因为第一个项目修复了他们的 Dockerfile, 而第二个项目的 Dockerfile 就会被自动修复.

  那么我们可不可以做一个基础镜像, 然后各个项目使用这个基础镜像呢?这样基础镜像更新, 各个项目不用同步 Dockerfile 的变化, 重新构建后就继承了基础镜像的更新?好吧, 可以, 让我们看看这样的结果. 那么上面的这个 Dockerfile 就会变为:

FROM node:slim
RUN mkdir /app
WORKDIR /app
CMD [ "npm", "start" ]

  这里我们把项目相关的构建指令拿出来, 放到子项目里去. 假设这个基础镜像的名字为 my-node 的话, 各个项目内的自己的 Dockerfile 就变为:

FROM my-node
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/

  基础镜像变化后, 各个项目都用这个 Dockerfile 重新构建镜像, 会继承基础镜像的更新.

  那么, 问题解决了么?没有. 准确说, 只解决了一半. 如果这个 Dockerfile 里面有些东西需要调整呢?比如 npm install 都需要加一些参数, 那怎么办?这一行 RUN 是不可能放入基础镜像的, 因为涉及到了当前项目的 ./package.json, 难道又要一个个修改么?所以说, 这样制作基础镜像, 只解决了原来的 Dockerfile 的前4条指令的变化问题, 而后面三条指令的变化则完全没办法处理.

  ONBUILD可以解决这个问题. 用ONBUILD重新写一下基础镜像的 Dockerfile:

FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]

  这次我们回到原始的 Dockerfile, 但是这次将项目相关的指令加上 ONBUILD, 这样在构建基础镜像的时候, 这三行并不会被执行. 然后各个项目的 Dockerfile 就变成了简单地:
FROM my-node
  是的, 只有这么一行. 当在各个项目目录中, 用这个只有一行的 Dockerfile 构建镜像时, 之前基础镜像的那三行 ONBUILD 就会开始执行, 成功的将当前项目的代码复制进镜像、并且针对本项目执行 npm install, 生成应用镜像.

Dockerfile 多阶段构建

之前的做法

  在 Docker 17.05 版本之前, 我们构建 Docker 镜像时, 通常会采用两种方式:

全部放入一个 Dockerfile

  一种方式是将所有的构建过程编包含在一个 Dockerfile 中, 包括项目及其依赖库的编译、测试、打包等流程, 这里可能会带来的一些问题:

  • Dockerfile 特别长, 可维护性降低
  • 镜像层次多, 镜像体积较大, 部署时间变长
  • 源代码存在泄露的风险

  例如:
编写 app.go 文件, 该程序输出 Hello World!

package main  
import "fmt"  
func main(){  
    fmt.Printf("Hello World!");
}

  编写 Dockerfile.one 文件:

FROM golang:1.9-alpine
RUN apk --no-cache add git ca-certificates
WORKDIR /go/src/github.com/go/helloworld/
COPY app.go .
RUN go get -d -v github.com/go-sql-driver/mysql \
  && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . \
  && cp /go/src/github.com/go/helloworld/app /root
WORKDIR /root/
CMD ["./app"]

  构建镜像

docker build -t go/helloworld:1 -f Dockerfile.one

分散到多个 Dockerfile

  另一种方式, 就是我们事先在一个 Dockerfile 将项目及其依赖库编译测试打包好后, 再将其拷贝到运行环境中, 这种方式需要我们编写两个 Dockerfile 和一些编译脚本才能将其两个阶段自动整合起来, 这种方式虽然可以很好地规避第一种方式存在的风险, 但明显部署过程较复杂.

  例如:

  编写 Dockerfile.build 文件:

FROM golang:1.9-alpine
RUN apk --no-cache add git
WORKDIR /go/src/github.com/go/helloworld
COPY app.go .
RUN go get -d -v github.com/go-sql-driver/mysql \
  && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . 

  编写 Dockerfile.copy 文件:

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"] 

  新建 build.sh:

#!/bin/sh
echo Building go/helloworld:build
docker build -t go/helloworld:build . -f Dockerfile.build
docker create --name extract go/helloworld:build
docker cp extract:/go/src/github.com/go/helloworld/app ./app
docker rm -f extract
echo Building go/helloworld:2
docker build --no-cache -t go/helloworld:2 . -f Dockerfile.copy
rm ./app 

  现在运行脚本即可构建镜像:

chmod +x build.sh
./build.sh  

  对比两种方式生成的镜像大小

docker image ls 
REPOSITORY      TAG    IMAGE ID        CREATED         SIZE
go/helloworld   2      f7cf3465432c    22 seconds ago  6.47MB
go/helloworld   1      f55d3e16affc    2 minutes ago   295MB

使用多阶段构建

  为解决以上问题, Docker v17.05 开始支持多阶段构建 (multistage builds). 使用多阶段构建我们就可以很容易解决前面提到的问题, 并且只需要编写一个 Dockerfile:

例如:

  编写 Dockerfile 文件:

FROM golang:1.9-alpine as builder
RUN apk --no-cache add git
WORKDIR /go/src/github.com/go/helloworld/
RUN go get -d -v github.com/go-sql-driver/mysql
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest as prod
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/go/helloworld/app .
CMD ["./app"]    

  构建镜像: docker build -t go/helloworld:3 .

  对比三个镜像大小

docker image ls  
REPOSITORY        TAG   IMAGE ID         CREATED            SIZE
go/helloworld     3     d6911ed9c846     7 seconds ago      6.47MB
go/helloworld     2     f7cf3465432c     22 seconds ago     6.47MB
go/helloworld     1     f55d3e16affc     2 minutes ago      295MB

  很明显使用多阶段构建的镜像体积小, 同时也完美解决了上边提到的问题.

  只构建某一阶段的镜像
我们可以使用 as 来为某一阶段命名, 例如
FROM golang:1.9-alpine as builder例如当我们只想构建 builder 阶段的镜像时, 我们可以在使用 docker build 命令时加上 --target 参数即可
docker build --target builder -t username/imagename:tag .

  构建时从其他镜像复制文件
上面例子中我们使用 COPY --from=0 /go/src/github.com/go/helloworld/app . 从上一阶段的镜像中复制文件, 我们也可以复制任意镜像中的文件.
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

  这块儿更详细的文章见博客: Docker多阶段构建最佳实践

其他制作镜像方式

  除了标准的使用 Dockerfile 生成镜像的方法外, 由于各种特殊需求和历史原因, 还提供了一些其它方法用以生成镜像.

从 rootfs 压缩包导入

格式: docker import [选项] <文件>|<URL>|- [<仓库名>[:<标签>]]

  压缩包可以是本地文件、远程 Web 文件, 甚至是从标准输入中得到. 压缩包将会在镜像 / 目录展开, 并直接作为镜像第一层提交.

  比如我们想要创建一个 OpenVZ 的 Ubuntu 14.04 模板的镜像:

docker import \
    http://download.openvz.org/template/precreated/ubuntu-14.04-x86_64-minimal.tar.gz \
    openvz/ubuntu:14.04

  这条命令自动下载了 ubuntu-14.04-x86_64-minimal.tar.gz 文件, 并且作为根文件系统展开导入, 并保存为镜像 openvz/ubuntu:14.04.

  导入成功后, 我们可以用 docker image ls 看到这个导入的镜像:

docker image ls openvz/ubuntu
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
openvz/ubuntu       14.04               f477a6e18e98        55 seconds ago      214.9 MB

  如果我们查看其历史的话, 会看到描述中有导入的文件链接:

docker history openvz/ubuntu:14.04
IMAGE               CREATED              CREATED BY          SIZE                COMMENT
f477a6e18e98        About a minute ago                       214.9 MB            Imported from http://download.openvz.org/template/precreated/ubuntu-14.04-x86_64-minimal.tar.gz

docker save 和 docker load

  Docker 还提供了docker loaddocker save命令, 用以将镜像保存为一个 tar 文件, 然后传输到另一个位置上, 再加载进来. 这是在没有 Docker Registry 时的做法, 现在已经不推荐, 镜像迁移应该直接使用 Docker Registry, 无论是直接使用 Docker Hub 还是使用内网私有 Registry 都可以.

保存镜像

  使用 docker save 命令可以将镜像保存为归档文件.

  比如我们希望保存这个 alpine 镜像.

docker image ls alpine

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
alpine              latest              baa5d63471ea        5 weeks ago         4.803 MB

保存镜像的命令为:

docker save alpine | gzip > alpine-latest.tar.gz

  然后我们将 alpine-latest.tar.gz 文件复制到了到了另一个机器上, 可以用下面这个命令加载镜像:

docker load -i alpine-latest.tar.gz

  如果我们结合这两个命令以及 ssh 甚至 pv 的话, 利用 Linux 强大的管道, 我们可以写一个命令完成从一个机器将镜像迁移到另一个机器, 并且带进度条的功能:

docker save <镜像名> | bzip2 | pv | ssh <用户名>@<主机名> 'cat | docker load'

镜像的实现原理

  Docker 镜像是怎么实现增量的修改和维护的?

  每个镜像都由很多层次构成, Docker 使用 Union FS 将这些不同的层结合到一个镜像中去.

  通常 Union FS 有两个用途, 一方面可以实现不借助 LVM、RAID 将多个 disk 挂到同一个目录下,另一个更常用的就是将一个只读的分支和一个可写的分支联合在一起, Live CD 正是基于此方法可以允许在镜像不变的基础上允许用户在其上进行一些写操作.

  Docker 在 AUFS 上构建的容器也是利用了类似的原理.

docker 容器

  容器是 Docker 又一核心概念.

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

  本章将具体介绍如何来管理一个容器, 包括创建、启动和停止等.

启动容器

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

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

新建并启动

  所需要的命令主要为 docker run.

  例如, 下面的命令输出一个 “Hello World”, 之后终止容器.

docker run ubuntu:14.04 /bin/echo 'Hello world'

  这跟在本地直接执行 /bin/echo 'hello world' 几乎感觉不出任何区别.

  下面的命令则启动一个 bash 终端, 允许用户进行交互.

docker run -t -i ubuntu:14.04 /bin/bash

  其中, -t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i 则让容器的标准输入保持打开.

  在交互模式下, 用户可以通过所创建的终端来输入命令, 例如

root@af8bae53bdd3:/# pwd
/
root@af8bae53bdd3:/# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

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

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

启动已终止容器

  可以利用 docker container start 命令, 直接将一个已经终止的容器启动运行.

  容器的核心为所执行的应用程序, 所需要的资源都是应用程序运行所必需的. 除此之外, 并没有其它的资源. 可以在伪终端中利用 ps 或 top 来查看进程信息.

root@ba267838cc1b:/# ps
  PID TTY          TIME CMD
    1 ?        00:00:00 bash
   11 ?        00:00:00 ps

  可见, 容器中仅运行了指定的 bash 应用. 这种特点使得 Docker 对资源的利用率极高, 是货真价实的轻量级虚拟化.

守护状态运行

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

  下面举两个例子来说明一下.
如果不使用 -d 参数运行容器.

# 构建容器并运行的指令
docker run ubuntu:17.10 /bin/sh -c "while true; do echo 

# 打印日志
hello world; sleep 1; done"
hello world
hello world
hello world
hello world

  容器会把输出的结果 (STDOUT) 打印到宿主机上面

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

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

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

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

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

docker container ls

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

docker container logs [container ID or NAMES]

终止容器

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

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

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

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

docker container ls -a

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

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

进入容器

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

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

attach 命令

  docker attach 是 Docker 自带的命令. 下面示例如何使用该命令.

docker run -dit ubuntu
docker run -dit Ubuntu
docker attach 243c

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

exec 命令

*-i -t 参数

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

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

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

docker run -dit ubuntu
docker container ls
docker exec -it 69d1 bash
docker exec -i 69d1 bash

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

  更多参数说明请使用 docker exec --help 查看.

导出和导入容器

导出容器

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

docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS               NAMES
7691a814370e        ubuntu:14.04        "/bin/bash"         36 hours ago        Exited (0) 21 hours ago                       test
docker export 7691a814370e > ubuntu.tar

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

导入容器快照

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

cat ubuntu.tar | docker import - test/ubuntu:v1.0
docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
test/ubuntu         v1.0                9d37a6082e97        About a minute ago   171.3 MB

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

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

  **注: 用户既可以使用 docker load 来导入镜像存储文件到本地镜像库, 也可以使用 docker import 来导入一个容器快照到本地镜像库. 这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态), 而镜像存储文件将保存完整记录, 体积也要大. 此外, 从容器快照文件导入时可以重新指定标签等元数据信息. **

删除容器

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

docker container rm  trusting_newton

  如果要删除一个运行中的容器, 可以添加 -f 参数. Docker 会发送 SIGKILL 信号给容器.

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

docker container prune

docker 仓库

  仓库(Repository)是集中存放镜像的地方.

  一个容易混淆的概念是注册服务器(Registry). 实际上注册服务器是管理仓库的具体服务器, 每个服务器上可以有多个仓库, 而每个仓库下面有多个镜像. 从这方面来说, 仓库可以被认为是一个具体的项目或目录. 例如对于仓库地址 dl.dockerpool.com/ubuntu 来说, dl.dockerpool.com 是注册服务器地址, ubuntu 是仓库名.

  大部分时候, 并不需要严格区分这两者的概念.

docker hub

注册

  你可以在 https://hub.docker.com/ 免费注册一个 Docker 账号.

登录

  可以通过执行 docker login 命令交互式的输入用户名及密码来完成在命令行界面登录 Docker Hub.

  可以通过 docker logout 退出登录.

拉取镜像

  你可以通过 docker search [镜像名] 命令来查找官方仓库中的镜像, 并利用docker pull命令来将它下载到本地.

  例如以 centos 为关键词进行搜索:

docker search centos

docker_17

  可以看到返回了很多包含关键字的镜像, 其中包括镜像名字、描述、收藏数(表示该镜像的受关注程度)、是否官方创建、是否自动创建.

  官方的镜像说明是官方项目组创建和维护的, automated 资源允许用户验证镜像的来源和内容.

  根据是否是官方提供, 可将镜像资源分为两类.

  一种是类似 centos 这样的镜像, 被称为基础镜像或根镜像. 这些基础镜像由 Docker 公司创建、验证、支持、提供. 这样的镜像往往使用单个单词作为名字.

  还有一种类型, 比如 tutum/centos 镜像, 它是由 Docker 的用户创建并维护的, 往往带有用户名称前缀. 可以通过前缀 username/ 来指定使用某个用户提供的镜像, 比如 tutum 用户.

  另外, 在查找的时候通过 --filter=stars=N 参数可以指定仅显示收藏数量为 N 以上的镜像.

  下载官方 centos 镜像到本地.

docker pull centos
Pulling repository centos
0b443ba03958: Download complete
539c0211cd76: Download complete
511136ea3c5a: Download complete
7064731afe90: Download complete

推送镜像

  用户也可以在登录后通过 docker push 命令来将自己的镜像推送到 Docker Hub.

这里请注意测试的时候请别拿真实产品测试...

  以下命令中的 username 请替换为你的 Docker 账号用户名

docker tag ubuntu:17.10 username/ubuntu:17.10
docker image ls
docker push username/ubuntu:17.10
docker search username

自动创建

  自动创建(Automated Builds)功能对于需要经常升级镜像内程序来说, 十分方便.

  有时候, 用户创建了镜像, 安装了某个软件, 如果软件发布新版本则需要手动更新镜像.

  而自动创建允许用户通过 Docker Hub 指定跟踪一个目标网站(目前支持 GitHub 或 BitBucket)上的项目, 一旦项目发生新的提交或者创建新的标签(tag), Docker Hub 会自动构建镜像并推送到 Docker Hub 中.

  要配置自动创建, 包括如下的步骤:

  创建并登录 Docker Hub, 以及目标网站;
在目标网站中连接帐户到 Docker Hub;
在 Docker Hub 中 配置一个自动创建;
选取一个目标网站中的项目(需要含 Dockerfile)和分支;
指定 Dockerfile 的位置, 并提交创建.
之后, 可以在 Docker Hub 的 自动创建页面 中跟踪每次创建的状态.

  **如果是持续集成产品,构建推送到线上服务器的应用场景(正常的互联网公司), 可以参考 svn/git + jenkins + docker 的模式来进行 构建-推送-部署 **

docker 私有仓库

  有时候使用 Docker Hub 这样的公共仓库可能不方便, 用户可以创建一个本地仓库供私人使用.

  本节介绍如何使用本地仓库.

  docker-registry 是官方提供的工具, 可以用于构建私有的镜像仓库. 本文内容基于 docker-registry v2.x 版本.

安装运行 docker-registry

  你可以通过获取官方 registry 镜像来运行.

docker run -d -p 5000:5000 --restart=always --name registry registry

  这将使用官方的 registry 镜像来启动私有仓库. 默认情况下, 仓库会被创建在容器的 /var/lib/registry 目录下. 你可以通过 -v 参数来将镜像文件存放在本地的指定路径. 例如下面的例子将上传的镜像放到本地的 /opt/data/registry 目录.

docker run -d \
    -p 5000:5000 \
    -v /opt/data/registry:/var/lib/registry \
    registry

在私有仓库上传、搜索、下载镜像

  创建好私有仓库之后, 就可以使用 docker tag 来标记一个镜像, 然后推送它到仓库. 例如私有仓库地址为 127.0.0.1:5000.

  先在本机查看已有的镜像.

docker image ls
REPOSITORY           TAG       IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu                 latest     ba5877dc9bec        6 weeks ago         192.7 MB

  使用 docker tagubuntu:latest 这个镜像标记为 127.0.0.1:5000/ubuntu:latest.

格式为: docker tag IMAGE[:TAG] [REGISTRY_HOST[:REGISTRY_PORT]/]REPOSITORY[:TAG].

docker tag ubuntu:latest 127.0.0.1:5000/ubuntu:latest
docker image ls
REPOSITORY        TAG       IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu           latest        ba5877dc9bec        6 weeks ago         192.7 MB
127.0.0.1:5000/ubuntu:latest   latest    ba5877dc9bec        6 weeks ago         192.7 MB

  使用 docker push 上传标记的镜像.

docker push 127.0.0.1:5000/ubuntu:latest

  用 curl 查看仓库中的镜像.

curl 127.0.0.1:5000/v2/_catalog
{"repositories":["ubuntu"]}

  这里可以看到 {"repositories":["ubuntu"]}, 表明镜像已经被成功上传了.

  先删除已有镜像, 再尝试从私有仓库中下载这个镜像.

docker image rm 127.0.0.1:5000/ubuntu:latest
docker pull 127.0.0.1:5000/ubuntu:latestdocker image ls
docker image ls

注意事项

  如果你不想使用 127.0.0.1:5000 作为仓库地址, 比如想让本网段的其他主机也能把镜像推送到私有仓库. 你就得把例如 192.168.199.100:5000 这样的内网地址作为私有仓库地址, 这时你会发现无法成功推送镜像.

  这是因为 Docker 默认不允许非 HTTPS 方式推送镜像. 我们可以通过 Docker 的配置选项来取消这个限制, 或者查看下一节配置能够通过 HTTPS 访问的私有仓库.

对于CentOS 7:
  请在 /etc/docker/daemon.json 中写入如下内容(如果文件不存在请新建该文件)

{
  "registry-mirrors": [
    "https://registry.docker-cn.com"
  ],
  "insecure-registries": [
    "192.168.199.100:5000"
  ]
}

对于windows:
docker_18

**注意: 该文件必须符合 json 规范, 否则 Docker 将不能启动. **

修改后保存配置,重启docker:

systemctl daemon-reload
systemctl restart docker

docker registry 配置证书

  上一节我们搭建了一个具有基础功能的私有仓库, 本小节我们来使用 Docker Compose 搭建一个拥有权限认证、TLS 的私有仓库.

  新建一个文件夹, 以下步骤均在该文件夹中进行.

域名以https://docker.domain.com为例

6.2.4.1 准备站点证书

  如果你拥有一个域名, 国内各大云服务商均提供免费的站点证书. 你也可以使用 openssl 自行签发证书.

6.2.4.2 配置私有仓库
  私有仓库默认的配置文件位于 /etc/docker/registry/config.yml, 我们先在本地编辑 config.yml, 之后挂载到容器中.

version: 0.1
log:
  accesslog:
    disabled: true
  level: debug
  formatter: text
  fields:
    service: registry
    environment: staging
storage:
  delete:
    enabled: true
  cache:
    blobdescriptor: inmemory
  filesystem:
    rootdirectory: /var/lib/registry
auth:
  htpasswd:
    realm: basic-realm
    path: /etc/docker/registry/auth/nginx.htpasswd
http:
  addr: :443
  host: https://docker.domain.com
  headers:
    X-Content-Type-Options: [nosniff]
  http2:
    disabled: false
  tls:
    certificate: /etc/docker/registry/ssl/docker.domain.com.crt
    key: /etc/docker/registry/ssl/docker.domain.com.key
health:
  storagedriver:
    enabled: true
    interval: 10s
threshold: 3

6.2.4.3 生成 http 认证文件

mkdir auth

docker run --rm \
    --entrypoint htpasswd \
    registry \
    -Bbn username password > auth/nginx.htpasswd

  将上面的 username password 替换为你自己的用户名和密码.

6.2.4.4 编辑 docker-compose.yml

version: '3'

services:
  registry:
    image: registry
    ports:
      - "443:443"
    volumes:
      - ./:/etc/docker/registry
      - registry-data:/var/lib/registry

volumes:
  registry-data:

6.2.4.6 启动

docker-compose up -d

  这样我们就搭建好了一个具有权限认证、TLS 的私有仓库, 接下来我们测试其功能是否正常.

6.2.4.7 测试私有仓库功能

  登录到私有仓库.

docker login docker.domain.com

  尝试推送、拉取镜像.

docker pull ubuntu:17.10
docker tag ubuntu:17.10 docker.domain.com/username/ubuntu:17.10
docker push docker.domain.com/username/ubuntu:17.10
docker image rm docker.domain.com/username/ubuntu:17.10
docker pull docker.domain.com/username/ubuntu:17.10

  如果我们退出登录, 尝试推送镜像.

docker logout docker.domain.com
docker push docker.domain.com/username/ubuntu:17.10

  发现会提示没有登录, 不能将镜像推送到私有仓库中.

6.2.4.8 注意事项
  如果你本机占用了 443 端口, 你可以配置 Nginx 代理, 这里不再赘述.

docker 企业级仓库 Harbor

  Harbor是构建企业级私有docker镜像的仓库的开源解决方案, 它是Docker Registry的更高级封装, 它除了提供友好的Web UI界面, 角色和用户权限管理, 用户操作审计等功能外, 它还整合了K8s的插件(Add-ons)仓库, 即Helm通过chart方式下载, 管理, 安装K8s插件, 而chartmuseum可以提供存储chart数据的仓库【注:helm就相当于k8s的yum】. 另外它还整合了两个开源的安全组件, 一个是Notary, 另一个是Clair, Notary类似于私有CA中心, 而Clair则是容器安全扫描工具, 它通过各大厂商提供的CVE漏洞库来获取最新漏洞信息, 并扫描用户上传的容器是否存在已知的漏洞信息, 这两个安全功能对于企业级私有仓库来说是非常具有意义的.

Harbor官方文档

  下面演示一下单机版的安装步骤:

注意.如果之前启动了docker-registry容器.需要提前删除掉

安装docker-compose:

yum install docker-compose

去官方资源发布页下载安装包:

https://github.com/goharbor/harbor/releases

docker_19

harbor-offline-installer-v2.1.0.tgz为例:

解压:

tar -zxvf harbor-offline-installer-v2.1.0.tgz

修改harbor.yml配置文件:

  如果安装的是2.x版本的harbor, 需要先把harbor.yml.tmpl复制/修改为harbor.yml.

  编辑harbor,把hostname port https harbor_admin_password data_volume等按照需要配置好.

docker_20

安装:

  执行prepare和install.sh脚本:

docker_21

  出现成功提示即为安装成功.用docker ps -a命令可以查看到已启动的harbor服务.

测试:

  成功后访问刚才配置的域名+端口,就可以正常访问harbor的UI界面, 默认的登录名是admin, 密码是配置文件中设置的密码.

docker_22

常用命令:

  进入解压后的文件夹内可执行:

  • 停止服务: docker-compose stop
  • 开始服务: docker-compose start

正常使用:

  仓库的使用见上文操作docker-registry的示例.

  注意只有建立对应仓库才能正常提交.

docker_23

docker_24

docker 数据管理

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

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

docker 数据卷

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

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

**注意: 数据卷 的使用, 类似于 Linux 下对目录或文件进行 mount, 镜像中的被指定为挂载点的目录中的文件会隐藏掉, 能显示看的是挂载的 数据卷. **

选择 -v 还是 -–mount 参数

  Docker 新用户应该选择 --mount 参数, 经验丰富的 Docker 使用者对 -v 或者 --volume 已经很熟悉了, 但是推荐使用 --mount 参数.

创建一个数据卷

docker volume create my-vol

  查看所有的 数据卷

docker volume ls

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

docker volume inspect my-vol
 [
    {
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/my-vol/_data",
        "Name": "my-vol",
        "Options": {},
        "Scope": "local"
    }
]

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

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

  下面创建一个名为 web 的容器, 并加载一个 数据卷 到容器的 /webapp 目录.

docker run -d -P \
    --name web \
    # -v my-vol:/wepapp \
    --mount source=my-vol,target=/webapp \
    training/webapp \
    python app.py

查看数据卷的具体信息

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

docker inspect web

  数据卷 信息在 "Mounts" Key 下面:

"Mounts": [
    {
        "Type": "volume",
        "Name": "my-vol",
        "Source": "/var/lib/docker/volumes/my-vol/_data",
        "Destination": "/app",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
    }
],

删除数据卷

docker volume rm my-vol

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

  无主的数据卷可能会占据很多空间, 要清理请使用以下命令

docker volume prune

监听主机目录

-v 还是 -–mount 参数

  Docker 新用户应该选择 --mount 参数, 经验丰富的 Docker 使用者对-v或者--volume已经很熟悉了, 但是推荐使用--mount参数.

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

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

docker run -d -P \
    --name web \
    # -v /src/webapp:/opt/webapp \
    --mount type=bind,source=/src/webapp,target=/opt/webapp \
    training/webapp \
    python app.py

  上面的命令加载主机的/src/webapp目录到容器的 /opt/webapp目录. 这个功能在进行测试的时候十分方便, 比如用户可以放置一些程序到本地目录中, 来查看容器是否正常工作. 本地目录的路径必须是绝对路径, 以前使用-v参数时如果本地目录不存在 Docker 会自动为你创建一个文件夹, 现在使用--mount参数时如果本地目录不存在, Docker 会报错.

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

docker run -d -P \
    --name web \
    # -v /src/webapp:/opt/webapp:ro \
    --mount type=bind,source=/src/webapp,target=/opt/webapp,readonly \
    training/webapp \
    python app.py

  加了 readonly 之后, 就挂载为 只读 了. 如果你在容器内/opt/webapp目录新建文件, 会显示如下错误

/opt/webapp # touch new.txt
touch: new.txt: Read-only file system

查看数据卷的具体信息

  在主机里使用以下命令可以查看 web 容器的信息
docker inspect web
挂载主机目录 的配置信息在 "Mounts" Key 下面

"Mounts": [
    {
        "Type": "bind",
        "Source": "/src/webapp",
        "Destination": "/opt/webapp",
        "Mode": "",
        "RW": true,
        "Propagation": "rprivate"
    }
],

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

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

docker run --rm -it \
   # -v $HOME/.bash_history:/root/.bash_history \
   --mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history \
   ubuntu:17.10 \
   bash
root@2affd44b4667:/# history
1  ls
2  diskutil list   

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

posted @ 2020-10-01 10:05  坐井  阅读(395)  评论(0编辑  收藏  举报