一文读懂docker原理

说起Docker,基本上就是指容器。许多同学熟悉Docker的操作,却搞不懂到底什么是容器。本文就来讲讲Docker容器到底是个啥。

容器被称为轻量级的虚拟化技术,实际上是不准确的。确切地说,容器是一种对进程进行隔离的运行环境。

由于生产环境的容器几乎都是运行在Linux上的,因此,本文提到的进程、Docker等概念或软件均以Linux平台为准。

我们知道进程是Linux操作系统执行任务的最小单元,一个时间同步服务是一个进程,一个Java服务是一个进程,一个Nginx服务是一个主进程+若干工作进程,总之,把一个系统比作一个办公室,进程就是一个个打工人:

workers

正常情况下,一个进程是能感知到其他进程的存在的,正如一个打工人放眼望去,办公室里还坐着一群其他打工人。进程的唯一标识是进程ID,用数字1、2、3……表示,好比打工人的工牌号,大家都各不一样。

而容器技术首先要解决的就是进程的隔离,即一个进程在运行的时候看不到其他进程。如何让一个打工人在工作时看不到其他打工人呢?方法是给这个打工人带一个VR眼镜,于是他看到的不是一个真实的办公室,而是一个虚拟的办公室。在这个虚拟办公室中,只有他一个打工人,没有别人。在Linux系统中,对一个进程进行隔离,主要是通过Namespace和Cgroup两大机制实现的。一个被隔离的进程,操作系统也会正常分配进程ID,比如12345,但是隔离进程自己看到的ID总是1,好比打工人的工牌是12345,但他自己通过VR眼镜看到的工牌号却是1,感觉自己是1号员工似的:

worker-vr

我们通过一个简单的Python程序就可以验证一下隔离进程的特点。我们编写一个简单的HTTP服务程序app.py,针对URL为//ps/ls分别返回自身进程ID、所有进程ID和磁盘根目录列表。

如果我们正常启动这个Python程序,在浏览器中,可以看到进程ID为10297

normal-pid

用/ps查看所有进程,可以看到1号进程是systemd,还有很多其他进程:

normal-ps

用/ls查看磁盘根目录,与当前系统根目录一致:

normal-ls

现在,我们制作一个Docker镜像,然后以Docker模式启动这个Python服务程序,再看看进程ID:

docker-pid

从进程自己的视角看,它看到的进程ID总是1,并且,用/ps看不到其他进程,只能看到自己:

docker-ps

再用/ls看一下磁盘,看到的也不是系统的根目录,而是Docker给挂载的一个虚拟的文件系统:

docker-ls

但其实从操作系统看,这个Docker进程和其他进程一样,也有一个唯一的进程ID为10475

ps

所以我们可以得出结论:

一个容器进程本质上是一个运行在沙盒中的隔离进程,由Linux系统本身负责隔离,Docker只是提供了一系列工具,帮助我们设置好隔离环境后,启动这个进程。

最基本的隔离就是进程之间看不到彼此,这是由Linux的Namespace机制实现的。进程隔离的结果就是以隔离方式启动的进程看到的自身进程ID总是1,且看不到系统的其他进程。

第二种隔离就是隔离系统真实的文件系统。Docker利用Linux的mount机制,给每个隔离进程挂载了一个虚拟的文件系统,使得一个隔离进程只能访问这个虚拟的文件系统,无法看到系统真实的文件系统。至于这个虚拟的文件系统应该长什么样,这就是制作Docker镜像要考虑的问题。比如我们的Python程序要正常运行,需要一个Python3解释器,需要把用到的第三方库如psutil引入进来,这些复杂的工作被简化为一个Dockerfile,再由Docker把这些运行时的依赖打包,就形成了Docker镜像。我们可以把一个Docker镜像看作一个zip包,每启动一个进程,Docker都会自动解压zip包,把它变成一个虚拟的文件系统。

第三种隔离就是网络协议栈的隔离,这个最不容易理解。

我们举个例子:在Docker中运行docker run redis:latest,然后在宿主机上写个程序连接127.0.0.1:6379,是无法连接到Redis的,因为Redis虽然监听127.0.0.1:6379这个端口,但Linux可以为进程隔离网络,Docker默认启动的Redis进程拥有自己的网络名字空间,与宿主机不同:

1
2
3
4
5
6
┌──────────────┐ ┌─────────────────────────┐
│redis:        │ │app:                     │
│  listen: 6379│ │  connect: 127.0.0.1:6379│
├──────────────┤ ├─────────────────────────┤
│127.0.0.1:6379│ │        127.0.0.1        │
└──────────────┘ └─────────────────────────┘

要让宿主机访问到Redis,可以用-p 6379:6379把Redis进程的端口号映射到宿主机,从而在宿主机上访问Redis:

1
2
3
4
5
6
7
8
9
┌──────────────┐ ┌─────────────────────────┐
│redis:        │ │app:                     │
│  listen: 6379│ │  connect: 127.0.0.1:6379│
├──────────────┤ ├─────────────────────────┤
│127.0.0.1:6379│ │     127.0.0.1:6379      │
└──────────────┘ └─────────────────────────┘
            │                     ▲
            │                     │
            └─────────────────────┘

因此,在Linux的网络名字空间隔离下,Redis进程和宿主机进程看到的IP地址127.0.0.1表面上一样,但实际上是不同的网络接口。

