Docker | 入门

Docker | 入门

此文用于总结主要在ios系统下学习Docker的记录。
2021/12/11, to be continued with Dockerfile指令详解 ENTRYPOINT 入口点, 场景二:应用运行前的准备工作





简介

概念

什么是Docker?

Docker 使用 Google 公司推出的 Go 语言 进行开发实现,基于 Linux 内核的 cgroup,namespace,以及 OverlayFS 类的 Union FS 等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。

由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。

最初实现是基于 LXC,从 0.7 版本以后开始去除 LXC,转而使用自行开发的 libcontainer,从 1.11 版本开始,则进一步演进为使用 runC 和 containerd。

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

runc 是一个 Linux 命令行工具,用于根据 OCI容器运行时规范创建和运行容器。
containerd 是一个守护程序,它管理容器生命周期,提供了在一个节点上执行容器和管理镜像的最小功能集。

下面对Docker 和传统虚拟机技术进行了对比:
在这里插入图片描述

特性DockerVirtual Machines
运行容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此,容器要比传统虚拟机更为轻便。虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程。这将非常占用系统资源的。
启动秒级分钟级
硬盘使用一般为MB一般为GB
性能接近原生弱于
系统支持量单机支持上千个容器一般几十个

优势

为什么是Docker?

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

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

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

  • 持续交付和部署
    对开发和运维(DevOps)人员来说,最希望的就是一次创建或配置,可以在任意地方正常运行。
    使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。开发人员可以通过 Dockerfile 来进行镜像构建,并结合 持续集成(Continuous Integration) 系统进行集成测试,而运维人员则可以直接在生产环境中快速部署该镜像,甚至结合 持续部署(Continuous Delivery/Deployment) 系统进行自动部署。
    使用 Dockerfile 使镜像构建透明化,不仅仅开发团队可以理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好的生产环境中部署该镜像。

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

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



部署

Docker 部署

详见参考文章中的链接🔗

  • 查看已安装成功的Docker版本
% docker --version
Docker version 20.10.11, build dea9396

Redis 部署

详见参考文章中的链接🔗

  • 查看可用的 Redis 版本
% docker search redis
NAME                             DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
redis                            Redis is an open source key-value store that…   10258     [OK]       
sameersbn/redis                                                                  83                   [OK]
grokzen/redis-cluster            Redis cluster 3.0, 3.2, 4.0, 5.0, 6.0, 6.2      80                   
rediscommander/redis-commander   Alpine image for redis-commander - Redis man…   70                   [OK]
......


概念

Docker的整个生命周期由三部分组成:镜像(Image),容器(Container)和仓库(Repository)。

在这里插入图片描述
打个比方,每台宿主机(电脑)下载好了Docker后可以生成多个镜像,每个镜像可以创建多个容器。发布到仓库时,以镜像为单位。
一个容器就是一个独立的虚拟操作系统,互不影响,而镜像就是这个操作系统的安装包。想要生成一个容器,就用安装包(镜像)生成一次

镜像

操作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。

Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:18.04 就包含了完整的一套 Ubuntu 18.04 最小系统的 root 文件系统。Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。

镜像 不包含 任何动态数据,其内容在构建之后也不会被改变。

分层存储

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

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

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

容器

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

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

前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为 容器存储层。
容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。

按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。
数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器删除或者重新运行之后,数据却不会丢失。

仓库

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

一个 Docker Registry 中可以包含多个 仓库(Repository);每个仓库可以包含多个 标签(Tag);每个标签对应一个镜像。
通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。
以 Ubuntu 镜像 为例,ubuntu 是仓库的名字,其内包含有不同的版本标签,如,16.04, 18.04。我们可以通过 ubuntu:16.04,或者 ubuntu:18.04 来具体指定所需哪个版本的镜像。如果忽略了标签,比如 ubuntu,那将视为 ubuntu:latest。
仓库名经常以 两段式路径 形式出现,比如 jwilder/nginx-proxy,前者往往意味着 Docker Registry 多用户环境下的用户名,后者则往往是对应的软件名。但这并非绝对,取决于所使用的具体 Docker Registry 的软件或服务。

Docker Registry

公开服务

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

最常使用的 Registry 公开服务是官方的 Docker Hub,这也是默认的 Registry,并拥有大量的高质量的官方镜像。
除此以外,还有 Red Hat 的 Quay.io;Google 的 Google Container Registry,Kubernetes 的镜像使用的就是这个服务;代码托管平台 GitHub 推出的 ghcr.io。

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

私有服务

