Docker容器

Docker入门简介

2019-04-08  本文已影响2人  西北偏北

Docker是什么

Docker是一种虚拟化技术,类似虚拟机,这使得安装在其中的程序能够只依赖虚拟机的环境,而不受外部操作系统环境的影响。同虚拟机不同的是,Docker的虚拟容器占用空间更小,使得它比虚拟机更容易分发和多实例安装。

Docker容器化技术的整个开发使用方式非常类似java应用开发,这里同java应用开发做一个类比,帮助有过java开发经验的同学快速掌握其中的核心概念

1554642418878.png

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如下:

  1. 先用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)
    
  2. 再编写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"]
    
  3. 构建image
    在宿主机上创建一个文件夹,名字任意,将Dockerfile和app.py 都放置其中(因为Dockerfile中有一个命令COPY . /app1,所以要确保程序跟Dockerfile在同一的路径下,才可以拷贝进去。当然你可以不在一个路径下,那就需要修改Dockerfile命令,将具体app.py的路径写全),然后在该路径下执行构建命令构建image,并将其取名为hellworld

    docker build --tag=helloworld .

  1. 发布image
    你可以像发布jar一样,将image发布到docker中央仓库,或公司的私有仓库,具体方式这里就不展开讨论了。

container

类似于java应用中的jar运行。我们基于image运行后,会创建一个运行的实例,即为container,容器。比如我们可以使用以下命令,通过前面build的image,创建一个container

docker run -p 4000:80 helloworld

network

container需要对外进行通信,可能需要网络服务。有5种网络驱动可供docker配置,用来配置docker的联网行为。

data volumes

container中的程序运行时,可能会产生一些数据,或者需要使用一些数据,甚至希望同其它container共享数据。那么实现这些的方式就是data volumes,它对应docker的存储概念,后续会详细讲解。

docker daemon

类似于Java虚拟机。它负责image构建,分发,获取,执行,以及container、volumes、network等上述核心组件的管理,屏蔽底层操作系统的细节,使得基于docker构建的服务能够跨平台。我们一般通过docker CLI也即docker命令行来向docker deamon发送命令执行上述管理。


1554642870860.png

Docker的基本使用方式

作为普通用户大多数时候,我们只是从中央仓库中获取别人制作好的image,在本地创建container来提供服务,比如获取mysql的image,在本地创建一个mysql的servers。所以下面主要介绍对container的一些核心操作命令。

获取image

使用如下命令去远程仓库中拉取,image文件

 docker pull IMAGE[:TAG]

比如我们想要获取redis的image,在中央仓库中我们可以看到有很多redis的image,他们用不同的tag区分


1554644617846.png

我们可以通过指定tag来拉取特定的image,比如我们拉取tag为5.0.4-alpine的image。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来控制重启行为。重启策略主要有以下几种

举例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

其对应的image层的示例为:

多个container公用image layer的示例:

1554727796358.png

文件系统

Docker中的任何数据的产生,默认都是存储在了container的write layer,这带来了以下一些问题:

为了解决这些问题,Docker提出数据更容器分离的理念。以挂载的路径来区分,有以下三种挂载方式

1554728019701.png

Volume mount

创建volume的几种方式

  1. 直接用volume命令创建例如docker volume create my-vol

  2. 在创建一个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的形式体现,能够清楚的知道指定配置项意义。能通过他们配置的信息有:

使用-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只提供一种服务。这样更加方便后期管理和程序扩展。

参考资料

https://docs.docker.com/

上一篇下一篇

猜你喜欢

热点阅读