我们再看一个更复杂的例子。如果我们要运行ZooKeeper和Kafka,先启动ZooKeeper:

1
docker run -p 2181:2181 zookeeper:latest

再启动Kafka,发现Kafka是无法连接ZooKeeper的,原因是,Kafka试图连接的127.0.0.1:2181在它自己的网络接口上并不存在:

1
2
3
4
5
6
7
8
9
┌──────────────┐ ┌──────────────┐ ┌─────────────────────────────┐
│zookeeper:    │ │kafka:        │ │Host                         │
│  listen: 2181│ │  listen: 9092│ │                             │
├──────────────┤ ├──────────────┤ ├──────────────┬──────────────┤
│127.0.0.1:2181│ │127.0.0.1:9092│ │127.0.0.1:9092│127.0.0.1:2181│
└──────────────┘ └──────────────┘ └──────────────┴──────────────┘
            │                │                ▲              ▲
            │                └────────────────┘              │
            └────────────────────────────────────────────────┘

必须连接到ZooKeeper的IP:2181或者宿主机的IP:2181。直接指定IP并不是一个好的方式,我们应该利用Docker Compose,把ZooKeeper和Kafka运行在同一个网络名字空间里,并通过zookeeper:2181来访问ZooKeeper端口,让Docker自动把zookeeper名字解析为动态分配的IP地址。docker-compose.yml参考配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
version: "3"
services:
  zookeeper:
    image: bitnami/zookeeper:3.5
    container_name: zookeeper
    ports:
      - "2181:2181"
    environment:
      - ALLOW_ANONYMOUS_LOGIN=yes
    volumes:
      - "./docker/zookeeper-data:/bitnami"

  kafka:
    image: bitnami/kafka:3.0
    container_name: kafka
    ports:
      - "9092:9092"
    depends_on:
      - zookeeper
    environment:
      - KAFKA_BROKER_ID=1
      - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092
      - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092
      - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
      - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true
      - ALLOW_PLAINTEXT_LISTENER=yes
    volumes:
      - "./docker/kafka-data:/bitnami"

搞懂了容器的运行原理,我们才能更好地掌握Docker命令。掌握了Docker,才能进一步学习K8s——云原生操作系统。

概述

Docker是一个开源项目,诞生于2013年初,最初是dotCloud公司内部的一个业余项目。它基于Google公司推出的Go语言实现。项目后来加入了Linux基金会,遵从了Apache2.0协议,项目代码在GitHub上进行维护。
Docker项目的目标是实现轻量级的操作系统虚拟化解决方案。Docker的基础是Linux容器(LXC)等技术。在LXC的基础上Docker进行了进一步封装,让用户不用去关心容器的管理,使得操作更为简便,用户操作docker容器就像操作一个轻量级虚拟机一样简单。
下面的图片比较了 Docker 和传统虚拟化方式的不同之处,可见docker是在操作系统上实现虚拟化,直接复用本地主机的操作系统,而传统方式则是在硬件层面实现。

为什么使用docker

作为一种新兴的虚拟化方式,docker跟传统的虚拟化方式相比具备众多的优势。

  • 更快速的交付和部署
    Docker在整个开发周期都可以完美的辅助你实现快速交付。Docker允许开发者在装有应用和服务的本地容器中做开发,可以直接集成到可持续开发流程中。
    例如:开发者可以使用一个标准的镜像来构建一套开发容器,开发完成之后,运维人员可以直接使用这个容器来部署代码。 Docker 可以快速创建容器,快速迭代应用程序,并让整个过程全程可见,使团队中的其他成员更容易理解应用程序是如何创建和工作的。 Docker 容器很轻很快!容器的启动时间是秒级的,大量地节约开发、测试、部署的时间。

  • 高效的部署和扩容
    Docker 容器几乎可以在任意的平台上运行,包括物理机、虚拟机、公有云、私有云、个人电脑、服务器等。 这种兼容性可以让用户把一个应用程序从一个平台直接迁移到另外一个。
    Docker的兼容性和轻量特性可以很轻松的实现负载的动态管理。你可以快速扩容或方便的下线的你的应用和服务,这种速度趋近实时。

  • 更高的资源利用率
    Docker 对系统资源的利用率很高,一台主机上可以同时运行数千个 Docker 容器。容器除了运行其中应用外,基本不消耗额外的系统资源,使得应用的性能很高,同时系统的开销尽量小。传统虚拟机方式运行 10 个不同的应用就要起 10 个虚拟机,而Docker 只需要启动 10 个隔离的应用即可。

  • 更简单的管理
    使用 Docker,只需要小小的修改,就可以替代以往大量的更新工作。所有的修改都以增量的方式被分发和更新,从而实现自动化并且高效的管理。

举个最简单的例子:
传统的javaWeb应用是部署在tomcat中的,一个服务器可能运行多个tomcat,每个tomcat中有一个或多个web应用,那么此时docker就可以起到应用隔离和资源独立的作用。

  • 如果不使用docker,一台服务器上多个tomcat,即A1,所有的应用共享服务器的CPU、内存等资源,这时如果一个应用出现问题,导致CPU爆满等等,其他应用也会无法运行。
  • 那么这时就需要做到资源独立,一个应用一份资源,即我们所说的传统的虚拟化方式,即A2,这样需要在服务器上安装数个虚拟机,每个虚拟机运行一个应用,这样的缺点是,重启虚拟机太麻烦,迁移应用需要重装虚拟机,配置环境等等。
  • A3即使用docker,此时服务器上运行多个容器,每个容器都拥有独立的CPU、内存等资源,完全满足了应用隔离的需求,重启容器只需要几秒,迁移应用也很方便,所有装了Docker的服务器,只需把镜像pull或者load进去,run,就可以了。