除了使用公开服务外,用户还可以在本地搭建私有 Docker Registry。Docker 官方提供了 Docker Registry 镜像,可以直接使用做为私有 Registry 服务。在私有仓库 一节中,会有进一步的搭建私有 Registry 服务的讲解。
开源的 Docker Registry 镜像只提供了 Docker Registry API 的服务端实现,足以支持 docker 命令,不影响使用。但不包含图形界面,以及镜像维护、用户管理、访问控制等高级功能。
除了官方的 Docker Registry 外,还有第三方软件实现了 Docker Registry API,甚至提供了用户界面以及一些高级功能。比如,Harbor 和 Sonatype Nexus。

使用镜像

镜像是 Docker 的三大组件之一。Docker 运行容器前需要本地存在对应的镜像,如果本地不存在该镜像,Docker 会从镜像仓库下载该镜像。更多关于镜像的内容,包括:从仓库获取镜像;管理本地主机上的镜像;介绍镜像实现的基本原理。

获取镜像

Docker Hub 上有大量的高质量的镜像可以用,下面讲述如何利用命令获取、运行镜像并给出一个示例。

获取

‘docker pull’ 命令可从 Docker 镜像仓库获取镜像。其命令格式为:

$ docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
元素Docker Pull 命令
选项可通过 docker pull --help 命令看到
Docker 镜像仓库地址一般是 <域名/IP>[:端口号]。
默认地址是 Docker Hub (docker.io)。
仓库名两段式名称,即 <用户名>/<软件名>。
对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。

运行

有了镜像后,我们就能够以这个镜像为基础启动并运行一个容器,其命令格式为:

$ docker run -it --rm 仓库名[:标签] 命令
元素Docker Run 命令
-it这是两个参数,一个是 -i:交互式操作,一个是 -t 终端。
如果打算进入 bash 执行一些命令并查看返回结果,则需要交互式终端,即-it。
- - rm容器退出后随之将其删除。
默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 docker rm。
在这里我们只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用 --rm 可以避免浪费空间。
仓库名指以哪一个镜像为基础来启动容器。
命令放在镜像名后的是命令。
在这里如果使用交互式 Shell,则用的是 bash。进入容器后,我们可以在 Shell 下操作,执行任何所需的命令。比如,执行cat /etc/os-release(Linux 常用的查看当前系统版本的命令),从返回的结果可以看到容器内是 什么系统。最后可通过 exit 退出了这个容器。

示例

% docker pull ubuntu:18.04
18.04: Pulling from library/ubuntu
18.04: Pulling from library/ubuntu
f46992f278c2: Pull complete 
Digest: sha256:0fedbd5bd9fb72089c7bbca476949e10593cebed9b1fb9edf5b79dbbacddd7d6
Status: Downloaded newer image for ubuntu:18.04
docker.io/library/ubuntu:18.04 

在上面所示的命令中:

  • 无 Docker 镜像仓库地址,因此将会从 Docker Hub (docker.io)获取镜像。
  • 镜像名称ubuntu:18.04,因此将会获取官方镜像 library/ubuntu 仓库中标签为 18.04 的镜像。
    docker pull 命令的输出结果最后一行给出了镜像的完整名称,即: docker.io/library/ubuntu:18.04。

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

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

接下来,执行下面所示的命令以启动上面获取的ubuntu里面的 bash 并且进行交互式操作:

% docker run -it --rm ubuntu:18.04 bash
root@e529fe617dd7:/# cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.6 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.6 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic

这里,执行cat /etc/os-release,可查看到容器内当前系统版本为Ubuntu 18.04.1 LTS 系统。
最后可通过 exit 退出容器。

列出镜像

下面讲述如何利用命令列出已经下载下来的镜像

列出

全部

可使用 “docker image ls" 命令列出已下载的顶层镜像,其命令格式为:

$ docker image ls

列表包含了仓库名、标签、镜像 ID、创建时间 以及 所占用的空间,如下所示:
其中仓库名、标签在 “获取镜像” 中已介绍过。
镜像 ID 则是镜像的唯一标识,一个镜像可以对应多个标签。

% docker image ls
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
redis         latest    93f2223d3607   30 hours ago   107MB
ubuntu        18.04     7266638574fb   7 weeks ago    56.6MB
hello-world   latest    18e5af790473   2 months ago   9.14kB

部分

“docker image ls" 加特定参数可列出指定部分镜像,其命令格式如下:

# $ docker image ls + 仓库名
$ docker image ls ubuntu

