侧边栏壁纸
博主头像
王一川博主等级

努力成为一个不会前端的全栈工程师

  • 累计撰写 70 篇文章
  • 累计创建 20 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录

Docker 容器技术

王一川
2021-09-30 / 0 评论 / 8 点赞 / 2,130 阅读 / 11,801 字
温馨提示:
本文最后更新于 2022-12-01,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

官方文档地址:https://www.docker.com/get-started

中文参考手册:https://docker_practice.gitee.io/zh-cn

一、概述

1.1 什么是 docker

Docker 是一个用于开发、发布和运行应用程序的开放平台。Docker 使您能够将应用程序与基础架构分开,以便快速交付软件。使用 Docker 您可以使用与管理应用程序相同的方式管理您的基础设施。通过利用 Docker 用于快速交付、测试和部署代码的方法,您可以显著减少编写代码和在生产中运行代码之间的延迟。Docker 提供了一个完整的容器解决方案,不管你是谁,不管你在哪,你都可以开始容器的的旅程

Docker 是一个容器技术

1.2 docker 起源

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

Docker 使用 Google 公司推出的 Go 语言 进行开发实现,基于 Linux 内核的 cgroup,namespace,以及 OverlayFS 类的 Union FS 等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器

1.3 为什么是 docker

开发的时候,本机测试环境可以跑,生产环境跑步起来

假如上线一个 java web 项目,这个应用程序涉及很多东西如:jdk、tomcat、redis、mysql等,当这些其中某一项版本不一致的时候,可能就会导致应用程序跑不起来这种情况。Docker 则将程序以及使用软件环境直接打包在一起,无论在那个机器上保证了环境一致。

优势一:一致的运行环境,更轻松的迁移

别人程序出了问题把服务器资源吃光,导致自己的应用挂掉了

如果你的程序重要性不是特别高的话,公司基本上不可能让你的程序独享一台服务器的,这时候你的服务器就会跟公司其他人的程序共享一台服务器,所以不可避免地就会受到其他程序的干扰,导致自己的程序出现问题。Docker 就很好解决了环境隔离的问题,别人程序不会影响到自己的程序。

优势二:进程级的隔离,容器与容器之间互不影响,更高效利用资源

促销活动需要临时拓展几十台服务器

在没有 Docker 的情况下,要在几天内部署几十台服务器,这对运维来说是一件非常折磨人的事,而且每台服务器的环境还不一定一样,就会出现各种问题,最后部署地头皮发麻。用 Docker 的话,只需要将程序打包成镜像,你要多少台服务,我就启动多少容器,极大地提高了部署效率。

优势三: 通过镜像复制N多个环境一致容器

1.4 docker 与虚拟机

  1. 虚拟机是携带操作系统的,docker 不携带操作系统
  2. 虚拟机调度资源步骤为:利用 Hypervisor 虚拟化内存,虚拟内存 -> 虚拟物理内存 -> 物理内存
  3. docker 调度资源步骤为:利用 Docker Engine 调用宿主机资源,虚拟内存 -> 物理内存
传统虚拟机 Docker容器
磁盘占用 几个GB到几十个GB左右 几十MB到几百MB左右
CPU内存占用 虚拟操作系统非常占用CPU和内存 Docker引擎占用极低
启动速度 (从开机到运行项目)几分钟 (从开启容器到运行项目)几秒
安装管理 需要专门的运维技术 安装、管理方便
应用部署 每次部署都费时费力 从第二次部署开始轻松简捷
耦合性 多个应用服务安装到一起,容易互相影响 每个应用服务一个容器,达成隔离
系统依赖 需求相同或相似的内核,目前推荐是Linux

1.5 docker 安装

在测试或开发环境中 Docker 官方为了简化安装流程,提供了一套便捷的安装脚本,CentOS 系统上可以使用这套脚本安装,另外可以通过 --mirror 选项使用国内源进行安装:执行这个命令后,脚本就会自动的将一切准备工作做好,并且把 Docker 的稳定(stable)版本安装在系统中。

$ curl -fsSL get.docker.com -o get-docker.sh
$ sudo sh get-docker.sh --mirror Aliyun

启动 docker

$ sudo systemctl enable docker
$ sudo systemctl start docker

验证

$ docker run hello-world

个人使用的是 docker for mac

$ brew install -cask docker

或者直接去官网下载 docker for desktop

1.6 docker 架构

  • 镜像:镜像代表一个应用环境,它是一个只读文件,如:mysql镜像、tomcat镜像等
  • 容器:镜像每次运行之后会产生一个容器,即正在运行的镜像,可读可写
  • 仓库:存档镜像的位置,用于镜像的上传和下载
  • Dockerfile:docker生成镜像的配置文件,用于自定义镜像的一些配置
  • tar:对镜像的打包文件,可以基于这个文件还原成镜像

1.7 docker 运行流程

1.8 配置阿里镜像加速

登录阿里云搜索镜像加速服务,获取加速地址如,修改配置文件

docker for desktop 点击 setting - docker engine