上图是docker的运行流程

docker引擎

docker引擎是一个c/s结构的应用,主要组件见下图:

  • server是一个常驻进程
  • REST API实现了server和client之间的交互协议
  • CLI实现容器和镜像的管理,为用户提供统一的管理界面

docker架构

Docker使用C/S架构,Client 通过接口与Server进程通信实现容器的构建,运行和发布。client和server可以运行在同一台集群,也可以通过跨主机实现远程通信。

核心概念

  • 镜像(Image)

docker镜像就是一个只读的模板,如:一个镜像可以包含一个完整的操作系统环境,里面仅安装了Apache或用户需要的其他应用程序。
镜像可以用来创建docker容器,一个镜像可以创建很多容器。Docker 提供了一个很简单的机制来创建镜像或者更新现有的镜像,用户甚至可以直接从其他人那里下载一个已经做好的镜像来直接使用。

镜像(Image)就是一堆只读层(read-only layer)的统一视角,也许这个定义有些难以理解,看看下面这张图:

右边我们看到了多个只读层,它们重叠在一起。除了最上面一层,其它层都会有一个指针指向下一层。这些层是Docker内部的实现细节,并且能够在docker宿主机的文件系统上访问到。统一文件系统(Union File System)技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一的视角,这样就隐藏了多层的存在,在用户的角度看来,只存在一个文件系统。

  • 仓库(repository)
    仓库是集中存放镜像文件的场所。有时候会把仓库和仓库注册服务器混为一谈,并不严格区分。实际上,仓库注册服务器往往存放着多个仓库,每个仓库又包含了多个镜像,每个镜像有不同的标签(tag)。

仓库分为公开仓库(Public)和私有仓库(Private)两种形式。最大的公开仓库是 Docker Hub,存放了数量庞大的镜像供用户下载。国内的公开仓库包括 时速云 、网易云 等,可以提供大陆用户更稳定快速的访问。当然,用户也可以在本地网络内创建一个私有仓库。
当用户创建了自己的镜像之后就可以使用 push 命令将它上传到公有或者私有仓库,这样下次在另外一台机器上使用这个镜像时候,只需要从仓库上 pull 下来就可以了。
Docker 仓库的概念跟 Git 类似,注册服务器可以理解为 GitHub 这样的托管服务。

  • 容器(container)
    docker利用容器来运行应用。容器时从镜像创建的运行实例。它可以被启动、开始、停止、删除。每个容器都是相互隔离的、保证安全的平台。可以把容器看做是一个简易版的 Linux 环境(包括root用户权限、进程空间、用户空间和网络空间等)和运行在其中的应用程序。
    容器的定义和镜像几乎一模一样,也是一堆层的统一视角,唯一区别在于容器的最上面那一层是可读可写的。

    一个运行态容器被定义为一个可读写的统一文件系统加上隔离的进程空间和包含其中的进程。下面这张图片展示了一个运行中的容器。

    正是文件系统隔离技术使得Docker成为了一个非常有潜力的虚拟化技术。一个容器中的进程可能会对文件进行修改、删除、创建,这些改变都将作用于可读写层。

docker安装部署

  • Docker 要求 CentOS 系统的内核版本高于 3.10 ,查看本页面的前提条件来验证你的CentOS 版本是否支持 Docker 。
    通过 uname -r 命令查看你当前的内核版本
    uname -r

  • 使用 root 权限登录 Centos。确保 yum 包更新到最新。
    sudo yum update

  • 卸载旧版本(如果安装过旧版本的话)
    sudo yum remove docker docker-common docker-selinux docker-engine

  • 安装需要的软件包, yum-util 提供yum-config-manager功能,另外两个是devicemapper驱动依赖的
    sudo yum install -y yum-utils device-mapper-persistent-data lvm2

  • 设置yum源
    这是dockerHub上的镜像源:
    sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    我们使用阿里的镜像源:
    sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

  • 查看仓库中所有docker版本,并选择特定版本安装
    yum list docker-ce --showduplicates | sort -r

  • 安装docker
    sudo yum install docker-ce#由于repo中默认只开启stable仓库,故这里安装的是最新稳定版17.12.0
    可以指定版本安装,如:
    sudo yum install docker-ce-18.03.0.ce

  • 启动并加入开机启动
    sudo systemctl start docker
    sudo systemctl enable docker

  • 验证安装是否成功(有client和service两部分表示docker安装启动都成功了)
    docker version

删除docker

yum remove docker docker-common docker-selinux docker-engine -y
/etc/systemd -name '*docker*' -exec rm -f {} ;
find /etc/systemd -name '*docker*' -exec rm -f {} \;
find /lib/systemd -name '*docker*' -exec rm -f {} \;

docker常用命令

获取镜像

docker pull
从仓库获取所需镜像,示例如下:
docker pull centos:centos6

实际上相当于 docker pull registry.hub.docker.com/centos:centos6
命令,即从注册服务器 registry.hub.docker.com 中的 centos 仓库来下载标记为 centos6 的镜像。