# 或者,docker image ls + 仓库名[:标签]
$ docker image ls ubuntu:18.04

# 或者,docker image ls -f 
## since = 仓库名[:标签], 列出某仓库之后建立的镜像,e.g 在mongo:3.2之后建立的镜像
$ docker image ls -f since=mongo:3.2
## 如果镜像构建时,定义了 LABEL,还可以通过 LABEL 来过滤
$ docker image ls -f label=com.example.version=0.1
虚悬

可使用如下所示的命令格式专门显示虚悬镜像,

$ docker image ls -f dangling=true

虚悬镜像(dangling image),即原本是有镜像名和标签的,比如原来为 mongo:3.2,随着官方镜像维护,发布了新版本后,重新 docker pull mongo:3.2 时,mongo:3.2 这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,新旧镜像同名,从而成为了一种仓库名、标签均为的镜像。

除了 docker pull 可能导致这种情况,docker build 也同样可以导致这种现象。

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

$ docker image prune

中间层

默认的 “docker image ls” 列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 -a 参数,其命令格式如下:

$ docker image ls -a

为了加速镜像构建、重复利用资源,Docker 会利用中间层镜像。所以在使用一段时间后,可能会看到一些依赖的无标签的中间层镜像。

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

自定义

默认情况下,docker image ls 会输出一个完整的表格,但是我们并非所有时候都会需要这些内容:可能对表格的结构不满意,希望自己组织列;或者不希望有标题,这样方便其它程序解析结果等。

# 列出所有镜像
# % docker image ls
# REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
# redis         latest    93f2223d3607   32 hours ago   107MB
# ubuntu        18.04     7266638574fb   7 weeks ago    56.6MB
# hello-world   latest    18e5af790473   2 months ago   9.14kB

# 列出只包含镜像ID和仓库名的结果
% docker image ls --format "{{.ID}}: {{.Repository}}"
93f2223d3607: redis
7266638574fb: ubuntu
18e5af790473: hello-world

# 以表格等距显示,并且有标题行,和默认一样,不过自己定义列
% docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
IMAGE ID       REPOSITORY    TAG
93f2223d3607   redis         latest
7266638574fb   ubuntu        18.04
18e5af790473   hello-world   latest

体积

可使用 ”docker system df“ 命令来便捷的查看镜像、容器、数据卷所占用的空间,如下所示:

% docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          3         2         163.9MB   56.63MB (34%)
Containers      2         1         11B       0B (0%)
Local Volumes   1         1         105B      0B (0%)
Build Cache     0         0         0B        0B

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

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

删除本地镜像

下面讲述如何利用命令按照需求删除相应镜像

单次删除:ID、名或摘要

可使用 “docker image rm" 命令删除本地镜像,其命令格式为:

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

<镜像> 可以是 镜像ID、镜像名或者镜像摘要。
ID 和摘要是其唯一标识,而一个镜像可以有多个标签。

  • 镜像ID:
    docker image ls 列出的 “IMAGE ID” 默认为短 ID 了,一般取前3个字符以上,只要足够区分于别的镜像就可以了。

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

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

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

  • 镜像名:
    可以用镜像名,也就是 <仓库名>:<标签>,来删除镜像。
  • 镜像摘要:
    更精确地是使用镜像摘要删除镜像。
# docker image ls列出全部镜像
$ docker image ls
REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
centos                      latest              0584b3d2cf6d        3 weeks ago         196.5 MB
redis                       alpine              501ad78535f0        3 weeks ago         21.03 MB
docker                      latest              cf693ec9b5c7        3 weeks ago         105.1 MB
nginx                       latest              e43d811ce2f4        5 weeks ago         181.5 MB


# 假设要删除 redis:alpine 镜像,可在docker image ls列出的列表中看到redis:apline的IMAGE ID的头3位可作为所有镜像的IMAGE ID中的唯一识别码,可以根据IMAGE ID执行:
$ docker image rm 501
Untagged: redis:alpine
Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
......


# 假设要删除 centos 镜像,可以根据镜像名执行:
$ docker image rm centos
Untagged: centos:latest
Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a
......


# 假设要根据镜像摘要删除镜像,可以根据摘要执行:
$ docker image ls --digests
REPOSITORY                  TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
node                        slim                sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228   6e0c4c8e3913        3 weeks ago         214 MB

$ docker image rm node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228

批量删除:docker image ls -q

可使用 “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)

使用Docker Commit理解镜像构成

下面以定制1个web服务器为例讲述镜像是如何构建的