{
  "experimental": false,
  "features": {
    "buildkit": true
  },
  "builder": {
    "gc": {
      "defaultKeepStorage": "20GB",
      "enabled": true
    }
  },
  "registry-mirrors": [
    "https://docker.mirrors.ustc.edu.cn",
    "http://hub-mirror.c.163.com",
    "https://registry.docker-cn.com"
  ]
}

Linux 配置

sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://docker.mirrors.ustc.edu.cn","http://hub-mirror.c.163.com","https://registry.docker-cn.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

验证镜像加速是否生效

[root@localhost ~]# docker info
		..........
    127.0.0.0/8
   Registry Mirrors:
    'https://t2eulrwb.mirror.aliyuncs.com'
   Live Restore Enabled: false
   Product License: Community Engine

二、Docker 入门

➜  ~ docker run hello-world                                       
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
93288797bd35: Pull complete 
Digest: sha256:393b81f0ea5a98a7335d7ad44be96fe76ca8eb2eaa76950eb8c989ebf2b78ec0
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (arm64v8)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

2.1 辅助命令

# 查看docker信息
docker version
# 查看消息信息
docker info
# 帮助命令
docker --help

2.2 镜像命令

# 查看本机所有镜像
docker images
	# 列出所有镜像(包含中间镜像层)
  docker images -a
  # 只显示镜像id(用于批量删除)
  docker -q
  
# 搜索镜像
docker search [options] 镜像名
	# 显示完整信息
	docker search --no-trunc tomcat

# 从仓库下载镜像
docker pull 镜像名[:TAG|@DIGEST]
	# 仅镜像名默认下载 latest

# 删除镜像
docker image rm 镜像名|镜像ID
  # 简写
  docker rmi 镜像名|镜像ID
  # 强制删除
  docker rmi -f 镜像名|镜像ID

补充:

docker images 展示的镜像信息如下:

REPOSITORY       TAG       IMAGE ID       CREATED       SIZE
  • REPOSITORY:仓库,即镜像名称
  • TAG:分支,可以理解为版本号
  • IMAGE ID:镜像ID,全网唯一
  • CREATED:镜像构建时间
  • SIZE:镜像大小

docker 命令提供类似 Linux 的管道功能,如删除所有镜像

➜  ~ docker rmi -f $(docker images -q)

2.3 容器命令

通常我们准备下载一个镜像不会通过 docker search,因为展示出来的信息不包含分支信息,即版本号,一般会在dockerhub网站搜索我们需要的镜像的具体分支,比如:下载tomcat

选择适合的版本复制命令下载镜像 docker pull tomcat:latest

运行容器

➜  ~ docker run tomcat:latest

后台运行容器

➜  ~ docker run -d tomcat:latest 

此时容器已经启动,尝试访问本机 8080 端口,发现拒绝连接,这是因为 docker 的特点:线程级隔离,相当于此时 tomcat 独自运行在本机的一个虚拟机中,若要实现容器和宿主机的通信,需要映射容器端口。

映射容器端口

➜  ~ docker run -d -p 8080:8080 tomcat:latest

格式:-p 宿主机端口:容器端口

此时再次访问宿主机8080端口

莫慌!!!这因为下载的 tomcat 镜像 webapps 目录没有内容

查看正在运行的容器

➜  ~ docker ps                
CONTAINER ID	IMAGE	COMMAND	CREATED	STATUS	PORTS	NAMES
  • CONTAINER ID:容器ID,唯一
  • IMAGE:容器基于的镜像
  • COMMAND:容器启动执行的命令
  • CREATED:容器创建的时间
  • STATUS:容器此时的状态
  • PORTS:容器端口信息
  • NAMES:容器名称,唯一

基于容器的操作不仅可以使用 CONTAINER ID 也可以使用 NAMES

拓展参数:

  • -a:查看所有容器(正在运行和历史运行过的容器)
  • -q:静默模式,只显示容器ID

运行容器指定名称

➜  ~ docker run -d -p 8081:8080 --name tomcat01 tomcat:latest

停止|关闭|重启容器

# 启动容器
docker start 容器名字或者容器ID
# 关闭容器
docker stop 容器名字或者容器ID
# 立刻停止容器
docker kill 容器名字或者容器ID
# 重启容器
docker restart 容器名字或者容器ID

上述这些命令搭配 docker ps -q 可实现批量操作

docker run 和 docker start 区别:run 运行镜像,创建一个新的容器;start 仅启动一个历史容器

删除容器

docker rm 容器名字或者容器ID

正在运行的容器不可以被删除,使用 -f 可以强制删除,额外:docker rm -f $(docker ps -aq) 删除所有容器!!!

查看容器内的进程

docker top 容器名字或者容器ID

和 top 命令效果一样

查看容器的运行日志

docker logs [options] 容器名字或者容器ID

options:

  • -t:加入时间戳
  • -f:阻塞实时打印日志
  • –tail int:打印日志最后n条

进入容器内部

docker exec [options] 容器名字或者容器ID 容器内命令

options:

  • -i:以交互式运行容器
  • -t:分配一个 tty

通常这个命令是固定写法

docker exec -it tomcat01 bash

这个镜像搭载着一个轻量级的 linux 系统,支持一些常用的命令(exit退出)