有时候官方仓库注册服务器下载较慢,可以从其他仓库下载。 从其它仓库下载时需要指定完整的仓库注册服务器地址。
docker默认从docker自己的服务器下载镜像,在国内自然是很慢。我们可以将镜像源替换成阿里的源。通过daemon.json这个文件配置镜像。
docker安装后默认没有daemon.json这个配置文件,需要进行手动创建。配置文件的默认路径:/etc/docker/daemon.json
touch /etc/docker/daemon.json
vim /etc/docker/daemon.json
输入如下内容:

1
2
3
{
  "registry-mirrors": ["https://t21sxucj.mirror.aliyuncs.com"]
}

registry-mirrors后边的就是镜像的下载地址,这个地址我是怎么找到的呢?地址失效了怎么办呢?
我们知道阿里云有提供docker镜像服务。

  • 登录阿里云(没有账号先注册)
  • 找到容器镜像服务
  • 点击创建镜像仓库
  • 创建之后点击镜像加速器选项卡
    在右侧可以看到配置镜像加速器,在下边可以看到想要的地址。

配置完之后重启docker
systemctl restart docker
然后再docker pull xxx试试,贼快。
参考1
参考2

查看镜像列表

docker images
列出了所有顶层(top-level)镜像。实际上,在这里我们没有办法区分一个镜像和一个只读层,所以我们提出了top-level镜像。
只有创建容器时使用的镜像或者是直接pull下来的镜像能被称为顶层(top-level)镜像,并且每一个顶层镜像下面都隐藏了多个镜像层。

如:

1
2
3
4
$ docker images
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
centos                   centos6             6a77ab6655b9        8 weeks ago         194.6 MB
ubuntu                   latest              2fa927b5cdd3        9 weeks ago         122 MB

在列出信息中,可以看到几个字段信息

  • 来自于哪个仓库,比如 ubuntu
  • 镜像的标记,比如 latest
  • 它的 ID 号(唯一)
  • 创建时间
  • 镜像大小

利用dockerfile来创建镜像

docker build
使用 docker commit 来扩展一个镜像比较简单,但是不方便在一个团队中分享。我们可以使用docker build 来创建一个新的镜像。为此,首先需要创建一个Dockerfile,包含一些如何创建镜像的指令。新建一个目录和一个Dockerfile。

1
2
3
mkdir test_docker
cd test_docker
touch Dockerfile

Dockerfile 中每一条指令都创建镜像的一层,例如:

1
2
3
4
5
6
7
FROM centos:centos6
MAINTAINER sandywei <sandy@test.tech>
# move all configuration files into container

RUN yum install -y httpd
EXPOSE 80
CMD ["sh","-c","service httpd start;bash"]

dockerfile基本语法:

Dockfile是一个用于编写docker镜像生成过程的文件,其有特定的语法。在一个文件夹中,如果有一个名字为Dockfile的文件,其内容满足语法要求,在这个文件夹路径下执行命令:docker build –tag name:tag .,就可以按照描述构建一个镜像了。name是镜像的名称,tag是镜像的版本或者是标签号,不写就是lastest。注意后面有一个空格和点。
Dockerfile的基本指令有十三个,分别是:FROM、MAINTAINER、RUN、CMD、EXPOSE、ENV、ADD、COPY、ENTRYPOINT、VOLUME、USER、WORKDIR、ONBUILD。下面对这些指令的用法一一说明。

FROM

用法:FROM <image>
说明:第一个指令必须是FROM了,其指定一个构建镜像的基础源镜像,如果本地没有就会从公共库中拉取,没有指定镜像的标签会使用默认的latest标签,可以出现多次,如果需要在一个Dockerfile中构建多个镜像。

MAINTAINER

用法:MAINTAINER <name> <email>
说明:描述镜像的创建者,名称和邮箱

RUN

用法:RUN “command” “param1” “param2”
说明:RUN命令是一个常用的命令,执行完成之后会成为一个新的镜像,这里也是指镜像的分层构建。一句RUN就是一层,也相当于一个版本。这就是之前说的缓存的原理。我们知道docker是镜像层是只读的,所以你如果第一句安装了软件,用完在后面一句删除是不可能的。所以这种情况要在一句RUN命令中完成,可以通过&符号连接多个RUN语句。RUN后面的必须是双引号不能是单引号(没引号貌似也不要紧),command是不会调用shell的,所以也不会继承相应变量,要查看输入RUN “sh” “-c” “echo” “$HOME”,而不是RUN “echo” “$HOME”。

CMD

用法:CMD command param1 param2
说明:CMD在Dockerfile中只能出现一次,有多个,只有最后一个会有效。其作用是在启动容器的时候提供一个默认的命令项。如果用户执行docker run的时候提供了命令项,就会覆盖掉这个命令。没提供就会使用构建时的命令。

EXPOSE

用法:EXPOSE <port> [<port>...]
说明:告诉Docker服务器容器对外映射的容器端口号,在docker run -p的时候生效。

EVN

用法:EVN <key> <value> 只能设置一个

  EVN <key>=<value>允许一次设置多个

说明:设置容器的环境变量,可以让其后面的RUN命令使用,容器运行的时候这个变量也会保留。

ADD