当一个容器被运行时(如果不使用卷的话),任何文件修改都会被记录于容器存储层里。
Docker 提供了一个 docker commit 命令,可将容器的存储层保存下来成为镜像。即,在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。当这个新镜像被运行时,就会拥有原有容器最后的文件变化。docker commit 的语法格式为:

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

镜像是容器的基础,每次执行 docker run 的时候都会指定哪个镜像作为容器运行的基础。
镜像来源可以是Docker Hub以满足需求,或根据需求定制镜像 (不要使用docker commit (除了学习以外,还可应用于被入侵后保存现场)定制镜像,定制镜像应该使用Dockerfile来完成)

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

示例

  • 启动容器
    执行下面的命令,此命令会用 nginx 镜像启动一个容器并命名为 webserver,映射80 端口,可以浏览器(如果是在本机运行的 Docker,可以直接浏览器访问 http://localhost;如果是在虚拟机、云服务器上安装的 Docker,则浏览器访问 http://虚拟机地址 / 实际云服务器地址)去访问这个 nginx 服务器,并看到如下所示的默认 Nginx 欢迎页面。
% docker run --name webserver -d -p 80:80 nginx
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
latest: Pulling from library/nginx
968621624b32: Already exists 
1b76794ee387: Pull complete 
1f4b2852cc08: Pull complete 
13a4a8fe840d: Pull complete 
4aa4aad304db: Pull complete 
69ec42e2d6af: Pull complete 
Digest: sha256:9522864dd661dcadfd9958f9e0de192a1fdda2c162a35668ab6ac42b465f0603
Status: Downloaded newer image for nginx:latest
f3aec95bc3e7a2bc69d0974e2f86ed601467607b885289cc3a38ba0900c31259

在这里插入图片描述

  • 定制容器
    执行 “docker exec” 命令,以交互式终端方式进入 webserver 容器,并执行了 bash 命令,也就是获得一个可操作的 Shell,修改容器的文件,即改动了容器的存储层,并看到如下所示的定制页面。
% docker exec -it webserver bash
root@f3aec95bc3e7:/# echo '<h1> Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
root@f3aec95bc3e7:/# exit
exit

在这里插入图片描述

  • 查看容器存储层变化
    执行 “docker diff" + 容器名 的命令以查看具体的改动
% docker diff webserver
......
C /run
A /run/nginx.pid
  • 定制镜像
% docker commit \
> --author "xueshan <xueshan@docker.com>" \
> --message "container modify" \
> webserver \
> nginx:v2
sha256:153fac2b8d06619d50ccb8ed5da2be30cc4c17bb85885465fd9d3bf32fc199d9

% docker image ls nginx
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
nginx        v2        153fac2b8d06   14 hours ago   134MB
nginx        latest    63eb316dc556   2 days ago     134MB

% docker history nginx:v2
IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
153fac2b8d06   14 hours ago   nginx -g daemon off;                            1.19kB    container modify
63eb316dc556   2 days ago     /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon…   0B        
<missing>      2 days ago     /bin/sh -c #(nop)  STOPSIGNAL SIGQUIT           0B        
......
file:002f2f7c6dc806b24…   74.3MB    

docker commit --author: 指定修改的作者
docker commit --message: 记录本次修改的内容 (与git 版本控制相似。可省略留空。)

docker image ls: 查看这个新定制的镜像
docker history: 具体查看镜像内的历史记录,如果比较 nginx:latest 的历史记录,可以借助 “COMMENT" 发现新增了提交的这一层。

  • 运行镜像
% docker run --name web2 -d -p 81:80 nginx:v2
f09db830d33e151da5320dab9a675b4b28f199f29b2655608ab2d52cdbab205e

运行此前定制的镜像并将其命名为新的服务为 web2,并且映射到 81 端口。访问 http://localhost:81 看到结果,其内容应该和之前修改后的 webserver 一样。

提示

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

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

此外,使用 docker commit 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体的操作。这种黑箱镜像的维护工作是非常痛苦的。

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

使用 Dockerfile 定制镜像

使用 docker commit 命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。以 "使用Docker Commit理解景象构成“ 中的定制 nginx 镜像为例,这次使用 Dockerfile 来定制。

镜像的定制实际上就是定制每一层所添加的配置、文件。

如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

FROM 指定基础镜像

所谓定制镜像,即指定一个镜像作为基础(FROM命令可指定基础镜像)并在其上进行定制。就像 “使用Docker Commit理解镜像构成“ 中运行了一个 nginx 镜像的容器,再进行修改一样。

一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。

在 Docker Hub 上有非常多的高质量的官方镜像:

  • 直接拿来使用的服务类的镜像,如 nginx、redis、mongo、mysql、httpd、php、tomcat 等;
  • 方便开发、构建、运行各种语言应用的镜像,如 node、openjdk、python、ruby、golang 等;
  • 更为基础的操作系统镜像,如 ubuntu、debian、centos、fedora、alpine 等(这些操作系统的软件库为我们提供了更广阔的扩展空间);
  • 虚拟镜像 “scratch"(并不实际存在,表示一个空白的镜像)作为基础镜像,意味着不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
    不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。
    Ps: 使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

RUN 执行命令

RUN 指令是用来执行命令行命令的。其格式有两种:

  • shell 格式:RUN <命令>
    比如如下所示的RUN语句
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

  • exec 格式:RUN [“可执行文件”, “参数1”, “参数2”]

RUN 可以像 Shell 脚本一样执行命令,但也不可以像 Shell 脚本一样把每个命令对应一个 RUN 。Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,都会新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像,即创建了多层镜像。这是完全没有意义的,而且很多运行时不需要的东西也被装进了镜像里,比如编译环境、更新的软件包等等,这意味着镜像是多层且臃肿的。不仅仅增加了构建部署的时间,也很容易出错。

可以使用一个 RUN 指令,并使用 && 将各个所需命令串联起来,将之前的多层简化为1层。

为了格式化可以Shell 类的行尾添加 \ 的命令换行,以及行首 # 进行注释的格式。

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

# 不推荐这种每个命令对应一个 RUN 的写法
FROM debian:stretch

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.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

# 推荐使用一个 RUN 指令,并使用 && 将各个所需命令串联起来,将之前的多层简化为1层,避免造成镜像臃肿。
FROM debian:stretch

RUN set -x; buildDeps='gcc libc6-dev make wget' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.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

BUILD 构建镜像

可使用 ”docker build“ 命令进行镜像构建,其命令格式为:

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

镜像构建上下文(Context)

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 引擎以帮助构建镜像。

理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 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 的用法

使用URL进行构建
Git repo

Docker Build也支持从 URL 构建,比如可以直接从 Git repo 中构建,如下所示:

$ docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world

# 这行命令指定了构建所需的 Git repo,并且指定分支为 master,构建目录为 /amd64/hello-world/,
# 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
    创建文本文件并命名为 Dockerfile
$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile

使用vim编辑器编辑Dockerfile的文件,输入并保存以下信息:

FROM nginx
RUN echo '<h1>Love & Peace</h1>' > /usr/share/nginx/html/index.html
  • 定制镜像
    在 Dockerfile文件所在目录执行以下命令,指定最终镜像的名称-t nginx:211205,构建成功后,可以像之前运行nginx:v2来运行这个镜像,其结果相同。

docker build 命令最后有一个 “.” 表示当前目录,而 Dockerfile 就在当前目录,然而这并非在指定 Dockerfile 所在路径,而是在指定上下文路径。

% docker build -t nginx:1205 . 
[+] Building 0.1s (6/6) FINISHED                                                
 => [internal] load build definition from Dockerfile                       0.0s
 => => transferring dockerfile: 369B                                       0.0s
 => [internal] load .dockerignore                                          0.0s
 => => transferring context: 2B                                            0.0s
 => [internal] load metadata for docker.io/library/nginx:latest            0.0s
 => [1/2] FROM docker.io/library/nginx                                     0.0s
 => CACHED [2/2] RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/ht  0.0s
 => exporting to image                                                     0.0s
 => => exporting layers                                                    0.0s
 => => writing image sha256:20965ecda05b605099fe787c227d27d7d43f71fe5fa6d  0.0s
 => => naming to docker.io/library/nginx:211205                            0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
  • 运行镜像
% docker run --name web3 -d -p 82:80 nginx:1205
9ac0f96da21a3785ee4a5039396bc818943d3a4385efa6dfeb28beb85f465f4e
# 浏览器登录http://localhost:82以查看效果

Dockerfile 指令详解

FROM			基础镜像
MAINTAINER		镜像作者信息
RUN				容器需要运行的指令
EXPOSE			容器对外端口号
WORKDIR			进入镜像后的登陆目录
ENV				用来构建镜像过程中设置环境变量
ADD				将宿主机文件拷贝进镜像且ADD会自动解压缩包和处理URL链接
COPY			将从构建上下文目录中<源路径>的文件/目录复制到新的一层的镜像内的<目标路径>
VOLUME			容器数据卷,用于数据保存和持久化
CMD				指定一个容器启动时要运行的命令(dockerfile中可以写多个CMD指令,但只有最后一个生效,CMD会被docker run之后的参数替换)
ENTRYPOINT		指定一个容器启动时要运行的命令(和CMD一样,都是在指定容器启动程序及参数)
ONBUILD			当构建一个被继承的dockerfile时运行命令,父镜像在被子继承后父镜像的onbuild被触发

在这里插入图片描述

COPY 复制文件

COPY 源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。
COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。
其有2种命令格式:

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

–chown=: 改变文件的所属用户及所属组。比如:
COPY --chown=55:mygroup files* /mydir/
COPY --chown=bin files* /mydir/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/


<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则;
如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。比如:
COPY hom* /mydir/
COPY hom?.txt /mydir/


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


ADD 更高级的复制文件

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

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

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

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

在 Docker 官方的 Dockerfile 最佳实践文档 中要求,尽可能的使用 COPY,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。
另外需要注意的是,ADD 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。

在 COPY 和 ADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD。
在使用该指令的时候还可以加上 --chown=: 选项来改变文件的所属用户及所属组。

举例如下,

ADD --chown=55:mygroup files* /mydir/
ADD --chown=bin files* /mydir/
ADD --chown=1 files* /mydir/
ADD --chown=10:11 files* /mydir/

CMD 容器启动命令

Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。

可使用以下的命令格式启动指定默认的容器主进程,其命令格式为:

CMD <命令>									# shell 格式
# 或者,
CMD ["可执行文件", "参数1", "参数2"...]		# exec 格式
# 参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。
# 在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。

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

如果使用 shell 格式的话,实际的命令会被包装为sh -c的参数的形式进行执行。
比如,CMD echo $HOME,在实际执行中,会将其变更为,CMD [ "sh", "-c", "echo $HOME" ],这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。

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

  • 错误示范:
    CMD service nginx start
    原本是希望 upstart 来以后台守护进程形式启动 nginx 服务。
    但是CMD service nginx start会被理解为CMD [ "sh", "-c", "service nginx start"],因此主进程实际上是 sh。则当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。
    对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

  • 正确示范:
    CMD ["nginx", "-g", "daemon off;"]
    直接执行 nginx 可执行文件,并且要求以前台形式运行。

ENTRYPOINT 入口点

ENTRYPOINT 用以在指定容器启动程序及参数。
ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run --entrypoint来指定。

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

应用场景
1. 让镜像变成像命令一样使用
错误示范: 未使用ENTRYPOINT
# 使用docker images查看列出已存在镜像信息
% docker images
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
......
ubuntu        latest    d5ca7a445605   8 weeks ago    65.6MB


# 在目标路径下创建Dockerfile
% mkdir Downloads/ip_docker_noentrypoint                        
% touch Downloads/ip_docker_noentrypoint/Dockerfile             
% vim Downloads/ip_docker_noentrypoint/Dockerfile       
// 在vim编辑器界面输入以下内容
FROM ubuntu
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://myip.ipip.net" ]
//


# 构建镜像并命名为myip:20211211     
% docker build Downloads/ip_docker_noentrypoint -t myip:20211211
[+] Building 0.1s (6/6) FINISHED                                                                                                                                                                          
 => [internal] load build definition from Dockerfile                                                                                                                                                 0.0s
 => => transferring dockerfile: 190B                                                                                                                                                                 0.0s
 => [internal] load .dockerignore                                                                                                                                                                    0.0s
 => => transferring context: 2B                                                                                                                                                                      0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                                                                                                                                     0.0s
 => [1/2] FROM docker.io/library/ubuntu                                                                                                                                                              0.0s
 => CACHED [2/2] RUN apt-get update     && apt-get install -y curl     && rm -rf /var/lib/apt/lists/*                                                                                                0.0s
 => exporting to image                                                                                                                                                                               0.0s
 => => exporting layers                                                                                                                                                                              0.0s
 => => writing image sha256:90cedeeeb6bcb0f52d53aa53d4b5d5608bc943a8aff3f9bdd521e1743da42e70                                                                                                         0.0s
 => => naming to docker.io/library/myip:20211211                                                                                                                                                     0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them


# 再次使用docker images查看列出已存在镜像信息, myip:20211211已成功构建
% docker images
REPOSITORY    TAG        IMAGE ID       CREATED         SIZE
myip          20211211   90cedeeeb6bc   3 minutes ago   80.9MB
......
ubuntu        latest     d5ca7a445605   8 weeks ago     65.6MB


# 运行镜像
% docker run myip 
Unable to find image 'myip:latest' locally
docker: Error response from daemon: pull access denied for myip, repository does not exist or may require 'docker login': denied: requested access to the resource is denied.
See 'docker run --help'.

% docker run myip:20211211
当前 IP:114.91.24.162  来自于:中国 上海 上海  电信

# 上面的 CMD 中可以看到实质的命令是 curl,那么如果我们希望显示 HTTP 头信息,就需要加上 -i 参数。
# 如果直接加 -i 参数,可以看到可执行文件找不到的报错,executable file not found。
# 之前我们说过,跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。
# 因此这里的 -i 替换了原来的 CMD,而不是添加在原来的 curl -s http://myip.ipip.net 后面。
# 而 -i 根本不是命令,所以自然找不到。
% docker run myip:20211211 -i
docker: Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: exec: "-i": executable file not found in $PATH: unknown.
ERRO[0000] error waiting for container: context canceled 

# 如果希望加入 -i 这参数,则必须重新完整的输入这个命令:
% docker run myip:20211211 curl -s http://myip.ipip.net -i
HTTP/1.1 200 OK
Date: Sat, 11 Dec 2021 06:42:53 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 68
Connection: keep-alive
X-Shadow-Status: 200
X-Via-JSL: 1cf16e4,8592f33,-
Set-Cookie: __jsluid_h=4143f92fde2a13b12f82bfa7fc8e6268; max-age=31536000; path=/; HttpOnly
X-Cache: bypass

当前 IP:114.91.24.162  来自于:中国 上海 上海  电信 

正确示范:使用ENTRYPOINT
# 使用docker images查看列出已存在镜像信息
% docker images
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
......
ubuntu        latest    d5ca7a445605   8 weeks ago    65.6MB
......


# 进入目标路径下并修改先前创建的Dockerfile中的内容
% cd Downloads/                              
% ls -lh                                                         
total 0
drwxr-xr-x   3 96B Dec 11 15:33 ip_docker_noentrypoint
% vim ip_docker_noentrypoint/Dockerfile     
// 在vim编辑器界面输入以下内容
FROM ubuntu
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]
//


# 构建镜像并命名为myip:20211211_ENTRYPOINT
% docker build ip_docker_noentrypoint -t myip:20211211_ENTRYPOINT
[+] Building 0.1s (6/6) FINISHED                                                                                                                                                                          
 => [internal] load build definition from Dockerfile                                                                                                                                                 0.0s
 => => transferring dockerfile: 197B                                                                                                                                                                 0.0s
 => [internal] load .dockerignore                                                                                                                                                                    0.0s
 => => transferring context: 2B                                                                                                                                                                      0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                                                                                                                                     0.0s
 => [1/2] FROM docker.io/library/ubuntu                                                                                                                                                              0.0s
 => CACHED [2/2] RUN apt-get update     && apt-get install -y curl     && rm -rf /var/lib/apt/lists/*                                                                                                0.0s
 => exporting to image                                                                                                                                                                               0.0s
 => => exporting layers                                                                                                                                                                              0.0s
 => => writing image sha256:4920e28aef3447183755c918d0e574638a9f014c413440fb9cd5162145337518                                                                                                         0.0s
 => => naming to docker.io/library/myip:20211211_ENTRYPOINT                                                                                                                                          0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them


# 再次使用docker images查看列出已存在镜像信息, myip:20211211_ENTRYPOINT已成功构建
% docker images
REPOSITORY    TAG                   IMAGE ID       CREATED             SIZE
myip          20211211_ENTRYPOINT   4920e28aef34   About an hour ago   80.9MB
......
ubuntu        latest                d5ca7a445605   8 weeks ago         65.6MB
......


# 运行镜像
% docker run myip:20211211_ENTRYPOINT
当前 IP:114.91.24.162  来自于:中国 上海 上海  电信

# 当存在 ENTRYPOINT 后,CMD 的内容将会作为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,因此会作为参数传给 curl。
% docker run myip:20211211_ENTRYPOINT -i
HTTP/1.1 200 OK
Date: Sat, 11 Dec 2021 07:48:57 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 68
Connection: keep-alive
X-Via-JSL: 1cf16e4,-
Set-Cookie: __jsluid_h=03794a9e41585591f4a5ec191e89c74a; max-age=31536000; path=/; HttpOnly
X-Cache: bypass

当前 IP:114.91.24.162  来自于:中国 上海 上海  电信

2. 应用运行前的准备工作

ENV 设置环境变量

ARG 构建参数

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

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

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

灵活的使用 ARG 指令,能够在不修改 Dockerfile 的情况下,构建出不同> 的镜像。

ARG 指令有生效范围:

# 使用这个Dockerfile会发现无法输出${DOCKER_USERNAME}变量的值。
ARG DOCKER_USERNAME=library		 // 只在该```FROM```中生效
FROM ${DOCKER_USERNAME}/alpine
RUN set -x ; echo ${DOCKER_USERNAME} 
ARG DOCKER_USERNAME=library		 // 在每个```FROM```中生效
FROM ${DOCKER_USERNAME}/alpine
RUN set -x ; echo 1
FROM ${DOCKER_USERNAME}/alpine
RUN set -x ; echo 2
ARG DOCKER_USERNAME=library 	 // 在```FROM```中生效
FROM ${DOCKER_USERNAME}/alpine
ARG DOCKER_USERNAME=library		 // 也在```FROM```后也生效
RUN set -x ; echo ${DOCKER_USERNAME}
ARG DOCKER_USERNAME=library		 // 在各个阶段中使用的变量
FROM ${DOCKER_USERNAME}/alpine   // 都必须在每个阶段分别指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME}
FROM ${DOCKER_USERNAME}/alpine
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME}

VOLUME 定义匿名卷

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

容器运行时应该尽量保持容器存储层不发生写操作。
所以对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。

为防止运行时用户忘记将动态文件所保存目录挂载为卷,在 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的端口。

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

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 指令。
正确示范:
% cd Downloads
Downloads % mkdir WD
Downloads % touch WD/Dockerfile
Downloads % vim WD/Dockerfile 
###
FROM ubuntu:latest

MAINTAINER xueshan

RUN mkdir -p /newdir && echo hello Docker 2021 > /newdir/HelloDocker.txt

WORKDIR /newdir

CMD ["more" ,"HelloDocker.txt"]
###
Downloads % docker build WD -t wdexample
[+] Building 0.3s (7/7) FINISHED                                                
 => [internal] load build definition from Dockerfile                       0.0s
 => => transferring dockerfile: 209B                                       0.0s
 => [internal] load .dockerignore                                          0.0s
 => => transferring context: 2B                                            0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest           0.0s
 => CACHED [1/3] FROM docker.io/library/ubuntu:latest                      0.0s
 => [2/3] RUN mkdir -p /newdir && echo hello Docker 2021 > /newdir/HelloD  0.2s
 => [3/3] WORKDIR /newdir                                                  0.0s
 => exporting to image                                                     0.0s
 => => exporting layers                                                    0.0s
 => => writing image sha256:487a8c1336e29ddcddae53851739ea2a48af09b619b1f  0.0s
 => => naming to docker.io/library/wdexample                               0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
Downloads % docker run wdexample
::::::::::::::
HelloDocker.txt
::::::::::::::
hello Docker 2021
Downloads % 

USER 指定当前用户

HEALTHCHECK 健康检查

ONBUILD 为他人作嫁衣裳

LABEL 为镜像添加元数据

SHELL 指令

附录

常用Docker命令

docker history + 指定镜像名 查看该镜像的历史信息
docker image rm -f + 指定镜像名 强制删除


# 参考文章 写本文时有参考以下文章:

博客

前端工程师学 Docker ? 看这篇就够了!
Docker – 从入门到实践
如何通俗解释Docker是什么?
什么是Docker?原理,作用,限制和优势简介
史上讲解最好的 Docker 教程,从入门到精通(建议收藏的教程)


部署

docker~linux下的部署和基本命令

IOS

Install Docker Desktop on Mac
Mac怎么设置docker国内镜像源来加速下载?
MacOS Docker 安装
Docker 安装 Redis
docker login登录非docker hub仓库
Mac系统Vim编辑器教程

Linux

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
Cannot connect to the Docker daemon at unix:/var/run/docker.sock. Is the docker daemon running?
linux环境下在终端使用vim进行编辑、保存、退出等相关操作

使用镜像

Docker error cannot delete docker container, conflict: unable to remove repository reference
bash - 启动容器进程导致 “exec:”/bin/sh": stat/bin/sh: no such file or directory": unknown

Docker指令详解

Dockerfile实例——(实践操作!)

USER

docker 与 gosu
dockerfile-新增用户并赋予sudo权限以及指定密码
groupdel删除组时出现groupdel: cannot remove the primary group of user ‘nginx‘

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值