Docker是开源的,基于Linux容器技术的引擎,统一了被隔离的应用程序访问系统核心的API。试图解决开发者的世纪难题在我的机器上可以跑。
前端同学可以视镜像为npm包,仓库为npm仓库。这样更方便理解。
Docker是一种类似虚拟机技术的缩减版,由于虚拟机启动过程过于漫长与虚拟化之后的硬件在运行程序的时候,并不能很好的契合物理机,比较典型的例子就是移动端开发,启动虚拟系统的时候,过程十分的漫长。
我们经常开启一个虚拟机仅仅是需要隔离一个应用,但是虚拟机创建占用了一套完整的系统资源(guest os),存在着大材小用的问题,成本也息息相关。
而Docker随着Linux功能的更新出现了,Docker本质仅隔离应用程序,共享当前系统核心。
下图为虚拟机与Docker架构对比:
下图为容器虚拟机功能对比:
这样的话,Docker就可以进行秒启动,因为Docker跳过了系统初始化(kernel init),直接使用了当前系统核心。但是这个也是有弊病的,比如 虚拟机热迁移 这个功能,Docker就做的不是很好。
使用Docker可以快速的搭建配置应用环境,简化操作,确保运行环境一致性“一次编译 到处运行”,应用级隔离,弹性伸缩,快速拓展。
镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。 镜像不包含任何动态数据,其内容在构建之后也不会被改变。
镜像利用(union file system)提供应用运行的只读模板,它可以只提供一个功能,也可以由多个镜像叠加创建多个功能服务。
镜像仅仅是定义隔离应用运行所需要的东西,容器则是运行这些镜像的进程。在容器内部,提供了完整的 文件系统、网络、进程空间等等。完全隔离于外部环境,不会受到其他应用的侵扰。
容器的读写必须使用**Volume**,或者宿主的存储环境,容器在重启或者关闭之后,存在于运行容器内部的数据将会丢失,每次启动容器,都是通过镜像创建一个新的容器。
Docker 仓库是集中存放镜像文件的场所。镜像构建完成后,可以很容易的在当前宿主上运行,但是, 如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry (仓库注册服务器)就是这样的服务。有时候会把仓库 (Repository) 和仓库注册服务器 (Registry) 混为一谈,并不严格区分。Docker 仓库的概念跟 Git 类似,注册服务器可以理解为 GitHub 这样的托管服务。实际上,一个 Docker Registry 中可以包含多个仓库 (Repository) ,每个仓库可以包含多个标签 (Tag),每个标签对应着一个镜像。所以说,镜像仓库是 Docker 用来集中存放镜像文件的地方类似于我们之前常用的代码仓库。
通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本 。我们可以通过<仓库名>:<标签>的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签.。
仓库又可以分为两种形式:
Docker client是一个泛称,用来向指定的Docker Engine发起请求,执行相应的容器管理操作.它既可以是Docker命令行工具,也可以是任何遵循了Docker API的客户端.目前, 社区中维护着的Docker client种类非常丰富,涵盖了包括C#(支持 Windows)、Java、Go、Ruby、JavaScript等常用语言,甚至还有使用Angular库编写的WebU格式的客户端,足以满足大多数用户的需求。
Docker Engine是Docker最核心的后台进程,它负责响应来自Docker client的请求,然后将这些请求翻译成系统调用完成容器管理操作。该进程会在后台启动一个API Server,负责接收由Docker client发送的请求;接收到的请求将通过Docker Engine内部的一个路由分发调度,再由具体的函数来执行请求。
##2.1 安装Docker
本文所有环境均运行在centos7下。
首先,移除所有老版本的Docker。
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
如果,是一个全新环境,那可以跳过这一步。
为国内一些原因,所以按照官网安装docker-ce大概率是装不上的,所以,我们需要国内镜像来加速安装。下面我们通过aliyun加速安装。
# step 1: 安装必要的一些系统工具
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
# Step 2: 添加软件源信息
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# Step 3: 更新并安装 Docker-CE
sudo yum makecache fast
sudo yum -y install docker-ce
# Step 4: 开启Docker服务
sudo service docker start
安装完成之后可以运行docker version检查是否安装成功。
➜ ~ docker version
Client: Docker Engine - Community
Version: 19.03.3
API version: 1.40
Go version: go1.12.10
Git commit: a872fc2f86
Built: Tue Oct 8 00:58:10 2019
OS/Arch: linux/amd64
Experimental: false
Server: Docker Engine - Community
Engine:
Version: 19.03.3
API version: 1.40 (minimum version 1.12)
Go version: go1.12.10
Git commit: a872fc2f86
Built: Tue Oct 8 00:56:46 2019
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.2.10
GitCommit: b34a5c8af56e510852c35414db4c1f4fa6172339
runc:
Version: 1.0.0-rc8+dev
GitCommit: 3e425f80a8c931f88e6d94a8c831b9d5aa481657
docker-init:
Version: 0.18.0
GitCommit: fec3683
##2.2 获取一个镜像
现在我们需要拉取一个nginx镜像,部署一个nginx应用。
➜ ~ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
68ced04f60ab: Pull complete
28252775b295: Pull complete
a616aa3b0bf2: Pull complete
Digest: sha256:2539d4344dd18e1df02be842ffc435f8e1f699cfc55516e2cf2cb16b7a9aea0b
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
拉取完成之后,使用docker image ls查看当前docker本地镜像列表。
➜ ~ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 6678c7c2e56c 13 hours ago 127MB
重新运行相同的命令docker pull nginx会更新本地镜像。
##2.3 运行一个Docker容器
创建一个shell脚本文件,写入以下内容:
docker run \
# 指定容器停止后的重启策略:
# no:容器退出时不重启
# on-failure:容器故障退出(返回值非零)时重启
# always:容器退出时总是重启
--restart=always \
# 指定docker运行在后台,如果不加-d,在执行完这条命令之后
# 你退出命令行也会将这个docker容器退掉
-d \
# 将宿主机端口号绑定至容器端口上
-p 8080:80 \
# 指定容器暴露的端口,即修改镜像的暴露端口
--expose=80 \
# 映射宿主目录至
-v /wwwroot:/usr/share/nginx/html \
# 指定容器名字,后续可以通过名字进行容器管理,links特性需要使用名字
--name=testdocker \
# 用哪个镜像初始化这个容器
nginx:lastest
我们要明确一点docker的容器网络是与宿主机隔离的,除非指定容器网络模式依托宿主机,否则是无法直接访问的。
现在我们运行这个脚本,接着打开浏览器,输入http://ip:8080就可以看到使用nginx镜像运行的应用程序了。
Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
02.
03. -d, --detach=false 指定容器运行于前台还是后台,默认为false
04. -i, --interactive=false 打开STDIN,用于控制台交互
05. -t, --tty=false 分配tty设备,该可以支持终端登录,默认为false
06. -u, --user="" 指定容器的用户
07. -a, --attach=[] 登录容器(必须是以docker run -d启动的容器)
08. -w, --workdir="" 指定容器的工作目录
09. -c, --cpu-shares=0 设置容器CPU权重,在CPU共享场景使用
10. -e, --env=[] 指定环境变量,容器中可以使用该环境变量
11. -m, --memory="" 指定容器的内存上限
12. -P, --publish-all=false 指定容器暴露的端口
13. -p, --publish=[] 指定容器暴露的端口
14. -h, --hostname="" 指定容器的主机名
15. -v, --volume=[] 给容器挂载存储卷,挂载到容器的某个目录
16. --volumes-from=[] 给容器挂载其他容器上的卷,挂载到容器的某个目录
17. --cap-add=[] 添加权限,权限清单详见:http://linux.die.net/man/7/capabilities
18. --cap-drop=[] 删除权限,权限清单详见:http://linux.die.net/man/7/capabilities
19. --cidfile="" 运行容器后,在指定文件中写入容器PID值,一种典型的监控系统用法
20. --cpuset="" 设置容器可以使用哪些CPU,此参数可以用来容器独占CPU
21. --device=[] 添加主机设备给容器,相当于设备直通
22. --dns=[] 指定容器的dns服务器
23. --dns-search=[] 指定容器的dns搜索域名,写入到容器的/etc/resolv.conf文件
24. --entrypoint="" 覆盖image的入口点
25. --env-file=[] 指定环境变量文件,文件格式为每行一个环境变量
26. --expose=[] 指定容器暴露的端口,即修改镜像的暴露端口
27. --link=[] 指定容器间的关联,使用其他容器的IP、env等信息
28. --lxc-conf=[] 指定容器的配置文件,只有在指定--exec-driver=lxc时使用
29. --name="" 指定容器名字,后续可以通过名字进行容器管理,links特性需要使用名字
30. --net="bridge" 容器网络设置:
31. bridge 使用docker daemon指定的网桥
32. host //容器使用主机的网络
33. container:NAME_or_ID >//使用其他容器的网路,共享IP和PORT等网络资源
34. none 容器使用自己的网络(类似--net=bridge),但是不进行配置
35. --privileged=false 指定容器是否为特权容器,特权容器拥有所有的capabilities
36. --restart="no" 指定容器停止后的重启策略:
37. no:容器退出时不重启
38. on-failure:容器故障退出(返回值非零)时重启
39. always:容器退出时总是重启
40. --rm=false 指定容器停止后自动删除容器(不支持以docker run -d启动的容器)
41. --sig-proxy=true 设置由代理接受并处理信号,但是SIGCHLD、SIGSTOP和SIGKILL不能被代理
我们可以使用docker exec -it [docker container id] /bin/bash 来进入正在运行的容器。
而要退出容器,有两种方式:
以上两种方式均可从容器中退出,并且保持容器在后台运行。
##2.5 自定义一个镜像Dockerfile
Dockerfile 分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。
这里我使用了一份简单的node启动开发环境的Dockerfile。
# 1. 设置来源的基础镜像
FROM node:12.0
# 指定后续 RUN、CMD、ENTRYPOINT 指令的工作目录
WORKDIR /workspace
# 在上一次通过WORKDIR指定的目录运行 RUN 后续命令
RUN npm install --registry=https://registry.npm.taobao.org
# 初始化暴露 8080 8001 8800端口号
# 以下端口号 也可以在docker run时暴露出去
EXPOSE 8080
EXPOSE 8001
EXPOSE 8800
# 默认执行的命令,如果在宿主机通过docker run -it /bin/bash进入时,以下命令不会被执行
# 完全不被覆盖的指令为 ENTRYPOINT
CMD ["npm","run","dev-server"]
保存退出编辑,执行docker build -t nodeapp:v1.0 . 注意 最后一个.表示当前目录。
在运行完成之后,使用docker image ls查看是否有已经编译完成的镜像即可。
这个时候,可能有些人有个提问,就是每次都需要npm install安装文件吗?
其实,如果你的node应用包不会变化,而你这个镜像又是专为此应用开发的,可以考虑使用ADD指令,将node_modules追加至docker镜像中。(现实基本不会如此处理,因为外部如果映射了数据卷进来会覆盖目录,此处只是为了演示像镜像追加文件。)
##2.6 多容器启动:Docker-compose
Docker-compose 需要单独安装。
我们来假设一个场景,我们启动了一个前端项目。需要启动nginx运行前台项目,启动一个数据库来记录数据,保证整个应用的完整。那么这就是docker-compose的用武之地了。
首先要知道,docker-compose由以下两种类型组成:
我们回到之前创建Dockerfile的目录中,编写一个docker-compose.yml文件,配置多容器。
version: '1'
services:
web:
build: .
ports:
- "8080:80"
volumes:
- /wwwroot:/usr/share/nginx/html
redis:
image: "redis:alpine"
运行命令docker-compose up之后,我们通过docker stats可以看到两个(web、redis)docker已经启动了。
访问http://ip:8080就可以看到跟之前一样的网页了。
由于应有隔离,你无法在外网直接访问到宿主机上的docker容器。所以我们需要将宿主机上的端口绑定到容器上。
2.3已经介绍了如何绑定端口与导出容器端口。我们这里了解一下容器互联。
# 运行命令创建一个docker网络
$ docker network create -d bridge my-net
# 创建两个容器 加入my-net网络
$ docker run -it --rm --name busybox1 --network my-net busybox sh
$ docker run -it --rm --name busybox2 --network my-net busybox sh
# 接着我们进入busybox1
$ docker exec -it busybox1 /bin/bash
# ping 另外一个容器,可以看到他的IP信息
$ root@busybox1:ping busybox2
PING busybox2 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.072 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.118 ms
Docker使用Go语言编写,并且使用了一系列Linux内核提供的特性来实现其功能。
一个能执行Docker的系统分为两大部分:
Docker使用的Linux核心模块功能包括下列各项:
使用虚拟机运行Linux,然后在Linux中运行Docker Engine。在本机运行Docker client。
前端同学关注点
为什么不能用CMD ['node','app.js']作为默认启动,因为在 Node.js 的官方最佳实践里有写到 "Node.js was not designed to run as PID 1 which leads to unexpected behaviour when running inside of Docker."。下图来自 github。
这个问题涉及linux运行机制,简单的说,就是 linux pid为1的进程是系统守护的进程,将会接收所有孤儿进程。并且在适当的时候发送关闭信号给这些进程。
但是,docker中pid 1的进程为node,而node并没有做回收孤儿进程的事情。所以,如果你的应用跑类似爬虫之类的应用,执行完毕之后将进程挂到pid 1 上,慢慢的容器就会BOOM。
解决方案:
1. 用`/bin/bash`启动。
2. 在`docker run`后面追加`--init`用于初始化一个docker的进程为pid 1。docker提供的进程可以回收所有孤儿进程。