用法:ADD <src> <dest>
说明:复制本机文件或目录或远程文件,添加到指定的容器目录,支持GO的正则模糊匹配。路径是绝对路径,不存在会自动创建。如果源是一个目录,只会复制目录下的内容,目录本身不会复制。ADD命令会将复制的压缩文件夹自动解压,这也是与COPY命令最大的不同。

COPY

用法:COPY <src> <dest>
说明:COPY除了不能自动解压,也不能复制网络文件。其它功能和ADD相同。

ENTRYPOINT

用法:ENTRYPOINT “command” “param1” “param2”
说明:这个命令和CMD命令一样,唯一的区别是不能被docker run命令的执行命令覆盖,如果要覆盖需要带上选项–entrypoint,如果有多个选项,只有最后一个会生效。

VOLUME

用法:VOLUME [“path”]
说明:在主机上创建一个挂载,挂载到容器的指定路径。docker run -v命令也能完成这个操作,而且更强大。这个命令不能指定主机的需要挂载到容器的文件夹路径。但docker run -v可以,而且其还可以挂载数据容器。

USER

用法:USER daemon
说明:指定运行容器时的用户名或UID,后续的RUN、CMD、ENTRYPOINT也会使用指定的用户运行命令。

WORKDIR

用法:WORKDIR path
说明:为RUN、CMD、ENTRYPOINT指令配置工作目录。可以使用多个WORKDIR指令,后续参数如果是相对路径,则会基于之前的命令指定的路径。如:WORKDIR /home  WORKDIR test 。最终的路径就是/home/test。path路径也可以是环境变量,比如有环境变量HOME=/home,WORKDIR $HOME/test也就是/home/test。

ONBUILD

用法:ONBUILD [INSTRUCTION]
说明:配置当前所创建的镜像作为其它新创建镜像的基础镜像时,所执行的操作指令。意思就是,这个镜像创建后,如果其它镜像以这个镜像为基础,会先执行这个镜像的ONBUILD命令。

Dockerfile例子

一个使用安装包安装的tomcat例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
FROM centos
MAINTAINER nobody "xx@qq.com"
RUN mkdir -p /opt/jdk/
RUN mkdir -p /opt/tomcat/
ADD jdk1.7.0_79 /opt/jdk/
ADD tomcat  /opt/tomcat/
ENV CATALINA_HOME /opt/tomcat
ENV JAVA_HOME /opt/jdk
EXPOSE 8080
ENV PATH $PATH:$JAVA_HOME/bin
CMD ["/opt/tomcat/bin/catalina.sh","run"]

更详细的语法说明请参考 https://docs.docker.com/engine/reference/builder/

编写完成 Dockerfile 后可以使用 docker build 来生成镜像。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$ docker build -t test/httpd:1.0 .

Sending build context to Docker daemon 2.048 kB
Step 1 : FROM centos:centos6
 ---> 6a77ab6655b9
Step 2 : MAINTAINER sandywei <sandy@test.tech>
 ---> Running in 1b26493518a7
 ---> 8877ee5f7432
Removing intermediate container 1b26493518a7
Step 3 : RUN yum install -y httpd
 ---> Running in fe5b6f1ef888

 .....

 Step 5 : CMD sh -c service httpd start
 ---> Running in b2b94c1601c2
 ---> 5f9aa91b0c9e
Removing intermediate container b2b94c1601c2
Successfully built 5f9aa91b0c9e

其中 -t 标记来添加 tag,指定新的镜像的用户信息。 “.” 是 Dockerfile 所在的路径(当前目录),
也可以替换为一个具体的 Dockerfile 的路径。注意一个镜像不能超过 127 层。

用docker images 查看镜像列表

1
2
3
4
5
$ docker images
REPOSITORY                 TAG               IMAGE ID            CREATED             SIZE
test/httpd               1.0               5f9aa91b0c9e        3 minutes ago       292.4 MB
centos                   centos6             6a77ab6655b9        8 weeks ago         194.6 MB
ubuntu                   latest              2fa927b5cdd3        9 weeks ago         122 MB

可以发现最后一层的ID(5f9aa91b0c9e)和 image id是一样的

上传镜像

docker push
用户可以通过 docker push 命令,把自己创建的镜像上传到仓库中来共享。例如,用户在 Docker Hub 上完成注册后,可以推送自己的镜像到仓库中。

运行实例:
$ docker push test/httpd:1.0

创建容器

docker create <image-id>
docker create 命令为指定的镜像(image)添加了一个可读写层,构成了一个新的容器。注意,这个容器并没有运行。

docker create 命令提供了许多参数选项可以指定名字,硬件资源,网络配置等等。

如:
创建一个centos的容器,可以使用仓库+标签的名字确定image,也可以使用image-id指定image。返回容器id

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#查看本地images列表
$ docker images

#用仓库+标签
$ docker create -it --name centos6_container centos:centos6

#使用image-id
$ docker create -it --name centos6_container 6a77ab6655b9 bash
b3cd0b47fe3db0115037c5e9cf776914bd46944d1ac63c0b753a9df6944c7a67

#可以使用 docker ps查看一件存在的容器列表,不加参数默认只显示当前运行的容器
$ docker ps -a

可以使用 -v 参数将本地目录挂载到容器中。
$ docker create -it --name centos6_container -v /src/webapp:/opt/webapp centos:centos6
这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径,如果目录不存在 Docker 会自动为你创建它。

