1. 什么是容器技术?
注意:容器本身只是一种技术规范,而Docker中的container实现了这个规范。
容器
是一种快速的打包技术,它的特点是
- 标准化:标准化指的是打包的方式的标准化,通过这个标准化的打包方式就能够将内部异构的软件结构通过一定标准的打包过程,就这些软件打包成一个标准的,开箱即用的容器,这个容器对于外部操作的引擎来说,尽管他们内部的实现有很大不同,但是从外部来看,他们都是一样的
- 轻量级:与虚拟机相比,容器的打包与允许对标的是VM的镜像生成和运行,由于容器的运行是共用操作系统内核的,因此在这样的情况下,它的启动是秒级启动,本质上是在操作系统内部运行一个新的进程组,而VM的运行则是严格地划分了宿主机和虚拟机之间的空间,然后在虚拟机空间重新运行一套自己的操作系统内核,这个过程需要经过操作内核到的初始化,硬件资源的虚拟化分配,因此启动是分钟级的启动
- 容易移植:这个特性是基于标准化的特性来实现的,因为标准,从而能够保证在这套标准执行下构建的容器能够在各个虚拟机上运行
2. 容器之间是如何实现相互隔离的?
关键技术:
Namespace
:我们说虚拟机之间,它们需要有一套自己的操作系统内核,他们是通过虚拟机软件生成这些操作系统内核软件来实现的,从而可以保证宿主机和虚拟机之间的运行是相互隔离的,但是对于Docker
容器技术来说,为了使得这些容器之间的相互运行是互不干扰的,因此利用的是Linux
中的Namespace
机制,它主要用来先资源的隔离CGroup
:我们说虚拟机之间,他们其实也是共享硬件资源的,只是通过模拟硬件的手段,来实现各个不同的虚拟机之间在相同的硬件上使用相关的资源,但是注意这是严格隔离的,比如说每个虚拟机都有自己的虚拟CPU,当需要使用CPU的时候,要通过虚拟化软件进行申请,CGroup能够限制各个容器之间的资源使用情况,比如说,我们在Docker
中要挂起一个容器,实际上是挂起了一个Namespace
中的进程组中所有的进程,使得进程组中的进程暂时无法获得CPU,看起来好像他们就被挂起了一样,同时通过限制CPU/MEM
,进程组的优先级,就可以控制这些容器的运行情况
3. 容器是如何实现标准化的?
首先,docker
它本身并不等价于container
,这句话可以从两个角度来解读
container
本身来说是一项技术标准,它规定以什么样的方式来构建软件,而docker
只是实现了这一项技术标准
而docker
除了container
这一项技术标准之外,还涉及到镜像
、仓库
等核心概念,这些概念丰富了容器构建技术标准
那么docker在实现这些标准化的时候,它实现了两个标准
- 容器运行时标准化:规定了容器的基本操作规范,比如说如何创建容器,如何开始容器,如何启动容器
- 容器构建时标准化:主要规定了构建容器的镜像模板
4. Docker的架构是怎么样的?
Docker
的架构从整体上来说是一个C/S
架构,Client通过发送诸如docker build、docker pull、docker run
等指令,从Client
来看,它就好像是Docker_Host
接收了指令并且执行,但是实际上是Docker daemon
后台线程在解析这些指令,然后根据这些指令的意图,调用底层重要的三个部分的内容,分别是:
- Container:容器操作引擎
- Images:镜像操作引擎
- Registry:镜像仓库引擎
5. 怎么知道Docker运行环境的安全性?
容器静态构建的安全策略
- 可以借助于一个
docker-bench
的工具来进行实现
为什么能够检测出来?它是通过检查代码的结构,以及构建镜像的Dockefile
做一个特征的识别,然后将这个识别出来的结果与CVE
数据库中收录的特征码进行比对,如果相同的话就进行收集,最终提供给用户
一般来说会检测出来这个特征码具体是什么漏洞,然后开发的人要根据这个漏洞的具体情况进行取舍,会分为三个四个等价严重
、高
、中
、低
容器动态运行的安全策略
也可以通过sysdig
工具来实现
6. 创建一个容器到底发生了什么?
比如说我给你一个下面的指令,说一下到底是怎么执行的
docker container run -d --publish 80:80 --name redis redis:latest
首先大体上来说,构建并且运行一个容器具体可以分成三步:
- 第一步,先从本地的
registry
中查找有没有redis
这个镜像 - 如果没有本地上没有这个镜像,那么就会在
docker_daemon.json
文件中规定的registry
中去下载镜像,如果没有设定registry
或者是在这个仓库中没有找到
,那么就会到dockerhub
中查询,如果都没有,那么最终创建就会失败 - 下载镜像,接着就会依照查询到的镜像来构建一个容器
- 当容器被构建出来之后,由于我们使用了
publish
的参数,那么就意味着需要docker engine
给这个容器分配一个IP
,然后设定对应的端口映射,其实就是在docker engine
路由表中设定了一个路由 - 然后最后在容器中,隐式地运行一个
.sh
脚本,最终这个容器就运行起来了
7. docker的基本操作
docker version
:查询docker的版本,可以看到docker的Client所对应的架构和Server所对应的架构
可以注意到的是MAC/Windows
下的Server
的OS/Arch
均为Linux/amd64
docker info
:通过这个指令就可以知道docker
的一些元信息了,比如说有全部镜像的信息,全部容器的信息等
docker container --help
docker container ps#当前正在运行的容器
docker container ps -a #列出所有容器(不管有没有运行)
docker images ls #查询所有的镜像
docker images rm imagesId/imagesName#删除指定的镜像
简单说说什么是docker镜像?
Docker Image
是一个RO
文件,这个文件中描述了文件系统,源代码,库文件,依赖,工具等一些application
所需要的文件,你可以把它理解成一个模板,docker image具有分层的概念
简单说说什么是docker容器?
可以理解为一个运行中的docker image
,它实质上是复制了image
并且在image
最上层加一层read-write
的层,这一层称为是容器层(container layer)
,基于同一个image
可以创建多个container
ps和ls
有什么区别?一个是远古时期的,一个是当前版本用的
如何批量删除容器?
docker container rm -f $(docker container ls -aq)
attached模式和detached模式有什么区别?
首先attached
称为是前台执行模式,而detached
称为是后台执行模式
attached
意味着容器内部执行的情况会同步反映到我们的终端上,而我们在终端上执行的操作也会同步反映到到容器内部,因此在这种情况下,我们以一个前台模式运行的时候,输入Ctrl+C
的时候,这个终止指令也会传入到容器中,导致容器停止运行,在windows
下的执行,实际上用了一个telnet
,它仅仅只是将远程的显示输出到windows
的终端,而在windows
不会将终端的输入传输到容器中
detached
:后台执行模式,它直接将容器在后台运行
docker attach containerId
如果我们输入一个Ctrl+C
的时候,就会导致这个信号同时被传入到容器中,导致容器中运行中的脚本被中断,然后容器执行结束
为什么如果直接运行一个
ubutun
容器会启动不成功?
这是因为我们说在启动容器的时候,实际上最后一步是执行了一个shell
脚本,容器的执行其实就是在执行一个命令,这个命令通常会以一个脚本的形式封装起来,因此当我们如果没有指定命令的话,那么也就是说这个容器中没有执行任何命令,从更本质的角度上来说,就是这个容器所代表的进程组中的指令流全部执行完毕了,那么自然进程组就消亡掉了
docker container run -it ubutun sh
如何查看日志?
docker container logs containerId
#动态地显示,终端以阻塞的形式交互
docker container logs -f containerId
如何交互?
docker exec -it containerId sh
windows是如何运行docker engine的?
windows
运行docker engine
的流程基本如下:
- 首先
Client
是一个Windows
进程,然后通过Socket
的方式连接到Server
(体现C/S)架构 - 然后基于
hyper-V
,构建出来一个linux
虚拟机,然后在这个linux
中运行docker
8. docker容器和虚拟机到底有什么区别?
首先根据这张图,我们首先就知道虚拟机的运行,首先需要有一层虚拟化软件,由这个虚拟化软件来划分硬件资源(具体的来说,就是这个软件代表虚拟机组去申请实际的硬件资源,然后这个软件将申请到的资源调度到实际的虚拟机上),生成操作系统内核,分配操作系统内核空间等操作,然后每一个VM都有一个自己的操作系统内核,通过这个操作系统内核,再在上层安装对应的库和依赖,然后运行对应的APP
而对于容器的运行,我们可以看到,它并不需要虚拟化软件抽象出来自己的操作系统内核
,不需要严格地划分硬件资源,而是通过操作系统内核的系统调用
,为这些运行在容器引擎上的APP限制他们使用的硬件资源,因此,容器本质上来说就是一些进程组,只不过这些进程组运行时的依赖和环境都被打包到了一个命名空间,然后每一个容器的命名空间都是相互独立的,并且由组分配的方式,为这些组分配对应的硬件资源,同时由于容器本质上就是一组容器,那么在这一组进程中,如果全部进程都运行完毕,那么就证明容器运行完毕,最终容器就会进入一个exit
的状态
docker container top containerId
# 查看容器中执行的进程具体信息
容器也就是进程,通过pstree
就可以看到它的父进程,当我们启动一个容器之后就能够看到,这个容器进程组所对应的父进程是一个containerd-shim
,它就是用于启动/创建容器的进程
9. 镜像的基本操作
获取一个镜像的基本方式主要有三种
- 从公开Registry或者私服中去拉取镜像
[pull from]
- 离线导入,通过
.tar
文件来载入镜像[docker load from file]
- 通过Dockerfile来构建
[docker build from Dockerfile]
使用Registry方式
docker hub
quay
docker image pull #拉取docker的镜像,从registry
docker image ls #列出本地具有的镜像
docker image pull quay.io/bitnami/nginx#镜像地址/所属的group/镜像名
docker image inspect#通过这个命令就可以看到这个镜像的更多信息
可以看到它的OS
和Architecture
,其中架构代表的是要使用的是CPU的架构
当删除的镜像被使用了,此时就无法删除了
10. Dockerfile
Dockerfile
用于构建docker
镜像的文件
Dockerfile
包含了构建镜像所需要的指令,如何去构建镜像?
Dockerfile
有其特定的语法规则
FROM ubuntu:21.04#选择基础镜像base-image,类似于Java中的import
RUN ...
ADD ...#将当前目录的文件添加到xx目录中
CMD [""...] #执行相关命令
RUN ["executable","param1","param2"]
ADD [src] [dest]
docker image build -t 镜像名:版本 .
.
:是指解析当前目录下的Dockerfile
通过保存当前容器的状态来产生一个镜像
docker container commit containerId imageId
# 通过当前容器的状态来创建一个新的镜像
Scratch:空镜像
FROM scratch
ADD hello
CMD["/hello"]
通过这个例子,就更加能够理解容器和虚拟机的区别了
虚拟机由于需要自己模拟出来一套操作系统内核软件,因此会带来巨大的开销,但是通过镜像构造容器的方式,就能够使得构建出来的容器体积更小,这是因为不需要模拟操作系统内核,而是共享当前宿主机的内核程序
例子:以构建一个C语言程序,输出helloworld为例,通过gcc
生成可执行文件,这个可执行文件的大约是800
K左右,然后通过查看镜像的大小,发现大约也只是800
K,因此从这个角度上来看,容器是更加轻量级的,不需要每一个容器都模拟一个操作系统软件出来
docker build -t .中的.有什么作用?
比如说使用ADD
等,因为需要获取文件夹中的资源,因此这个.
它指代的是build context
,它用来表达构建镜像的上下文环境,docker-client
会将我们当前所指定的这个build context
发送给docker-server
,如果是.
,它会将当前目录下的所有东西都给发送到docker-server
中,这可能会导致构建速度过慢
在这种情况下,可以使用.dockerignore
.vscode
env/
通过这个文件就可以排除掉当前目录下的不需要的文件了
如何选择基础镜像?
- 官方的镜像优先于非官方的,如何没有官方,那么就选择Dockerfile开源的
- 固定版本tag,而不是使用latest,这是因为如果适应latest的话,那么可能导致每一次构建出来的镜像不稳定,每一次的版本都可能不一致
- 尽量选择体积小的镜像,这取决于
基础镜像的选择
,比如说有alpine
如何使用RUN?
它主要用于在镜像中的预先处理,比如说安装软件,其实就相当于执行一个shell
脚本,但是要注意,每执行一次RUN
,那么就会产生一层layer
docker build -f Dockerfile.bad -t ipad .
#指定dockerfile具体路径
docker image history
#将会看到docker镜像的具体分层
如何优化?比如说你要执行一个mv文件的操作,如果你后面要将文件删除的,那么你就必须要将删除和mv进行合并,否则即使你删除了这个文件,也会导致镜像层次的冗余
原因是构建镜像导致的镜像自增不减
文件复制和目录操作
COPY
和ADD
都可以将local
中的一个文件复制到镜像中,如果镜像不存在,那么就会自动创建
ADD
比COPY
高级的地方在于,如果复制的是目标文件是gzip的话,ADD会帮我们自动解压缩文件
- 在
COPY
和ADD
指令中选择的时候,只要是文件的复制,就都可以使用COPY
,在有需要自动解压缩文件的需求下使用ADD
WorkDir?
WorkDir
:说的是要进行的目录的切换,也就是在进行操作的时候,类似于一个cd
的操作,但是它的一个好处是当目录没有存在的就会自动创建
ARG和ENV?
ARG VERSION=2.0.1
ENV VERSION=2.0.1
RUN echo ${VERSION}
有什么区别?
主要的区别在于ENV
会添加到系统的环境变量中,但是使用ARG
只会存在镜像构建的时候使用,构建完毕后就不会保存到镜像中了,ENV
关注的是镜像运行状态,而ARG
关注的是镜像构建状态
--build-arg list
会覆盖Dockerfile
中的参数值
CMD?
首先我们可以知道,容器从本质上来说就是一个进程,那么我们可以通过自定义指令,来规定在我们所构建的标准化环境中,运行就哪些进程,怎么样来运行进程
- 容器启动的时候的默认命令
- 如果docker container run启动容器时指定了其他的命令,那么CMD命令就会被
忽略掉
- 如果定义了多个CMD,只有最后一个会被执行
如何快速清除后台已经停止运行的容器?
docker container ps -a#查看当前所有的容器情况
docker system prune -f#可以执行清除
docker image prune -a
docker container run -it --rm haha ipconfig#在容器退出后自动删除
EntryPoint
Docker CMD
所设置的命令会被覆盖,但是对于EntryPoint
来说,它一定会被执行
EntryPoint
和CMD
可以联合使用,EntryPoint
设置执行的命令,CMD传递参数
FROM ubutun:21.04
ENTRYPOINT ["echo"]
CMD ["hello,docker "]
在这样的情况下:
docker container run -it --rm demo-entrypoint echo "hello world"
此时将会输出
hello,docker echo hello world
以Shell
的方式执行脚本命令
ENV $NAME="docker"
CMD ["sh","-c","echo hello $NAME"]
总结一下,CMD
的优先级是比较低的,它的存在会被命令行参数所覆盖
Dockerfile最佳实践
合理使用缓存:将经常改变的
dockerfile
命令尽量放到后面,使得构建镜像时尽量使用缓存多阶段构建:在同一个dockerfile中写多个阶段的构建,通过临时引入基础镜像,构建想要的应用程序,从而给镜像减少体积
FROM gcc:9.4 AS builder
COPY hello.c /src/hello.c
WORKDIR /src
RUN gcc --static -o hello hello.c
FROM alpine:3.13.5
COPY /src/hello /src/hello#从上面那个builder中的源目录拷贝到新目录下
...
- 尽量使用非Root用户
docker
的root
权限是一个很大的问题,假设我们有一个用户是demo
,它本身就不具备有sudo
的权限,所以对很多文件无法进行读写操作,比如说root
目录它是无法查看的
但是我们可以通过docker
指令来挂载文件到docker容器中,从而窃取用户主机上的信息
包括还有将文件挂载到docker容器中,然后就能够在容器中对文件进行修改,比如说/etc/sudoers
进行修改,最终就会导致这个用户获取到了宿主机的所有权限,导致非常大的安全隐患
如何使用非root用户?
RUN group add -r flask && useradd -r -g flask flask && chown -R flask:flask /src
USER flask#作为一个用户启动,如果你不指定,那么就会以root用户来使用这个容器
- 通过增加一个用户组和增加一个用户
- 通过USER指定后面的命令要以这个
flask
这个用户的身份运行
#
是root用户,而$
是非root用户
11. Docker如何完成数据的持久化?
首先讲讲为什么需要持久化?
这是因为目前容器对文件的读写操作,实际上是对容器中的容器层进行操作的,而这个容器层是存在容器层面的,一旦容器被删除,那么容器对文件的读写操作就会丢失,那么为了防止因为容器的误删而导致的数据丢失问题,因此就会提供一种技术,让容器对容器内文件的修改直接反映到宿主机中,同时通过这个机制,其他容器也能够去访问这个文件,实现容器间的数据共享,Docker主要提供了两种方式的持久化
- 挂载
volume
:由Docker进程管理的,Docker将对应的数据存储在指定的位置,称为是Docker area
Bind Mount
,用户来指定存储的数据具体要mount
在系统中的什么位置,由用户决定
[root@192 ~]# docker volume ls
DRIVER VOLUME NAME
local 034731fbb2ea8b0a82b4b972ce6a2874b7bf70c470b94071c38d353bde375006
其中这个Driver
表示文件挂载在哪里,是本地磁盘?
[root@192 ~]# docker volume inspect 034731fbb2ea8b0a82b4b972ce6a2874b7bf70c470b94071c38d353bde375006
[
{
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/034731fbb2ea8b0a82b4b972ce6a2874b7bf70c470b94071c38d353bde375006/_data",
"Name": "034731fbb2ea8b0a82b4b972ce6a2874b7bf70c470b94071c38d353bde375006",
"Options": {},
"Scope": "local"
}
]
通过这个MountPoint
,就能够找到挂载的实际数据了
docker container run -d -v cron-data:/app my-cron
这个模式的关键是定义一个
VOLUME ["/cron-data"]
这个关键字到底是起一个什么作用呢?当创建容器的时候,如果不指定-v参数,而且dockefile
中有这个VOLUME
的关键字,那么Docker
进程就会自动地为我们创建这样一个VOLUME
,此时Docker
会自动创建一个具有随机名字的VOLUME
,去存储在Dockerfile
中定义的VOLUME
,
如果我们指定了-v
参数的话,那么我们可以手动设置需要创建的VOLUME
的名字,以及这个VOLUME
所对应的容器的路径,这个路径是任意的,不必在Dockerfile
中通过VOLUME
进行定义
docker system prune -f
docker container run -d -v /usr/local/src:/src
区别在于,一个是通过docker
进程管理的,VOLUME []
会创建VOLUME,并且存储在Docker area
而通过-v src:src
直接挂载的话,不会创建VOLUME