Docker是什么
Docker是一种虚拟化技术,类似虚拟机,这使得安装在其中的程序能够只依赖虚拟机的环境,而不受外部操作系统环境的影响。同虚拟机不同的是,Docker的虚拟容器占用空间更小,使得它比虚拟机更容易分发和多实例安装。
Docker容器化技术的整个开发使用方式非常类似java应用开发,这里同java应用开发做一个类比,帮助有过java开发经验的同学快速掌握其中的核心概念
Dockerfile
相当于Java应用开发中的Maven配置文件pom.xml或则gradle的build.gradle文件。java开发中的pom.xml和build.gradle是用来声明java应用依赖的jar包,和应用的构建方式。而Dockerfile是用来声明一个程序依赖的环境和构建运行方式。比如redis的Dockerfile如下:
# 第一部分,声明redis程序依赖系统环境,是使用的debian
FROM debian:stretch-slim
# 第二部分,配置系统权限,添加新的组和用户,专供redis使用
RUN groupadd -r redis && useradd -r -g redis redis
# 第三部分,是安装系统更新,环境变量配置,以及下载redis并安装
ENV GOSU_VERSION 1.10
RUN set -ex; \
\
fetchDeps=" \
ca-certificates \
dirmngr \
gnupg \
wget \
"; \
apt-get update; \
apt-get install -y --no-install-recommends $fetchDeps; \
rm -rf /var/lib/apt/lists/*; \
\
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; \
chmod +x /usr/local/bin/gosu; \
gosu nobody true; \
\
apt-get purge -y --auto-remove $fetchDeps
ENV REDIS_VERSION 5.0.4
ENV REDIS_DOWNLOAD_URL http://download.redis.io/releases/redis-5.0.4.tar.gz
ENV REDIS_DOWNLOAD_SHA 3ce9ceff5a23f60913e1573f6dfcd4aa53b42d4a2789e28fa53ec2bd28c987dd
# for redis-sentinel see: http://redis.io/topics/sentinel
RUN set -ex; \
\
buildDeps=' \
ca-certificates \
wget \
\
gcc \
libc6-dev \
make \
'; \
apt-get update; \
apt-get install -y $buildDeps --no-install-recommends; \
rm -rf /var/lib/apt/lists/*; \
\
wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL"; \
echo "$REDIS_DOWNLOAD_SHA *redis.tar.gz" | sha256sum -c -; \
mkdir -p /usr/src/redis; \
tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1; \
rm redis.tar.gz; \
\
grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 1$' /usr/src/redis/src/server.h; \
sed -ri 's!^(#define CONFIG_DEFAULT_PROTECTED_MODE) 1$!\1 0!' /usr/src/redis/src/server.h; \
grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 0$' /usr/src/redis/src/server.h; \
\
make -C /usr/src/redis -j "$(nproc)"; \
make -C /usr/src/redis install; \
\
rm -r /usr/src/redis; \
\
apt-get purge -y --auto-remove $buildDeps
# 第四部分,设置redis后续命令的工作目录
RUN mkdir /data && chown redis:redis /data
VOLUME /data
WORKDIR /data
#第五部分,启动redis服务,并配置向外暴露的端口
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 6379
CMD ["redis-server"]
复制代码
可能每个不同的Docker程序,其Dockerfile略有不同,但大致都可以总结为这么几步
- 声明运行系统环境
- 安装系统更新,安装程序
- 配置环境变量
- 设置向外暴露的端口,并启动程序
image
相当于java应用开发中的jar包。java中基于pom.xml或build.gradle build而成jar。而docker中,基于Dockerfile build出的是image。它可以像jar包一样,提交到Docker的中央仓库,并被下发指其它机器使用。一个使用Dockerfile构建image的demo如下:
-
先用python开发一个简单的web服务,名为app.py
from flask import Flask from redis import Redis, RedisError import os import socket # Connect to Redis redis = Redis(host="redis", db=0, socket_connect_timeout=2, socket_timeout=2) app = Flask(__name__) @app.route("/") def hello(): try: visits = redis.incr("counter") except RedisError: visits = "<i>cannot connect to Redis, counter disabled</i>" html = "<h3>Hello {name}!</h3>" \ "<b>Hostname:</b> {hostname}<br/>" \ "<b>Visits:</b> {visits}" return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname(), visits=visits) if __name__ == "__main__": app.run(host='0.0.0.0', port=80) 复制代码
-
再编写Dockerfile
# 从程序代码中,我们知道使用的python,需要依赖python的环境。python环境的image在docker公共仓库中,可以直接使用,在这个image基础上,添加我们的应用,构建另一个image FROM python:2.7-slim # 把容器看做一个小型操作系统的话,这一步设置后续命令在这个容器操作系统内的路径。名字可以任意。相当于普通linux中的cd命令。路径不存在应该可以直接创建。Dockerfile后续的所有命令,都是在这个文件夹下执行的 WORKDIR /app1 # 将宿主机的当前路径内容拷贝到app1下 COPY . /app1 # 从app.py程序代码中,可以看到其依赖FLask库环境和Redis,这里通过pip安装,这一步是在python image的内部执行的,不是外部环境。相当于再给python的image系统镜像安装东西 RUN pip install --trusted-host pypi.python.org Flask RUN pip install --trusted-host pypi.python.org Redis # 将容器的80端口暴露出来。 EXPOSE 80 # 在容器内设置一个环境遍历,key为NAME, value为world。就像linux中设置环境变量一样。只不过这里是在容器这个操作系统内设置环境变量,相应的容器中的程序可以读取这个环境变量 ENV NAME World # 这一步放在最后,前面的所有命令基本上把程序要求的环境都初始化好了,这里直接执行命令,CMD的第一个参数是程序命令,后面的是参数。这里就是通过python来run app.py。 由于当前路径是/app1(前面WORKDIR设置的),并且其中包含app.py,所以在该路径下执行python app.apy当然找得到程序文件 CMD ["python", "app.py"] 复制代码
-
构建image 在宿主机上创建一个文件夹,名字任意,将Dockerfile和app.py 都放置其中(因为Dockerfile中有一个命令COPY . /app1,所以要确保程序跟Dockerfile在同一的路径下,才可以拷贝进去。当然你可以不在一个路径下,那就需要修改Dockerfile命令,将具体app.py的路径写全),然后在该路径下执行构建命令构建image,并将其取名为hellworld
docker build --tag=helloworld .
-
发布image 你可以像发布jar一样,将image发布到docker中央仓库,或公司的私有仓库,具体方式这里就不展开讨论了。
container
类似于java应用中的jar运行。我们基于image运行后,会创建一个运行的实例,即为container,容器。比如我们可以使用以下命令,通过前面build的image,创建一个container
docker run -p 4000:80 helloworld
复制代码
network
container需要对外进行通信,可能需要网络服务。有5种网络驱动可供docker配置,用来配置docker的联网行为。
-
bridge 桥接模式,通过链路层设备链接host网络,它同host使用不同的ip,一般在单节点的host使用这种方式,默认是这种方式
-
host 模式,container直接跟host公用一个ip,这也意味这container暴露什么端口,通过host的ip可直接访问,不推荐这种方式
-
overlay docker集群的网络连接驱动方式
-
Macvlan 对docker配置mac地址,通过物理地址进行网络通信
-
none 使docker没有任何网络连接
data volumes
container中的程序运行时,可能会产生一些数据,或者需要使用一些数据,甚至希望同其它container共享数据。那么实现这些的方式就是data volumes,它对应docker的存储概念,后续会详细讲解。
docker daemon
类似于Java虚拟机。它负责image构建,分发,获取,执行,以及container、volumes、network等上述核心组件的管理,屏蔽底层操作系统的细节,使得基于docker构建的服务能够跨平台。我们一般通过docker CLI也即docker命令行来向docker deamon发送命令执行上述管理。
Docker的基本使用方式
作为普通用户大多数时候,我们只是从中央仓库中获取别人制作好的image,在本地创建container来提供服务,比如获取mysql的image,在本地创建一个mysql的servers。所以下面主要介绍对container的一些核心操作命令。
获取image
使用如下命令去远程仓库中拉取,image文件
docker pull IMAGE[:TAG]
复制代码
比如我们想要获取redis的image,在中央仓库中我们可以看到有很多redis的image,他们用不同的tag区分
docker pull redis:5.0.4-alpine
如何创建一个container
docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
复制代码
其命令主干是docker run IMAGE
。每一次run,都会创建一个新的container
我们基于前面拉到的redis image启动一个containerdocker run redis:5.0.4-alpine
可以在创建的时候指定许多参数,比如创建container时,指定名字docker run --name test-redis redis:5.0.4-alpine
将容器中的程序以后台形式运行docker run --name some-redis -d redis
查看docker相关组件
我们可以使用ls命令,来查看docker中container,image,network,volume等组件的id和名字,就像linux中的ls命令一样。
docker container ls #查看正在运行的container
docker container ls --all #查看包括已停止和运行中的所有container
docker image ls #查看本地拥有的image
docker network ls#查看当前系统具有的网络驱动
docker volume ls#查看当前系统具有的volume存储
复制代码
如何停止一个container
docker container stop CONTAINER_ID|CONTAINER_NAME
复制代码
可以使用container的id或name来将处在run中的container停止
如何启动一个start
通过上述的ls命令,获取到container的名字或id,然后通过命令docker container start CONTAINER_ID|CONTAINER_NAME
来启动容器,举例docker container start 48b24d849908
如何查看container中的程序执行日志
我们可以将container当做一个小的linux系统。那启动后如何登入?有两种方式,第一种是attach命令到指定的容器,比如
sudo docker container attach 48b24d849908
复制代码
但这个命令是只将当前的host终端attach到指定的container中正在运行的进程。并显示其输出。但并不能任意的浏览container的其他系统目录。如果仅仅是为了看当前container中的运行程序日志,大可不必用上述方法,直接用logs命令输出即可(当然这种方式的能看到日志的前提是,container中的程序将日志输出到了STDOUT或STDERR中才行)比如:
sudo docker container logs 48b24d849908
复制代码
如果嫌输出的日志太多,也可以管道加less慢慢看
sudo docker container logs 48b24d849908 | less
复制代码
想要正真的直接登录container去浏览其系统文件,需要使用一下命令
sudo docker container exec -it 48b24d849908 /bin/bash
复制代码
当然这个要容器里确实有bash程序才行。exec还可以run程序中的其他命令
如何启动一个一次性的container
基于image创建一个container后,如果不主动删除,那么该container会一直存在,若以希望container被停止后,自动删除。那么可以在创建命令run中加参数--rm
。例如:
docker run --rm --name some-redis -d redis
复制代码
如何让容器自动重启
有时我们希望宿主机在重启后,或docker deamon重启后,相应的container能自动重启。那么在创建container时,使用参数--restart
来控制重启行为。重启策略主要有以下几种
-
no 默认选项,不会自动重启container
-
on-failure 当container非正常退出时,自动重启
-
always 无论什么情况都自动重启。但手动停止容器后,需要docker daemon进程重启时,才会重启container,也即宿主机重启时,会重启container
-
unless-stopped 同always类似,但是手动停止的container不会在自动重启。
举例docker run -dit --restart unless-stopped redis
如何做端口映射
程序运行在container中。container又被docker deamon管理。所以需要将container中的程序暴露的端口,映射到宿主机自己的指定端口,否则外部程序无法直接同container通信。可以在创建时指定参数-p
来指定。例如:docker run -p 6379:6379/udp -p6379:6379 redis:5.0.4-alpine
其中冒号左边为宿主机的端口,右边为container中程序暴露的端口。斜杠后面指定暴露的端口类型是UDP还是TCP,如果是TCP可以不写。
如何映射文件系统
container中程序可能需要读或写一些数据,要使得这些数据能够被宿主机可见,需要像端口映射一样,将container中的文件路径映射到外部文件系统中。这些外部的文件系统可以是宿主机的文件系统,也可是docker管理的volume。这里以宿主机的文件系统为例
docker run -v /home/v2ray_proxy:/etc/v2ray -p 1081:1081 v2ray/official v2ray -config=/etc/v2ray/config.json
复制代码
将宿主机路径/home/v2ray_proxy映射到container的/etc/v2ray路径,这样宿主机在/home/v2ray_proxy中修改的内容,container可以通过其/etc/v2ray路径获取到。反之亦然。
如何清理所有不使用的container、image、volume、network
可以使用rm命令,删除指定id或name的相关组件。比如:
docker container rm CONTAINER_ID
docker image rm IMAGE_ID
docker volume rm VOLUME_ID
docker network rm NETWOKR_ID
复制代码
可能上述手动挨个删太麻烦,你可以使用prune
命令,直接将符合需求的组件全部删除。比如:
docker image prune#删除未被任何容器使用的image
docker container prune#删除所有未启动的container
docker volume prune#删除所有未被使用的volume
docker network prune#删除所有未被使用的网络
docker system prune#删除所有未被使用的container,image ,volume, network。docker 1.7以上需要显示执行`--volumes`参数,才能一并将volume也删除,之所以这么做是害怕一不小心把数据给删了。多加参数增加了误删数据的门槛
复制代码
以上所有的删除prune命令,都可以基于过滤条件来删除。加参数--filter
即可,比如删除过去24小时未启动的容器
docker container prune --filter "until=24h"
复制代码
如何查看container的资源使用情况
使用命令docker stats
Layer
一个Dockerfile最终会被构建成image,一个image被run后会生成一个container。为了最大化共享存储文件,减少存储空间的浪费,docker引入了层的概念layer. Docerkfile中RUN, COPY, ADD三个命令会产生layer
一个dockerfile中从上下到下的命令,反应到image上是由下到上的层,每一层都是基于上一层进行构建的。layer又分为image layer和container layer,前者是image构建时,每句dockerfile命令对应生成的layer,后者是通过image 生成一个新的container 时,container所独有的read writer 层。
container的read writer layer是container的程序读写文件时,文件的存储的层,它会随着container的销毁而销毁。通常来说,container运行生成或修改文件内容最好不要放到其read write layer,因为不方便cotnainer间共享,又容易影响container本身的读写性能,所以一般通过volume或bind mount的方式,将container读写的文件内容映射挂载到外部。
比如,Dockerfile
FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py
复制代码
- 第一句是基础层,表示基于ubuntu15的image构建
- 第二句在ubuntu15的基础上,将宿主机当前路径的内容拷贝到image的/app路径做为新的layer
- 第三句,使用make命令,将/app中的文件进行编译,生成的内容为新的layer
- 第四句,使用python命令运行上一步build的可执行文件app.py,其对应container中的R/W layer
其对应的image层的示例为:
文件系统
Docker中的任何数据的产生,默认都是存储在了container的write layer,这带来了以下一些问题:
- 不方便备份和访问,因为数据在容器里面
- 数据易丢失,当容器被删除后,数据也跟着被删除
- 不方便程序更新,容器跟数据绑定了,这个时候你想通过更新的image,创建新的容器来达到升级程序的目的变得很难,因为你要丢数据
为了解决这些问题,Docker提出数据更容器分离的理念。以挂载的路径来区分,有以下三种挂载方式
- volume mount 受docker deamon管理的文件系统
- bind mount 当前宿主机的文件系统
- tmpfs mount 内存
Volume mount
创建volume的几种方式
-
直接用volume命令创建例如
docker volume create my-vol
-
在创建一个container时或service时,通过参数
-v
或者--mount
挂载volume时,volume不存在,也会自动创建。举例如下://volume名为myvol2,挂载到container的指定目录为/app $ docker run -d \ --name devtest \ -v myvol2:/app \ nginx:latest //创建四个nginx container组成的service $ docker service create -d \ --replicas=4 \ --name devtest-service \ --mount source=myvol2,target=/app \ nginx:latest 复制代码
-v
和--mount
这两个都能指定挂载的volume(如果不存在,都会创建),创建service时,只能使用mount命令。-v
参数后面直接指定所有的配置value不直观,--mount
的配置,则是以key=value的形式体现,能够清楚的知道指定配置项意义。能通过他们配置的信息有:
- source container外的宿主文件系统(bind mount时,source就是宿主的文件路径)或volume
- destination path: container 内的指定路径
- 读写模式:对挂载的宿主文件路径或volume是否有读写的权利
- driver: 如果挂载到container的是volume时,配置该volume的驱动类型。volume的驱动类型默认是local,也即宿主机所在文件系统。但有些volume对应的存储可能是aws,所以其驱动就不是local.
使用-v
参数的大概形式为:
-v <source>:<destination>
//其中source可以忽略,忽略时,默认创建一个匿名的volume
复制代码
使用--mount
参数的大概形式为:
--mount 'type=volume,src=<VOLUME-NAME>,dst=<CONTAINER-PATH>,volume-driver=local,volume-opt=type=nfs,volume-opt=device=<nfs-server>:<nfs-path>'
//其中src可以写为source
//dst 可以写为destination或target
复制代码
像volume中填充内容 如果一个空的volume挂载到指定的container目录,并且该目录下已经有内容,那么这些内容会自动被复制到volume下。举例如下;
$ docker run -d \
--name=nginxtest \
--mount source=nginx-vol,destination=/usr/share/nginx/html \
nginx:latest
//名为nginx-vol的volume,里面会被拷贝进/usr/share/nginx/html文件
复制代码
bind mount
volume是挂载一个由docker 守护进程管理的文件系统到container。而bind mount是直接挂载宿主机的任意文件路径到container。这样宿主机其他进程该挂载路径下的文件内容,container也会感受到,反之亦然。其挂载命令跟volume差不读,不再赘述,只是其mount的type为bind。
简单总结来看,希望容器间相互共享内容,使用volume挂载到container 希望容器和宿主机之间相互共享内容,使用bind mount
tmpfs mount
tmpfs是将容器指定路径映射到内存,这样当容器对指定路径写数据时,不会写到容器自己的write layer。并且tmpfs不能被容器共享,即A容器mount 的tmpfs,不能被B容器读到,这就使得tmpfs非常适合存储一些易失的,且容器独有的私密信息。
tmpfs只能在linux的docker中使用
tmpfs的挂载也有两种参数方式,一是--tmpfs
,二是--mount
,前者不能指定任何参数,后者则可以,后者的功能和工作范围都比较广。
volume和bind在挂载时,需要指定一个source,而tmpfs的挂载不需要,只用指定挂载到对应contaienr的路径即可。
后话
容器化使得部署应用变得简单方便。docker还提供了swarm,使得服务以集群化形式编排和部署同样变得简单。这里不再详述。
使用容器化提供服务时,需要遵循微服务化的原则,保持服务的原子性,即一个container只提供一种服务。这样更加方便后期管理和程序扩展。