docker -v 挂载问题:
Docker容器启动的时候,如果要挂载宿主机的一个目录,可以用-v参数指定。
譬如我要启动一个centos容器,宿主机的/test目录挂载到容器的/soft目录,可通过以下方式指定:
docker run -it -v /test:/soft centos /bin/bash
这样在容器启动后,容器内会自动创建/soft的目录。通过这种方式,我们可以明确一点,即-v参数中,冒号":“前面的目录是宿主机目录,后面的目录是容器内目录。
貌似简单,其实不然,下面我们来验证一下:

  • 容器目录不可以为相对路径
1
2
3
[root@localhost ~]# docker run -it -v /test:soft centos /bin/bash
invalid value "/test:soft" for flag -v: soft is not an absolute path
See 'docker run --help'.

直接报错,提示soft不是一个绝对路径,所谓的绝对路径,必须以下斜线“/”开头。

  • 宿主机目录如果不存在,则会自动生成
    如果宿主机中存在/test目录,首先删除它
1
2
3
[root@localhost ~]# rm -rf /test
[root@localhost ~]# ls /
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

启动容器

1
2
3
[root@localhost ~]# docker run -it -v /test:/soft centos /bin/bash
[root@a487a3ca7997 /]# ls
bin  dev  etc  home  lib  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  soft  srv  sys  tmp  usr  var

查看宿主机,发现新增了一个/test目录

1
2
[root@localhost ~]# ls /
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  test  tmp  usr  var
  • 宿主机的目录如果为相对路径呢?
    这次,我们换个目录名test1试试
    docker run -it -v test1:/soft centos /bin/bash
    再到宿主机上查看是否新增了一个/test1目录,结果没有,是不是因为我用的是相对路径,所以生成的test1目录在当前目录下,结果发现还是没有。
    那容器内的/soft目录挂载到哪里去了?通过docker inspect命令,查看容器“Mounts”那一部分,我们可以得到这个问题的答案。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"Mounts": [
        {
            "Name": "test1",
            "Source": "/var/lib/docker/volumes/test1/_data",
            "Destination": "/soft",
            "Driver": "local",
            "Mode": "z",
            "RW": true
        }
    ],

可以看出,容器内的/soft目录挂载的是宿主机上的/var/lib/docker/volumes/test1/_data目录,原来所谓的相对路径指的是/var/lib/docker/volumes/,与宿主机的当前目录无关。

  • 如果只是-v指定一个目录,这个又是如何对应呢?
    启动一个容器
1
2
3
[root@localhost ~]# docker run -it -v /test2 centos /bin/bash
[root@ea24067bc902 /]# ls
bin  dev  etc  home  lib  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  test2  tmp  usr  var

同样使用docker inspect命令查看宿主机的挂载目录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"Mounts": [
        {
            "Name": "96256232eb74edb139d652746f0fe426e57fbacdf73376963e3acdb411b3d73a",
            "Source": "/var/lib/docker/volumes/96256232eb74edb139d652746f0fe426e57fbacdf73376963e3acdb411b3d73a/_data",
            "Destination": "/test2",
            "Driver": "local",
            "Mode": "",
            "RW": true
        }
    ],

可以看出,同3中的结果类似,只不过,它不是相对路径的目录名,而是随机生成的一个目录名。

  • 如果在容器内修改了目录的属主和属组,那么对应的挂载点是否会修改呢?
    首先开启一个容器,查看容器内/soft目录的属性
1
2
3
[root@localhost ~]# docker run -it -v /test:/soft centos /bin/bash
[root@b5ed8216401f /]# ll -d /soft/
drwxr-xr-x 2 root root 6 Sep 24 03:48 /soft/

查看宿主机内/test目录的属性

1
2
[root@localhost ~]# ll -d /test/
drwxr-xr-x 2 root root 6 Sep 24 11:48 /test/

在容器内新建用户,修改/soft的属主和属组

1
2
3
4
[root@b5ed8216401f /]# useradd victor
[root@b5ed8216401f /]# chown -R victor.victor /soft/
[root@b5ed8216401f /]# ll -d /soft/
drwxr-xr-x 2 victor victor 6 Sep 24 03:48 /soft/

再来看看宿主机内/test目录的属主和属组是否会发生变化?

1
2
[root@localhost ~]# ll -d /test/
drwxr-xr-x 2 mycat mycat 6 Sep 24 11:48 /test/

竟然变为mycat了。。。
原来,这个与UID有关系,UID,即“用户标识号”,是一个整数,系统内部用它来标识用户。一般情况下它与用户名是一一对应的。
首先查看容器内victor对应的UID是多少,

1
2
[root@b5ed8216401f /]# cat /etc/passwd | grep victor
victor:x:1000:1000::/home/victor:/bin/bash

victor的UID为1000,那么宿主机内1000对应的用户是谁呢?

1
2
[root@localhost ~]# cat /etc/passwd |grep 1000
mycat:x:1000:1000::/home/mycat:/bin/bash

可以看出,宿主机内UID 1000对应的用户是mycat。

  • 容器销毁了,在宿主机上新建的挂载目录是否会消失?
    在这里,主要验证两种情况:一、指定了宿主机目录,即 -v /test:/soft。二、没有指定宿主机目录,即-v /soft
    第一种情况:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[root@localhost ~]# rm -rf /test    --首先删除宿主机的/test目录
