1. Docker简介
由go
语言开发,基于Linux内核的cgroup
、namespace
等技术,对进程进行封装隔离。属于操作系统层面的虚拟化技术。
传统的虚拟机通过在宿主主机中运行 hypervisor 来模拟一整套完整的硬件环境,在其上运行一个完整操作系统,再在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。
前者具有以下优点:
- 更高效的系统资源利用率
- 更快的启动时间
- 通过
Dockerfile
快速进行镜像构建,更轻松的迁移和部署交付
2. 基本概念
2.1 Dokcer镜像
一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。
镜像不包含任何动态数据,其内容在构建之后也不会被改变。镜像利用Union FS技术,被设计为分层存储的架构,由多层文件系统组层,可以运行docker info
查看当前Union FS 类型
镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。分层存储的特征使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。
2.2 容器
容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。
每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为 容器存储层。任何保存于容器存储层的信息都会随容器删除而丢失。所有的文件写入操作,都应该使用 数据卷(Volume)、或者 绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。
2.3 仓库
如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,这就是Docker Registry。与git类似,一个 Docker Registry 中可以包含多个 仓库(Repository);每个仓库可以包含多个 标签(Tag);每个标签对应一个镜像。最常使用的 Registry 公开服务是官方的 Docker Hub。还有一些功能丰富的开源的仓库,例如 harbor
2.4 Dockerfile
Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction)表明如何构建镜像,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。每一层构建的最后一定要清理掉无关文件,避免制作出很臃肿的镜像。
主要命令包括(https://docs.docker.com/engine/reference/builder/#from):
-
FROM
就是指定 基础镜像。若不存在基础镜像,则使用FROM scratch
。RUN
指令是用来执行命令行命令的。 -
COPY
指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置 -
ADD
与COPY
类似,支持自动解压缩等功能,不支持缓存 -
CMD
指令就是用于指定默认的容器主进程的启动命令的,只能指定一个,否则后面的会覆盖前面的CMD <命令> # 或 CMD ["可执行文件", "参数1", "参数2"...]
-
ENTRYPOINT
的目的和CMD
一样,都是在指定容器启动程序及参数。当指定了ENTRYPOINT
后,CMD
的含义就发生了改变,不再是直接的运行其命令,而是将CMD
的内容作为参数传给ENTRYPOINT
指令CMD ["参数1", "参数2"...] ENTRYPOINT <命令>
-
ENV
设置环境变量ENV <key> <value> ENV <key1>=<value1> <key2>=<value2>...代码片
-
ARG
也是设置环境变量,但相比ENV
,其设置的构建环境的环境变量,在将来容器运行时不存在。 -
VOLUME
设置数据卷VOLUME ["<路径1>", "<路径2>"...] VOLUME <路径> VOLUME /data docker run -d -v mydata:/data xxxx
-
EXPOSE
暴露端口,其只是声明,并不会自动进行端口暴露,需要在运行时使用-p <宿主端口>:<容器端口>
EXPOSE <端口1> [<端口2>...]
-
WORKDIR
指定工作目录(或者称为当前目录)。每一个RUN
都是启动一个容器、执行命令、然后提交存储层文件变更。因此不同层间需要改变目录则需要使用该命令WORKDIR /a WORKDIR b WORKDIR c RUN pwd # /a/b/c
-
USER
指令和WORKDIR
用法相似,用于指定用户。USER <用户名>[:<用户组>]
-
ONBUILD
是一个特殊的指令,它后面跟的是其它指令,比如RUN
,COPY
等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。FROM node:slim RUN mkdir /app WORKDIR /app ONBUILD COPY ./package.json /app ONBUILD RUN [ "npm", "install" ] ONBUILD COPY . /app/ CMD [ "npm", "start" ]
-
LABEL
指令用来给镜像以键值对的形式添加一些元数据LABEL <key>=<value> <key>=<value> <key>=<value> ...
多阶段构建镜像
FROM golang:1.9-alpine as builder # as命令为某阶段命名
RUN apk --no-cache add git
WORKDIR /go/src/github.com/go/helloworld/
RUN go get -d -v github.com/go-sql-driver/mysql
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest as prod
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/go/helloworld/app . # 从第一个镜像中复制文件
CMD ["./app"]
单独运行某阶段
$ docker build --target builder -t username/imagename:tag .
2.5 容器网络
当 Docker 启动时,会自动在主机上创建一个 docker0 虚拟网桥( 参考1,参考2),实际上是 Linux 的一个 bridge,可以理解为一个软件交换机。它会在挂载到它的网口之间进行转发。
同时,Docker 随机分配一个本地未占用的私有网段(在 RFC1918 中定义)中的一个地址给 docker0 接口。比如典型的 172.17.42.1,掩码为 255.255.0.0。此后启动的容器内的网口也会自动分配一个同一网段(172.17.0.0/16)的地址。
当创建一个 Docker 容器的时候,同时会创建了一对 veth pair 接口(当数据包发送到一个接口时,另外一个接口也可以收到相同的数据包)。这对接口一端在容器内,即 eth0;另一端在本地并被挂载到 docker0 网桥,名称以 veth 开头(例如 vethAQI2QT)。通过这种方式,主机可以跟容器通信,容器之间也可以相互通信。Docker 就创建了在主机和所有容器之间一个虚拟共享网络。
3. 常用命令
3.1 镜像相关
从仓库拉取镜像
docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
列出镜像
docker image ls [仓库名[:标签]] [选项]
查看镜像、容器、数据卷占用的使用空间
docker system df
查看并消除虚悬镜像(dangling image)
docker image ls -f dangling=true
docker image prune
删除本地镜像
删除行为分为两类,一类是 Untagged
,另一类是 Deleted
。镜像的唯一标识是其 ID 和摘要,而一个镜像可以有多个标签。当删除指定标签镜像时,若还有别的标签指向这个镜像,则为 Untagged
,此时并不会真正删除镜像。
docker image rm [选项] <镜像1> [<镜像2> ...]
将容器保存为镜像
docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]
$ docker commit \
--author "zjw" \
--message "修改了默认网页" \
webserver \
nginx:v2
查看当前镜像内历史记录
docker history <镜像>
构建镜像
Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具(例如命令行),客户端调用远程服务进行各种操作。当构建的时候,用户会指定构建镜像上下文的路径,docker build
命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
docker build [选项] <上下文路径/URL/->
$ docker build -t nginx:v3 .
#基于git构建
$ docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world
#基于压缩包构建
$ docker build http://server/context.tar.gz
3.2 容器相关
新建容器并启动
检查本地是否存在指定的镜像,不存在就从公有仓库下载
1.利用镜像创建并启动一个容器
2.分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
3.从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
4.从地址池配置一个 ip 地址给容器
5.执行用户指定的应用程序
6.执行完毕后容器被终止
docker run -it ubuntu:18.04 /bin/bash #获取用户交互
docker run -d ubuntu:18.04 #后台运行,输出结果使用 docker container logs查看
启动已终止的容器
docker start <name>
进入容器
docker exec -it <name> bash
查看容器列表
docker ps
终止容器
docker stop <name>
删除容器
docker rm <name> # -f 为强制停止容器并删除
查看容器详细信息
docker inspect <name>
3.3 存储相关
创建数据卷
docker volume create my-vol
查看数据卷列表
docker volume ls
查看数据卷详细信息,包括挂载点、驱动等
docker volume inspect my-vol
删除数据卷
docker volume rm my-vol
挂载数据卷
docker run -d -P \
--mount source=my-vol,target=/usr/share/nginx/html \
nginx:alpine
挂载主机目录
$ docker run -d -P \
--mount type=bind,source=/src/webapp,target=/usr/share/nginx/html,readonly \
nginx:alpine
3.4 网络相关
随机映射一个端口到内部容器开放的网络端口
docker run -d -P nginx:alpine
指定映射端口,本地 : 容器
docker run -d -p 80:80 nginx:alpine
docker run -d \
-p 80:80 \
-p 443:443 \
nginx:alpine
创建docker网络
docker network create -d bridge my-net
连接容器网络
docker run -it --rm --name busybox1 --network my-net busybox sh
docker run -it --rm --name busybox2 --network my-net busybox sh
4. 底层原理
Docker 底层的核心技术包括 Linux 上的命名空间(Namespaces)、控制组(Control groups)、Union文件系统(Union file systems)和容器格式(Container format)
Docker 采用了 C/S架构,包括客户端和服务端。Docker 守护进程 (Daemon)作为服务端接受来自客户端的请求,并处理这些请求(创建、运行、分发容器)。
客户端和服务端既可以运行在一个机器上,也可通过 socket 或者 RESTful API 来进行通信。
docker通过命名空间隔离文件目录、进程、网络、user等,通过控制组隔离共享底层的资源,比如cpu、内存、磁盘IO、网络带宽等。通过Union文件系统实现镜像分层。参考