容器与宿主机传输文件

# 宿主机文件(夹) -> 容器
docker cp 宿主机文件(夹) 容器id|名称:容器路径
# 容器 -> 宿主机文件(夹)
docker cp 容器id|名称:容器路径 宿主机文件(夹)

注:宿主机文件(夹)一定要是绝对路径

回到上面访问宿主机 8080 端口 404 问题,这是因为容器中 tomcat/webapps 下没有文件。我们可以借助这个命令将本机的一些 war 包,或者官网 tomcat 目录下的东西放进去即可

➜  ~ docker cp ~/env/tomcat/webapps tomcat01:/usr/local/tomcat

再次访问即可出现经典的劈叉猫

容器与宿主机共享目录

尝试删除运行的tomcat容器,以相同的方式再次运行一个容器,发现 webapps 下依然是空的,这说明容器被删除后容器内所有文件都会被删除,当一些重要的数据我们就不能放在容器里面,因此 docker 提供数据卷的功能将容器的目录挂载到宿主机的目录,实现数据的持久化(更多数据卷的内容见后面的高级部分)

docker run -v 宿主机的绝对路径|任意别名:/容器内的路径 镜像名
  1. 如果是宿主机路径:必须是绝对路径,宿主机目录会覆盖容器内目录内容
  2. 如果是别名:运行容器时自动在宿主机中创建一个目录,并将容器目录文件复制到宿主机中;若别名存在宿主机目录会覆盖容器内目录内容
# 挂载宿主机目录
➜  ~ docker run -d -p 8081:8080 --name tomcat04 -v ~/env/tomcat/webapps:/usr/local/tomcat/webapps tomcat:latest
# 自动创建数据卷(如果不存在的话)
➜  ~ docker run -d -p 8083:8080 --name tomcat07 -v v_t:/usr/local/tomcat/webapps tomcat:latest

打包镜像

docker save 镜像名 -o 路径/名称.tar

载入镜像

docker load -i 名称.tar

将容器打包成镜像

docker commit -m "描述信息" -a "作者信息"   (容器id或者名称)打包的镜像名称:标签
# 提交dockerhub(一般不提交,需要登录dockerhub)
docker push 镜像名称:标签

三、docker 高级

3.1 镜像原理

3.1.1 什么是镜像

Docker 镜像是一种轻量级的,可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需的所有内容,包括代码、运行时所需的库、环境变量和配置文件。

3.1.2 为什么镜像这么大

可以看到 docker 中一个 tomcat 镜像居然高达 670M,这是为什么?

➜  ~ docker images                             
REPOSITORY       TAG       IMAGE ID       CREATED        SIZE
tomcat           latest    eadaf4390f32   29 hours ago   670MB
kpretty/tomcat   v1.0      3c447801519f   2 days ago     674MB
debian           latest    a331823f8665   2 days ago     118MB
hello-world      latest    18e5af790473   6 days ago     9.14kB
centos           centos7   dfc30428e163   2 weeks ago    301MB

我们都知道,操作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而 Docker 镜像,就相当于是一个 root 文件系统。除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。因此对于 tomcat 镜像来说,不仅包含 tomcat 程序包,还包含 tomcat 运行所依赖的所有环境。

3.1.3 镜像原理

UnionFs(联合文件系统)

UnionFs 是一种分层,轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。UnionFs 是Docker 镜像的基础。这种文件系统特性:就是一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录 。

Docker 的镜像实际是由一层一层的文件系统组成

bootfs(boot file system)主要包含bootloader和kernel,bootloader主要是引导加载kernel,Linux刚启动时会加载bootfs文件系统。在docker镜像的最底层就是bootfs。这一层与Linux/Unix 系统是一样的,包含boot加载器(bootloader)和内核(kernel)。当boot加载完,后整个内核就都在内存中了,此时内存的使用权已由bootfs转交给内核,此时会卸载bootfs。

rootfs(root file system),在bootfs之上,包含的就是典型的linux系统中的/dev,/proc,/bin,/etc等标准的目录和文件。rootfs就是各种不同的操作系统发行版,比如Ubuntu/CentOS等等。

我们平时安装进虚拟机的centos都有1到几个GB,为什么docker这里才200MB?对于一个精简的OS,rootfs可以很小,只需要包括最基本的命令,工具,和程序库就可以了,因为底层直接使用Host的Kernal,自己只需要提供rootfs就行了。由此可见不同的linux发行版,他们的bootfs是一致的,rootfs会有差别。因此不同的发行版可以共用bootfs。

但docker的镜像不包含操作系统,因为docker与宿主机共享内核。

3.1.4 分层结构

这种分层结构在 pull 镜像是会体现出来,一份镜像会分好几几个分支下载,同时有的分支还提示已存在。

这种分层结构最大的好处就是资源共享,多个镜像都是从相同的 base 镜像构建而来,那么宿主机只需要保存一份 base 镜像;同时内存也只需要加载一份 base 镜像即可。

3.2 网络原理