[root@localhost ~]# ls /    --可以看到,宿主机上无/test目录
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
[root@localhost ~]# docker run -it --name=centos_test -v /test:/soft centos /bin/bash  --启动容器,为了删除方便,我用--name参数指定了容器的名字
[root@82ad7f3a779a /]# exit
exit
[root@localhost ~]# docker rm centos_test   --删除容器
centos_test
[root@localhost ~]# ls /   --发现 /test目录依旧存在
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  test  tmp  usr  var

可以看出,即便容器销毁了,新建的挂载目录不会消失。进一步也可验证,如果宿主机目录的属主和属组发生了变化,容器销毁后,宿主机目录的属主和属组不会恢复到挂载之前的状态。

第二种情况,通过上面的验证知道,如果没有指定宿主机的目录,则容器会在/var/lib/docker/volumes/随机配置一个目录,那么我们看看这种情况下的容器销毁是否会导致相应目录的删除
首先启动容器

1
2
3
[root@localhost ~]# docker run -it --name=centos_test -v /soft centos /bin/bash
[root@6b75579ec934 /]# exit
exit

通过docker inspect命令查看容器在宿主机上生成的挂载目录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"Mounts": [
        {
            "Name": "b53164cb1c9f1917788638692fb22ad11994cf1fbbc2461b6c390cd3e10ea301",
            "Source": "/var/lib/docker/volumes/b53164cb1c9f1917788638692fb22ad11994cf1fbbc2461b6c390cd3e10ea301/_data",
            "Destination": "/soft",
            "Driver": "local",
            "Mode": "",
            "RW": true
        }
    ],

对应的是/var/lib/docker/volumes/b53164cb1c9f1917788638692fb22ad11994cf1fbbc2461b6c390cd3e10ea301/_data目录
销毁容器,看目录是否存在

1
2
3
4
5
[root@localhost ~]# docker rm centos_test
centos_test
[root@localhost ~]# ll /var/lib/docker/volumes/b53164cb1c9f1917788638692fb22ad11994cf1fbbc2461b6c390cd3e10ea301
total 0
drwxr-xr-x 2 root root 6 Sep 24 14:25 _data

发现该目录依旧存在,即便重启了docker服务,该目录依旧存在

1
2
3
4
[root@localhost ~]# systemctl restart docker
[root@localhost ~]# ll /var/lib/docker/volumes/b53164cb1c9f1917788638692fb22ad11994cf1fbbc2461b6c390cd3e10ea301
total 0
drwxr-xr-x 2 root root 6 Sep 24 14:25 _data
  • 挂载宿主机已存在目录后,在容器内对其进行操作,报“Permission denied”。
    可通过两种方式解决:

1> 关闭selinux。
临时关闭:# setenforce 0
永久关闭:修改/etc/sysconfig/selinux文件,将SELINUX的值设置为disabled。

2> 以特权方式启动容器
指定–privileged参数
如:# docker run -it –privileged=true -v /test:/soft centos /bin/bash

docker挂载参考地址

启动容器

docker start <container-id>
Docker start命令为容器文件系统创建了一个进程隔离空间。注意,每一个容器只能够有一个进程隔离空间。

如:

1
2
3
4
5
#通过名字启动
$ docker start -i centos6_container

#通过容器ID启动
$ docker start -i b3cd0b47fe3d

进入容器

docker exec <container-id>

在当前容器中执行新命令,如果增加 -it参数运行bash 就和登录到容器效果一样的。
docker exec -it centos6_container bash

停止容器

docker stop <container-id>

删除容器

docker rm <container-id>

运行容器

docker run <image-id>

docker run就是docker create和docker start两个命令的组合,支持参数也是一致的,如果指定容器
名字是,容器已经存在会报错,可以增加 –rm 参数实现容器退出时自动删除。

如:
docker create -it --rm --name centos6_container centos:centos6

查看容器列表

docker ps
docker ps 命令会列出所有运行中的容器。这隐藏了非运行态容器的存在,如果想要找出这些容器,增加-a参数。

删除镜像

docker rmi <image-id>

删除构成镜像的一个只读层。你只能够使用docker rmi来移除最顶层(top level layer)
(也可以说是镜像),你也可以使用-f参数来强制删除中间的只读层。

commit容器

docker commit <container-id>
将容器的可读写层转换为一个只读层,这样就把一个容器转换成了不可变的镜像。

镜像保存

docker save <image-id>

创建一个镜像的压缩文件,这个文件能够在另外一个主机的Docker上使用。和export命令不同,这个命令
为每一个层都保存了它们的元数据。这个命令只能对镜像生效。

如:

1
2
3
4
5
#保存centos镜像到centos_images.tar 文件
$ docker save  -o centos_images.tar centos:centos6

#或者直接重定向
$ docker save  -o centos_images.tar centos:centos6 > centos_images.tar

容器导出

docker export <container-id>

创建一个tar文件,并且移除了元数据和不必要的层,将多个层整合成了一个层,只保存了当前统一视角看到
的内容。expoxt后的容器再import到Docker中,只有一个容器当前状态的镜像;而save后的镜像则不同,
它能够看到这个镜像的历史镜像。

inspect

docker inspect <container-id> or <image-id>
docker inspect命令会提取出容器或者镜像最顶层的元数据

docker安装指定版本TAG的镜像