当 docker 启动时,会自动在主机上创建一个 bridge 的虚拟网桥,可以理解为一个软件交换机,它会在挂载到它的网口之间进行转发。同时,docker 随机分配一个本地未占用的私有网段中的一个地址给 bridge 接口,此后启动的容器内的网口也会自动分配一个同网段的地址。

当创建一个 docker 容器的时候,同时会创建一对 veth pair 接口,这一对接口一段在容器内为 eth0;另一端在本地被挂载到 bridge 网桥以 veth 开头,通过这种方式,主机可以跟容器通信,挂载在同一网桥的容器也可以互相通信,docker就创建了在主机和所有容器之间的一个虚拟网络共享,这种方式为桥接模式,是docker默认的网络模式也是推荐的网络模式。

3.2.1 查看网络信息

➜  ~ docker network ls            
NETWORK ID     NAME      DRIVER    SCOPE
63893e5e4e77   bridge    bridge    local
2c90c0c62179   host      host      local
8347580e4ecb   none      null      local

这里的 bridge 就是上面所说的默认网桥,运行容器默认挂载在这个网桥上

3.2.2 创建一个网桥

➜  ~ docker network create 网桥名称

3.2.3 删除一个网桥

➜  ~ docker network rm 网桥名称|ID

3.2.4 查看网桥细节

➜  ~ docker network inspect 网桥名称|ID

3.2.5 容器之间通信

根据 docker 的网络原因,实现容器间的通信,只需要保证容器挂载在相同网桥上即可,其实默认直接启动容器之间就可以直接通信,但通常我们会一个业务创建一个网桥,将相同业务的容器挂载到一个网桥,业务与业务之间网络是隔离的防止干扰

创建一个网桥 docker0

➜  ~ docker network create docker0

查看网桥信息

➜  ~ docker network inspect docker0
[
    {
        "Name": "docker0",
        "Id": "e283a82de3852629506a0b1e2957560109ca22d49b1c1708d90af17385da7839",
        "Created": "2021-10-01T09:37:57.434314302Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.22.0.0/16",
                    "Gateway": "172.22.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

发现这个网桥分配了一个 172.22 的子网端,之后挂载在这个网桥上的容器都会是这个网段内

运行容器指定挂载的网桥

➜  ~ docker run -d -p 8080:8080 --name tomcat01 --network docker0 tomcat:latest
c17deed4f99312568f8189217b78254f58f27fc992e019bce90f2e908d4a3f07
➜  ~ docker run -d -p 8081:8080 --name tomcat02 --network docker0 tomcat:latest
ab4b13fd2b78f7610a1d71d3b1ea4a26bac82fde15fbd025bdcd07e69c6bc5c2

再次查看网桥信息

➜  ~ docker network inspect docker0                                            
[
    {
        "Name": "docker0",
        "Id": "e283a82de3852629506a0b1e2957560109ca22d49b1c1708d90af17385da7839",
        "Created": "2021-10-01T09:37:57.434314302Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.22.0.0/16",
                    "Gateway": "172.22.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "ab4b13fd2b78f7610a1d71d3b1ea4a26bac82fde15fbd025bdcd07e69c6bc5c2": {
                "Name": "tomcat02",
                "EndpointID": "61e0436bc9b437eb65989b2a1de22d44cae6dcea8cff862b97308167d9f7988a",
                "MacAddress": "02:42:ac:16:00:03",
                "IPv4Address": "172.22.0.3/16",
                "IPv6Address": ""
            },
            "c17deed4f99312568f8189217b78254f58f27fc992e019bce90f2e908d4a3f07": {
                "Name": "tomcat01",
                "EndpointID": "beb19cd9621e9cd98a8654555a59b8753b3d014c788f229d76f320569dab264d",
                "MacAddress": "02:42:ac:16:00:02",
                "IPv4Address": "172.22.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

两个容器分别被分配了 172.22.0.3、172.22.0.2,下面进入其中一个容器 curl 另一个容器的8080端口(因为tomcat构建的容器只支持基本的一些命令,没有ping)

root@c17deed4f993:/usr/local/tomcat# curl http://172.22.0.3:8080

同时 docker 会自动的对容器的名称和ip做映射,即:

root@c17deed4f993:/usr/local/tomcat# curl http://tomcat02:8080

3.3 数据卷

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

  1. 数据卷可以在容器之间共享和重用
  2. 对数据卷的修改会立马生效
  3. 对数据卷的更新,不会影响镜像
  4. 数据卷默认还一直存在,集市容器被删除

数据卷的使用,类似 Liunx 下对目录或者文件进行 mount,镜像中的被指定为挂载点的目录中的文件恢复至到数据卷中(仅数据卷为空)

3.3.1 创建数据卷

docker volume create 数据卷名称

3.3.2 查看数据卷

docker volume inspect 数据卷名称|ID
如:
➜  ~ docker volume inspect volume1 
[
    {
        "CreatedAt": "2021-10-01T11:43:33Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/volume1/_data",
        "Name": "volume1",
        "Options": {},
        "Scope": "local"
    }
]

其中,Mountpoint 为挂载在宿主机的目录

3.3.3 挂载数据卷

~ docker run -d -p 8081:8080 --name tomcat03 -v volume01:/usr/local/tomcat/webapps/ tomcat:latest

3.3.4 删除数据卷

正在被使用的数据卷无法被删除

docker volume rm 数据卷名称|ID

3.3.5 Mac下的数据卷

Linux下数据卷的Mountpoint路径真实存在,直接 cd 即可看到容器内的文件,但对于 mac 来说

➜  ~ cd /var/lib/docker/volumes/volume1/_data  
cd: no such file or directory: /var/lib/docker/volumes/volume1/_data

网上很多人给的方法是通过 screen,如数据卷会被放在下面的 tty 中,但实际是没有的,很多人也反馈没有

➜  ~ cd Library/Containers/com.docker.docker/Data/vms/0/
➜  0 pwd                  
/Users/wjun/Library/Containers/com.docker.docker/Data/vms/0
➜  0 ll
total 0
srwxr-xr-x  1 wjun  staff   0  9 28 14:45 00000002.00001003
srwxr-xr-x  1 wjun  staff   0  9 28 14:45 console.sock
drwxr-xr-x@ 3 wjun  staff  96  9 30 11:12 data
drwxr-xr-x  2 wjun  staff  64  9 27 18:30 log

经过不懈努力终于在墙外找到了解决方案

docker run -it --privileged --pid=host debian nsenter -t 1 -m -u -n -i sh

运行这个镜像,进入容器中 cd 到指定路径就能看到 mac 的挂载点,原理未知,知道原理的可以私信我,感谢!!!

四、Dockerfile

Dockerfile 是 Docker 镜像的描述文件,是由一系列命令和参数构成的脚本,主要作用是用来构建 docker 镜像的构建文件。

4.1 Dockerfile解析过程

通常这个描述文件就叫 Dockerfile,该文件所在的目录称之为 docker 的上下文目录,当基于 Dockerfile 开始构建镜像时,首先将上下文目录里的所有文件发送给 docker server,在服务端完成构建;在构建镜像过程中,Dockerfile 中每一个命令都会生成一个镜像层,下一个的命令基于上一个命令生成的镜像继续构建,形成一个个的镜像层,同时每个镜像层都会被缓存,当遇到相同的命令会直接使用,最终构建完成后体现给用户的只有最终的镜像。

根据上面的解释,一般在构建镜像时会新建一个空白目录,该目录仅放置 Dockerfile 和与构建镜像所必须的文件;无关紧要的文件会影响构建速度,但 docker 也提供类似 gitignore 的作用,在上下文目录中新建 .dockerignore 文件,docker 会忽略该文件里面指定的内容。

4.2 Dockerfile保留命令

常用命令如下(官方说明:https://docs.docker.com/engine/reference/builder/)

保留字 作用
FROM 当前镜像是基于哪个镜像的 第一个指令必须是FROM
MAINTAINER 镜像维护者的姓名和邮箱地址
RUN 构建镜像时需要运行的指令
EXPOSE 当前容器对外暴露出的端口号
WORKDIR 指定在创建容器后,终端默认登录进来的工作目录,一个落脚点
ENV 用来在构建镜像过程中设置环境变量
ADD 将宿主机目录下的文件拷贝进镜像且ADD命令会自动处理URL和解压tar包
COPY 类似于ADD,拷贝文件和目录到镜像中
将从构建上下文目录中<原路径>的文件/目录复制到新的一层的镜像内的<目标路径>位置
VOLUME 容器数据卷,用于数据保存和持久化工作
CMD 指定一个容器启动时要运行的命令
Dockerfile中可以有多个CMD指令,但只有最后一个生效,CMD会被docker run之后的参数替换
ENTRYPOINT 指定一个容器启动时要运行的命令
ENTRYPOINT的目的和CMD一样,都是在指定容器启动程序及其参数

上述命令必须大写

4.2.1 FROM

基于哪个镜像进行构建新的镜像,在构建中基于的镜像会自动从dockerhub进行拉取,且必须保证该命令作为 Dockerfile 第一个指令出现

语法如下:

FROM <image>[:<tag>]

TAG 不写默认 latest

4.2.2 MAINTAINER

镜像维护者的姓名和邮箱地址[废弃]

语法:

MAINTAINER <name>

4.2.3 RUN

RUN 指定将在当前镜像层中执行指定的命令并提交结果,其格式有两种 shell 和 exec

shell:RUN <命令>如:

RUN echo "hello docker" >> /README

exec: RUN [“可执行文件”,“参数1”,“参数2”,…]如:

RUN ["echo","'hello docker'",">>","/README"]

注:上面说过每一个指令都会构建一层因此不建议写多个 RUN,多个 RUN 的结果会显得臃肿、形成非常多的镜像层,不仅增加了镜像构建的时间,也容易出错,同时 UnionFs 有最大层的显示,现版本是 127 层;要时刻提醒自己 Dockerfile 不是在写 shell 脚本,而是定义每一层的构建规则;对于多条 shell 参考写法如下:

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

stretch 是一个空白镜像,基于此镜像构建意味着你不以任何镜像为基础,意味着你的镜像并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧

4.2.4 EXPOSE

用于声明构建的镜像在运行为容器时对外暴露的端口,这只是一个声明,在容器运行时不会因为这而产生任何影响,该命令的好处是:帮助镜像使用者理解这个镜像服务守护端口,一方面配置映射,同时在运行是随机端口映射(-P),会自动随机映射 EXPOSE 的端口

语法:

EXPOSE 8080 # 默认 TCP
EXPOSE 8080/tcp
EXPOSE 8080/udp

4.2.5 ENV

用来为构建镜像设置环境变量,该值可以在出现在后续指令环境中。

语法:

ENV key value
ENV key=value
ENV k1=· k2=v2 k3=v3

如官方 node 镜像就出现类似代码

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

4.2.6 WORKDIR

用于为 Dockerfile 中后续指令设置工作目录,如果 WORKDIR 指定的目录不存在会被自动创建

语法:

WORKDIR PATH

该命令的好处如下:

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

基于上述构建完的镜像发现并没有 /app/world.txt 文件,因为每个命令都会产生一个镜像层,第一个 RUN 仅仅是一个内存上的变化,不会对文件产生任何影响,到第二层又是一个新的镜像层自然不会继承上一层的内存变化,而 WORKDIR 会改变以后各层的工作目录位置,多个 WORKDIR 使用相对路径,则该路径与之前 WORKDIR 的路径相对即:

WORKDIR /a
WORKDIR b
WORKDIR c

RUN pwd

RUN pwd 的工作路径是 /a/b/c

4.2.7 COPY

用于将上下文指定的文件(夹)复制到镜像的指定目录中

语法:

COPY src dest
COPY ["src",...,"dest"]

其 src 支持统配符,其通配规则满足 Go 的 filepath.Math,此外 COPY 会保留源文件的各种元数据信息如:读写权限,用户用户组,修改时间等;若要修改该添加 --chown 如:

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

如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。

4.2.8 ADD

更高级的 COPY,用于从上下文中复制文件,支持链接下载,自动解压压缩包

语法:

ADD hom* /mydir/       通配符添加多个文件
ADD hom?.txt /mydir/   通配符添加
ADD test.txt relativeDir/  可以指定相对路径
ADD test.txt /absoluteDir/ 也可以指定绝对路径
ADD url 

但是该语法语义不明,不建议使用;该语法仅限需要自动解压的场合使用,该语法可以被 COPY 配合 RUN 替代

4.2.9 VOLUME

在数据卷章节说过,容器中重要的数据需要挂载到宿主机实现持久化,该命令可以在运行容器且没有指定数据卷时自动将 VOLUME 指定的文件目录挂载到一个自动创建的匿名数据卷中,当然在运行时指定 -v 会覆盖这个命令

格式

VOLUME path

4.2.10 CMD

用于启动容器指定执行的命令,在 Dockerfile 中只能有一条 CMD,多条命令只有最后一条生效

语法格式与 RUN 一致,在指令格式上推荐使用 exec 格式,如果使用 shell 格式实际命令会被包装成 sh -c 的参数进行执行如:

CMD echo $PATH
# 实际上
CMD [ "sh", "-c", "echo $PATH" ]

提到 CMD 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 systemd 去启动后台服务,容器内没有后台服务的概念。

一些初学者将 CMD 写为:

CMD service nginx start

然后发现容器执行后就立即退出了。甚至在容器内去使用 systemctl 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

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

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

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

4.2.11 ENTRYPOINT

用来指定容器启动时执行命令和CMD类似

格式与 CMD 一致,ENTRYPOINT指令,往往用于设置容器启动后的第一个命令,这对一个容器来说往往是固定的。CMD指令,往往用于设置容器启动的第一个命令的默认参数,这对一个容器来说可以是变化的。

如:

ENTRYPOINT ["ls"]
CMD ["/"]

这种情况最终的执行效果是 ENTRYPOINT “$CMD”,但 CMD 在启动容器时会被覆盖,假如构建的镜像名为 itest

docker run itest

运行结果其实是 ls /

docker run itest /bin

此时 CMD 命令被覆盖最终运行 ls /bin

五、Docker Compose

Compose 项目是 Docker 官方的开源项目,实现对 Docker 容器集群的快速编排,其定位是:定义和运行多个 Docker 容器应用。

上一节,Dockerfile 末班文件,可以让用户很方便的定义一个单独的应用容器,然后在日常工作中经常需要多个容器相互配合。例如实现一个 web 项目,除了 web 服务容器本身,往往还需要加上后端的数据库服务容器,甚至还包括负载均衡容器等。

Compose 恰好满足这样的需求,它允许用户通过一个单独的 docker-compose.yml 模板文件来定义一组相关联的应用容器为一个项目

Compose 中两个重要的概念:

  • 服务:一个应用容器就是一个服务
  • 项目:由一组关联的应用容器组合的完整业务单元,在 docker-compose.yml 中定义

5.1 安装与卸载

1.linux
  • 在 Linux 上的也安装十分简单,从 官方 GitHub Release 处直接下载编译好的二进制文件即可。例如,在 Linux 64 位系统上直接下载对应的二进制包。
$ sudo curl -L https://github.com/docker/compose/releases/download/1.25.5/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
2.macos、window
  • Compose 可以通过 Python 的包管理工具 pip 进行安装,也可以直接下载编译好的二进制文件使用,甚至能够直接在 Docker 容器中运行。Docker Desktop for Mac/Windows 自带 docker-compose 二进制文件,安装 Docker 之后可以直接使用
3.bash命令补全
$ curl -L https://raw.githubusercontent.com/docker/compose/1.25.5/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose
4.卸载
  • 如果是二进制包方式安装的,删除二进制文件即可。
$ sudo rm /usr/local/bin/docker-compose
5.测试安装成功
$ docker-compose --version
 docker-compose version 1.25.5, build 4667896b

5.2 使用

docker-compose 模板:https://docker_practice.gitee.io/zh-cn/compose/compose_file.html

version: "3.0"
services:
  mysqldb:
    image: mysql:5.7.19
    container_name: mysql
    ports:
      - "3306:3306"
    volumes:
      - /root/mysql/conf:/etc/mysql/conf.d
      - /root/mysql/logs:/logs
      - /root/mysql/data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
    networks:
      - ems
    depends_on:
      - redis

  redis:
    image: redis:4.0.14
    container_name: redis
    ports:
      - "6379:6379"
    networks:
      - ems
    volumes:
      - /root/redis/data:/data
    command: redis-server
    
networks:
  ems:

通过 docker-compose.yml 运行一组容器

[root@centos ~]# docker-compose up    							//前台启动一组服务
[root@centos ~]# docker-compose up -d 							//后台启动一组服务

5.3 模板文件

模板文件是使用 Compose 的核心,涉及到的指令关键字也比较多。但大家不用担心,这里面大部分指令跟 docker run 相关参数的含义都是类似的。

默认的模板文件名称为 docker-compose.yml,格式为 YAML 格式。

version: "3"

services:
  webapp:
    image: examples/web
    ports:
      - "80:80"
    volumes:
      - "/data"

注意每个服务都必须通过 image 指令指定镜像或 build 指令(需要 Dockerfile)等来自动构建生成镜像。

如果使用 build 指令,在 Dockerfile 中设置的选项(例如:CMD, EXPOSE, VOLUME, ENV 等) 将会自动被获取,无需在 docker-compose.yml 中重复设置。

下面分别介绍各个指令的用法。

build

指定 Dockerfile 所在文件夹的路径(可以是绝对路径,或者相对 docker-compose.yml 文件的路径)。 Compose 将会利用它自动构建这个镜像,然后使用这个镜像。

version: '3'
services:

  webapp:
    build: ./dir

你也可以使用 context 指令指定 Dockerfile 所在文件夹的路径。

使用 dockerfile 指令指定 Dockerfile 文件名。

使用 arg 指令指定构建镜像时的变量。

version: '3'
services:

  webapp:
    build:
      context: ./dir
      dockerfile: Dockerfile-alternate
      args:
        buildno: 1

command

覆盖容器启动后默认执行的命令。

command: echo "hello world"

container_name

指定容器名称。默认将会使用 项目名称_服务名称_序号 这样的格式。

container_name: docker-web-container

注意: 指定容器名称后,该服务将无法进行扩展(scale),因为 Docker 不允许多个容器具有相同的名称。

depends_on

解决容器的依赖、启动先后的问题。以下例子中会先启动 redis db 再启动 web

version: '3'

services:
  web:
    build: .
    depends_on:
      - db
      - redis

  redis:
    image: redis

  db:
    image: postgres

注意:web 服务不会等待 redis db 「完全启动」之后才启动。

env_file

从文件中获取环境变量,可以为单独的文件路径或列表。

如果通过 docker-compose -f FILE 方式来指定 Compose 模板文件,则 env_file 中变量的路径会基于模板文件路径。

如果有变量名称与 environment 指令冲突,则按照惯例,以后者为准。

env_file: .env

env_file:
  - ./common.env
  - ./apps/web.env
  - /opt/secrets.env

环境变量文件中每一行必须符合格式,支持 # 开头的注释行。

# common.env: Set development 
environmentPROG_ENV=development

environment

设置环境变量。你可以使用数组或字典两种格式。

只给定名称的变量会自动获取运行 Compose 主机上对应变量的值,可以用来防止泄露不必要的数据。

environment:
  RACK_ENV: development
  SESSION_SECRET:

environment:
  - RACK_ENV=development
  - SESSION_SECRET

如果变量名称或者值中用到 true|false,yes|no 等表达 布尔 含义的词汇,最好放到引号里,避免 YAML 自动解析某些内容为对应的布尔语义。这些特定词汇,包括

y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF

healthcheck

通过命令检查容器是否健康运行。

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost"]
  interval: 1m30s
  timeout: 10s
  retries: 3

image

指定为镜像名称或镜像 ID。如果镜像在本地不存在,Compose 将会尝试拉取这个镜像。

image: ubuntu
image: orchardup/postgresql
image: a4bc65fd

networks

配置容器连接的网络。

version: "3"
services:

  some-service:
    networks:
     - some-network
     - other-network

networks:
  some-network:
  other-network:

ports

暴露端口信息。

使用宿主端口:容器端口 (HOST:CONTAINER) 格式,或者仅仅指定容器的端口(宿主将会随机选择端口)都可以。

ports:
 - "3000"
 - "8000:8000"
 - "49100:22"
 - "127.0.0.1:8001:8001"

注意:当使用 HOST:CONTAINER 格式来映射端口时,如果你使用的容器端口小于 60 并且没放到引号里,可能会得到错误结果,因为 YAML 会自动解析 xx:yy 这种数字格式为 60 进制。为避免出现这种问题,建议数字串都采用引号包括起来的字符串格式。

sysctls

配置容器内核参数。

sysctls:
  net.core.somaxconn: 1024
  net.ipv4.tcp_syncookies: 0

sysctls:
  - net.core.somaxconn=1024
  - net.ipv4.tcp_syncookies=0

ulimits

指定容器的 ulimits 限制值。

例如,指定最大进程数为 65535,指定文件句柄数为 20000(软限制,应用可以随时修改,不能超过硬限制) 和 40000(系统硬限制,只能 root 用户提高)。

  ulimits:
    nproc: 65535
    nofile:
      soft: 20000
      hard: 40000

volumes

数据卷所挂载路径设置。可以设置为宿主机路径(HOST:CONTAINER)或者数据卷名称(VOLUME:CONTAINER),并且可以设置访问模式 (HOST:CONTAINER:ro)。

该指令中路径支持相对路径。

volumes:
 - /var/lib/mysql
 - cache/:/tmp/cache
 - ~/configs:/etc/configs/:ro

如果路径为数据卷名称,必须在文件中配置数据卷。

version: "3"

services:
  my_src:
    image: mysql:8.0
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:

5.4 常用命令

1. 命令对象与格式

对于 Compose 来说,大部分命令的对象既可以是项目本身,也可以指定为项目中的服务或者容器。如果没有特别的说明,命令对象将是项目,这意味着项目中所有的服务都会受到命令影响。

执行 docker-compose [COMMAND] --help 或者 docker-compose help [COMMAND] 可以查看具体某个命令的使用格式。

docker-compose 命令的基本的使用格式是

docker-compose [-f=<arg>...] [options] [COMMAND] [ARGS...]
2. 命令选项
  • -f, --file FILE 指定使用的 Compose 模板文件,默认为 docker-compose.yml,可以多次指定。
  • -p, --project-name NAME 指定项目名称,默认将使用所在目录名称作为项目名。
  • --x-networking 使用 Docker 的可拔插网络后端特性
  • --x-network-driver DRIVER 指定网络后端的驱动,默认为 bridge
  • --verbose 输出更多调试信息。
  • -v, --version 打印版本并退出。
3.命令使用说明
up

格式为 docker-compose up [options] [SERVICE...]

  • 该命令十分强大,它将尝试自动完成包括构建镜像,(重新)创建服务,启动服务,并关联服务相关容器的一系列操作。

  • 链接的服务都将会被自动启动,除非已经处于运行状态。

  • 可以说,大部分时候都可以直接通过该命令来启动一个项目。

  • 默认情况,docker-compose up 启动的容器都在前台,控制台将会同时打印所有容器的输出信息,可以很方便进行调试。

  • 当通过 Ctrl-C 停止命令时,所有容器将会停止。

  • 如果使用 docker-compose up -d,将会在后台启动并运行所有的容器。一般推荐生产环境下使用该选项。

  • 默认情况,如果服务容器已经存在,docker-compose up 将会尝试停止容器,然后重新创建(保持使用 volumes-from 挂载的卷),以保证新启动的服务匹配 docker-compose.yml 文件的最新内容


down
  • 此命令将会停止 up 命令所启动的容器,并移除网络

exec
  • 进入指定的容器。

ps

格式为 docker-compose ps [options] [SERVICE...]

列出项目中目前的所有容器。

选项:

  • -q 只打印容器的 ID 信息。

restart

格式为 docker-compose restart [options] [SERVICE...]

重启项目中的服务。

选项:

  • -t, --timeout TIMEOUT 指定重启前停止容器的超时(默认为 10 秒)。

rm

格式为 docker-compose rm [options] [SERVICE...]

删除所有(停止状态的)服务容器。推荐先执行 docker-compose stop 命令来停止容器。

选项:

  • -f, --force 强制直接删除,包括非停止状态的容器。一般尽量不要使用该选项。
  • -v 删除容器所挂载的数据卷。

start

格式为 docker-compose start [SERVICE...]

启动已经存在的服务容器。


stop

格式为 docker-compose stop [options] [SERVICE...]

停止已经处于运行状态的容器,但不删除它。通过 docker-compose start 可以再次启动这些容器。

选项:

  • -t, --timeout TIMEOUT 停止容器时候的超时(默认为 10 秒)。

top

查看各个服务容器内运行的进程。


unpause

格式为 docker-compose unpause [SERVICE...]

恢复处于暂停状态中的服务。

至此 docker 相关的内容就结束了,接下来会计划一篇基于 docker 的一个环境搭建,具体搭建什么还没有想好

8

评论区