在docker中安装镜像,一般过程是,docker search 软件名称。
本文以tomcat为例,讲解下载指定版本TAG的tomcat。

搜索tomcat镜像:
sudo docker search tomcat

发现只有tomcat,而如果docker pull tomcat,下载下来的是最新版的tomcat,也就是tomcat11。假设希望下载的是tomcat:8-jre8。那么该如何做呢?
如果search出来的镜像,不是自己想要的版本,可通过在docker hub上搜索好版本TAG,地址如下:https://hub.docker.com,进入之后,在页面左上角搜索框搜索tomcat
搜索出来之后,点详情,点击标签,在下面通过翻页,找到8-jre标签,复制标签名:8-jre8,下载tomcat镜像
sudo docker pull tomcat:8-jre8

/var/lib/docker/overlay2 占用过大

查看磁盘使用情况
sudo du -hs /var/lib/docker/

查看Docker的磁盘使用情况
docker system df

清理磁盘,删除关闭的容器、无用的数据卷和网络,以及dangling镜像(即无tag的镜像)
docker system prune

docker system prune -a命令清理得更加彻底,可以将没有容器使用Docker镜像都删掉。注意,这两个命令会把你暂时关闭的容器,以及暂时没有用到的Docker镜像都删掉了…所以使用之前一定要想清楚.。我没用过,因为会清理 没有开启的 Docker 镜像。

迁移 /var/lib/docker目录

停止docker服务
systemctl stop docker

创建新的docker目录,执行命令df -h,找一个大的磁盘。 我在 /home目录下面建了 /home/docker/lib目录,执行的命令是:
mkdir -p /home/docker/lib

迁移/var/lib/docker目录下面的文件到 /home/docker/lib
rsync -avz /var/lib/docker /home/docker/lib/

配置 /etc/systemd/system/docker.service.d/devicemapper.conf。查看 devicemapper.conf 是否存在。如果不存在,就新建
sudo mkdir -p /etc/systemd/system/docker.service.d/
sudo vi /etc/systemd/system/docker.service.d/devicemapper.conf

然后在 devicemapper.conf 写入:(同步的时候把父文件夹一并同步过来,实际上的目录应在 /home/docker/lib/docker)

1
2
3
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd  --graph=/home/docker/lib/docker

重新加载 docker
systemctl daemon-reload
systemctl restart docker
systemctl enable docker

为了确认一切顺利,运行
docker info

命令检查Docker 的根目录.它将被更改为 /home/docker/lib/docker

1
2
3
4
5
6
...
Docker Root Dir: /home/docker/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
...

启动成功后,再确认之前的镜像还在

1
2
3
4
linlf@dacent:~$ docker images
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
AAA/AAA               v2                  7331b8651bcc        27 hours ago        3.85GB
BBB/BBB               v1                  da4a80dd8424        28 hours ago        3.47GB

确定容器没问题后删除/var/lib/docker/目录中的文件。

Docker和K8S

参考地址

给已存在的docker容器添加或修改端口映射(或挂载目录)

建立完一个容器后不能增加端口映射了,因为 docker run -p 有 -p 参数,但是 docker start 没有 -p 参数。
通常会在启动docker容器的时候,指定端口映射,如:

1
2
3
4
5
6
docker run --name jenkins -d \
-v /usr/local/src:/share \
-v /home/jenkins:/var/jenkins_home \
-v /etc/localtime:/etc/localtime \
-p 9999:8080 -p 50000:50000 \
jenkins

如果需要新的端口映射,不必删除重建这个容器,可以通过修改容器配置文件来解决。

  • 首先,停止要修改配置文件的容器(可不必停止docker服务)
    docker stop 容器名

  • 通过docker inspect 容器名字找到容器的信息。
    最上边的id,是一段hash值,大概记住这个id。

  • 找到路径/var/lib/docker/containers
    通过ls可以看到,每个容器的hash值文件夹,找到刚才的文件夹,cd进去。

  • 修改hostconfig.json
    找到"PortBindings”:{} 这个配置项。修改:
    "PortBindings":{"80/tcp":[{"HostIp":"","HostPort":"8080"}]}
    这里 80 是容器端口, 8080 是本地端口。可能有多个,注意格式。

修改挂载目录:
"Binds": ["/data:/opt"],
将主机的/data目录挂载到容器的/opt目录。

  • 修改config.v2.json
    找到配置项"Tty": true,前边如果有ExposedPorts,就是已经有其他端口映射了,继续添加。
    如果没有,添加"ExposedPorts":{"80/tcp":{}} ,
    我不知道添加到别的地方会不会有影响,因为经过对比正常的端口映射配置项是在这个位置,这个就是将容器内部端口暴露出来,如果不加这一句端口映射不会成功的。

修改挂载目录:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
"MountPoints": {
"/import": {
            "Source": "/data",
            "Destination": "/opt",
            "RW": true,
            "Name": "",
            "Driver": "",
            "Type": "bind",
            "Propagation": "rprivate",
            "Spec": {
                "Type": "bind",
                "Source": "/data",
                "Target": "/opt"
            },
            "SkipMountpointCreation": false
        }
}, 
  • 然后重启docker守护进程(并且不可先重启容器,否则你的配置文件又变回之前的了)
    service docker restart

  • docker服务重启后,启动容器。
    docker start 容器名

  • 查看容器的映射端口
    docker inspect 容器名
    发现端口已经映射上了。

参考