Docker进阶

Dockerfile

Dockerfile是由一系列命令和参数构成的脚本,这些命令应用于基础(原始)镜像并最终创建一个自定义的新的镜像。


常用命令

命令作用
FROM image_name:tag定义了使用哪个基础镜像启动构建流程
MAINTAINER user_name声明镜像的创建者
ENV key value设置环境变量 (可以写多条)
RUN command是Dockerfile的核心部分(可以写多条)
ADD source_dir/file dest_dir/file将宿主机的文件复制到容器内,如果是一个压缩文件,将会在复制后自动解压
COPY source_dir/file dest_dir/file和ADD相似,但是如果有压缩文件并不能解压
WORKDIR path_dir设置工作目录
EXPOSE port1 prot2用来指定端口,使容器内的应用可以通过端口和外界交互
CMD argument在构建容器时使用,会被docker run 后的argument覆盖
ENTRYPOINT argument入口点,容器启动后会执行的命令,比如启动mysql。效果和CMD相似,但是并不会被docker run指定的参数覆盖
VOLUME将本地文件夹或者其他容器的文件挂载到容器中

使用Dockerfile创建镜像

目标:创建一个安装有jdk1.8的centos的docker基础镜像。

  1. 首先创建一个目录,用来存放脚本资源等:
mkdir -p ~/dockerjdk8
  1. 下载jdk-8u241-linux-x64.tar.gz并上传到服务器(虚拟机)的该文件夹中,这里放个百度云的链接,提取码:14a1
  2. 创建文件Dockerfile(必须叫这个名字,区分大小写)
    文件内容如下:
#依赖的基础镜像名称和ID,如果本地不存在,则自动下载
FROM centos:7.6.1810
#指定镜像创建者信息
MAINTAINER EXAMPLE_NAME
#切换当前的工作目录,进入容器化,默认进入的目录
WORKDIR /usr
#容器中创建java的目录
RUN mkdir /usr/local/java
#将容器外的文件(相对、绝对路径均可)拷贝到容器内指定的目录,并自动解压
ADD jdk-8u241-linux-x64.tar.gz /usr/local/java/
#容器中配置java环境变量
ENV JAVA_HOME /usr/local/java/jdk1.8.0_241
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH
ENV PATH $JAVA_HOME/bin:$PATH

基础镜像暂无需设置入口点。
这里稍微解释一下入口点:

有些容器启动后会直接在前台运行命令,导致你进入容器后不能做其他的事情。这时你可以使用run命令下的参数--entrypoint,它的作用是覆盖镜像的默认入口点,示例如下:
启动一个以python3.6为基础镜像的容器:

[root@docker ~]# docker run -it --name python3.6 python:3.6
Python 3.6.5 (default, Mar 31 2018, 01:15:58) 
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 
>>> 
>>>

容器启动后,直接进入python的交互终端了,不能做其它操作,而exit后,就直接退出容器了。这里加入--entrypoint bash参数,用bash覆盖它默认的入口点。

[root@docker ~]# docker run -it --entrypoint /bin/bash --name python3.6 python:3.6
root@430e3c9c09ac:/# 
root@430e3c9c09ac:/# 

这样进入容器后,就是bash交互了。

  1. 执行构建命令:
docker build -t com.example/centos-jdk8 .
#或
docker build -t 'com.example/centos-jdk8' .

不要忘记后面的空格和点

  1. 查看镜像是否完成docker images
  2. 创建容器:
#退出容器命令行后,自动会删除该容器
docker run -it --rm com.example/centos-jdk8 bash
#退出容器命令后,容器自动停止但不会被删除
docker run -it --name=mycentosjdk8 com.example/centos-jdk8 /bin/bash

然后可以使用执行javajavacjava -version等命令测试环境。

Docker私有注册中心

私有注册中心搭建

  1. 拉取私有仓库镜像(此步可省略)
docker pull registry:2.7.1
  1. 启动私有仓库容器
docker run -d -p 5000:5000 --restart always --name myregistry registry:2.7.1

restart参数:容器是否自动重启的策略。

Docker容器的重启策略是面向生产环境的一个启动策略,在开发过程中可以忽略该策略。Docker容器的重启都是由Docker守护进程完成的,因此与守护进程息息相关。

Docker容器的重启策略如下:

  • no,默认策略,在容器退出时不重启容器
  • on-failure,在容器非正常退出时(退出状态非0),才会重启容器
  • on-failure:3,在容器非正常退出时重启容器,最多重启3次
  • always,在容器退出时总是重启容器
  • unless-stopped,在容器退出时总是重启容器,但是不考虑在Docker守护进程启动时就已经停止了的容器
  1. 查看检验是否安装启动成功,打开一下网址:
//IP为虚拟机(服务器地址)
http://192.168.48.130:5000/v2/_catalog

看到{"repositories":[]} 即代表仓库搭建成功,并且内容为空。

上传镜像至私有中心

  1. 修改daemon.json,让docker信任私有仓库地址
{
"registry-mirrors": ["https://docker.mirrors.ustc.edu.cn"]
,"insecure-registries":["192.168.48.130:5000"]
}

注意此文件如果有错误,会导致docker服务无法启动,并报如下错误:

Job for docker.service failed because the control process exited with error code. See "systemctl status docker.service" and "journalctl -xe" for details.
  1. 重启docker服务和私服容器:
systemctl restart docker
#如果创建容器时带有--restart always,下面这句可以省略
docker start registry
  1. 修改镜像的相关信息,重打标记
#创建标记(tag)此镜像为私有仓库的镜像
docker tag com.example/centos-jdk8 192.168.48.130:5000/centos-jdk8 

打标记从某种意义上说就是给一个镜像再加一个名字。

重启私服容器:

docker start registry
  1. 上传镜像:
docker push 192.168.48.130:5000/centos-jdk8

然后访问镜像列表(http://192.168.48.130:5000/v2/_catalog)即可看到该镜像:

{
    "repositories": [
        "centos-jdk8"
    ]
}

名称空间默认是仓库的名字,如192.168.48.130:5000/centos-jdk8,则默认在http://192.168.48.130:5000/v2中寻找centos-jdk8镜像。
example.com/centos-jdk8,则默认在https://example.com:5000/v2中寻找centos-jdk8镜像。

Docker数据持久化

Docker数据持久化主要有两种方式:

  • bind mount
  • volume

Docker的数据持久化即使数据不随着container的结束而结束,数据存在于host机器上——要么存在于host的某个指定目录中(使用bind mount),要么使用docker自己管理的volume(/var/lib/docker/volumes下)。

bind mount

该方式在docker早期就开始使用,用于将宿主机目录挂载到容器中。

例如:将宿主机上当前目录下的host-data目录挂载到容器中的/container-data目录:

#将宿主机的host-data目录挂载为容器的container-data目录
docker run -it -v $(pwd)/host-data:/container-data alpine sh

注意:

  • host机器的目录路径必须为全路径(准确的说需要以/或~/开始的路径),否则docker会将路径作为Volume处理
  • 如果host机器上的目录不存在,docker会自动创建该目录
  • 如果container中的目录不存在,docker会自动创建该目录
  • 如果container中的目录已经有内容,那么docker会使用host上的目录将其覆盖掉

使用挂载可以很方便的在宿主机与容器之间进行文件交换,但是bind mount在不同的宿主机系统时不可移植的,比如Windows和Linux的目录结构是不一样的,bind mount所指向的host目录也不能一样。这也是为什么bind mount不能出现在Dockerfile中的原因,因为这样Dockerfile就不可移植了。

volume

Docker镜像是由多个文件系统(只读层)叠加而成。当我们启动一个容器的时候,Docker会加载只读镜像层并在其上(译者注:镜像栈顶部)添加一个读写层。如果运行中的容器修改了现有的一个已经存在的文件,那该文件将会从读写层下面的只读层复制到读写层,该文件的只读版本仍然存在,只是已经被读写层中该文件的副本所隐藏。当删除Docker容器,并通过该镜像重新启动时,之前的更改将会丢失。在Docker中,只读层及在顶部的读写层的组合被称为Union File System(联合文件系统)。

因此在构建Image的时候操作行数越少,镜像越小

为了能够保存(持久化)数据以及共享容器间的数据,Docker提出了Volume的概念。简单来说,Volume就是目录或者文件,它可以绕过默认的联合文件系统,而以正常的文件或者目录的形式存在于宿主机上。

可以通过两种方式来初始化Volume:docker run -vdocker volume create

volume也是绕过container的文件系统,直接将数据写到host机器上,只是volume是被docker管理的,docker下所有的volume都在host机器上的指定目录下/var/lib/docker/volumes

将myVolume挂载到容器/myData目录:

docker run -it -v myVolume:/myData alpine sh

这里的myVolume指的是volume的名字,在/var/lib/docker/volumes/固定位置。

# 构析容器
docker inspect 容器名字
# 构析volume
docker volume inspect volume名称
[
    {
        "CreatedAt": "2020-03-10T22:13:24+08:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/myVolume/_data",
        "Name": "myVolume",
        "Options": null,
        "Scope": "local"
    }
]

可以看到"Mountpoint": "/var/lib/docker/volumes/myVolume/_data",即文件就在这个文件夹,目录不存在时docker会自动创建。

也可以使用docker volume create myVolume2手动创建volume。

需要注意的是,与bind mount不同的是,如果volume是空的而container中的目录有内容,那么docker会将container目录中的内容拷贝到volume中,但是如果volume中已经有内容,则会将container中的目录覆盖。

Dockerfile中的volume

在Dockerfile中,我们也可以使用VOLUME指令来申明contaienr中的某个目录需要映射到某个volume:

#Dockerfile
VOLUME /foo

这表示,在docker运行时,docker会创建一个匿名的volume,并将此volume绑定到container的/foo目录中,如果container的/foo目录下已经有内容,则会将内容拷贝的volume中。即,Dockerfile中的VOLUME /foodocker run -v /foo alpine的效果一样。

Dockerfile中的VOLUME使每次运行一个新的container时,都会为其自动创建一个匿名的volume,如果需要在不同container之间共享数据,那么我们依然需要通过docker run -it -v my-volume:/foo的方式将/foo中数据存放于指定的my-volume中。

因此,VOLUME /foo在某些时候会产生歧义,如果不了解的话将导致问题。

删除volume

一般有以下几种情况删除卷:

  • 默认情况下docker rm my_container,删除容器不会删除卷,即不会影响相关的数据。在删除完容器后,需要再手动删除卷,命令为docker volume rm 卷名。如果容器正在使用该volume,则无法删除,提示卷正在使用,必须先删除容器,才而已删除卷。
  • 如果需要删除容器的时候,同时将卷删除,则可以使用-v参数,如:docker rm -v 容器。但要注意,“bind-mount” 类型的Volume不会被删除。一个“正常”的Volume,Docker会自动将指定Volume路径(如/some/path`)上的数据复制到由Docker创建的新的目录下,如果是“bind-mount”,Volume就不会这样做。
  • 如果是临时容器,在创建启动容器的时候,使用--rm参数,当容器退出销毁的时候,卷也会自动被删除。

不管哪种情况,只能删除没有容器连接的Volume。连接到用户指定主机目录的Volume永远不会被docker删除。

如果不删除Volume,那么/var/lib/docker/volumes目录下得到一些僵尸文件和目录,并且还不容易说出它们到底代表什么。

即想要删除volume,需要先删除链接的容器(即使是停止的容器),然后再删除volume。

volume共享

容器之间可以共享volume:

docker run --name my_container -v /some/path ...
docker run --volumes-from my_container --name my_container2 ...

上面的命令将告诉Docker从第一个容器挂载相同的Volume到第二个容器,它可以在两个容器之间共享数据。

如果你执行docker rm -v my_container命令,而上方的第二容器依然存在,那Volume不会被删除,如果你不使用docker rm -v my_container2命令删除第二个容器,那它会一直存在。

示例:

#第一个容器
docker run -it --rm -v /mydata --name=mycontainner01 bash:5.0.11 sh
#第二个容器相当于继承了上一个容器的所有的映射
docker run -it --rm --volumes-from mycontainner01 --name=mycontainner02 bash:5.0.11 sh

Docker Compose

Compose是用于定义和运行多容器Docker应用程序的工具。使用compose,可以使用yaml文件配置应用程序的服务。然后,通过一个命令,可以从配置中创建和启动所有服务。

下载地址为https://github.com/docker/compose/releases

该页面有安装命令:

curl -L https://github.com/docker/compose/releases/download/1.25.4/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

然后可以通过查看版本是否安装成功:

docker-compose --version
docker-compose version

显示版本即为成功

对于Mac和windows用户来说,Docker for Mac 、Docker for Windows 和 Docker Toolbox 早已经集成了docker-compose,所以用户不需要分别再安装docker-compose了。

卸载docker compose直接删除二进制文件即可:sudo rm /usr/local/bin/docker-compose

使用Docker Compose构建一个简单的web程序

首先创建一个composetest目录:

mkdir composetest
cd composetest

然后创建一个docker-compose.yml文件:

touch docker-compose.yml
vim docker-compose.yml

文件中写入一下内容:

#Compose文件的格式版本,需要和docker版本对应,这里使用3的系列版本
version: '3'
#定义若干的服务,每个服务就是一个容器实例,其子元素可以有N个,代表N个服务。
services:
  #服务(实例)的名字,随意
  mywebservice:
    #指定为镜像名称或镜像ID。如果镜像在本地不存在,Compose将会尝试拉取这个镜像
    image: nginx:1.17.2
    #暴露端口信息
    ports: 
      #宿主机和容器的端口映射
      - "8888:80"
    #指定容器名称。默认将会使用项目名称_服务名称_序号这样的格式。随意
    container_name: "mynginx"
#volumes:

#networks:

然后使用docker-compose up来启动服务:

[root@localhost]# docker-compose up
Starting mynginx ... done
Attaching to mynginx

服务便以交互式启动。启动时创建了一个虚拟网卡,然后创建了一个容器,接着启动。

如果想要守护式启动,加上-d参数即可。

可以使用docker-compose ps来查看compose服务。
使用docker-compose exec 服务名 shell来进入服务。

比如上面可以执行docker-compose exec mywebservice bash来进入服务的容器内部。

停止服务:docker-compose stop 服务名
启动服务:docker-compose start 服务名
删除服务:docker-compose rm 服务名,服务必须停止之后才能删除。

如果同时需要删除服务及其网络、数据卷,可以使用docker-compose down命令,会自动停止服务,并删除容器、网络、数据卷。

划分网络

我们可以同时创建多个服务,这些服务之间的网络可以是联通的,也可以是不联通的。要实现这个需求,我们可以对网络进行划分。

目标:一次性创建3个Nginx服务,并且通过划分网络,来实现服务之间的互通。

创建一个子文件夹,并创建docker-compose.yml文件,内容为:

#Compose文件的格式版本,需要和docker版本对应,这里使用3的系列版本
version: '3'
#定义若干的服务,每个服务就是一个容器实例,其子元素可以有N个,代表N个服务。
services:
  #服务(实例)的名字,随意
  mywebservice1:
    #指定为镜像名称或镜像ID。如果镜像在本地不存在,Compose将会尝试拉取这个镜像
    image: nginx:1.17.2
    #暴露端口信息
    ports: 
      #宿主机和容器的端口映射
      - "8001:80"
    #指定容器名称。默认将会使用项目名称_服务名称_序号这样的格式。随意
    container_name: "mynginx1"
    # 指定使用什么网络
    networks:
      - dev
    #卷映射挂载
    volumes:
      - myntfs:/usr/share/nginx/html
  mywebservice2:
    image: nginx:1.17.2
    ports: 
      - "8002:80"
    container_name: "mynginx2"
    networks:
      - dev
      - prod
  mywebservice3:
#   这里可以使用build来构建Dockerfile镜像
    build: .
    ports: 
      - "8003:80"
    container_name: "mynginx3"
    networks:
      - prod

#网络配置定义
networks:
  #定义开发环境下使用的网络方式为桥接
  dev:
    driver: bridge
  #定义生产环境下使用的网络方式为桥接
  prod:
    driver: bridge

#卷配置定义
volumes:
  #定义ntfs的驱动
  myntfs:
    driver: local

项目部署

手动部署

一般情况下都是打jar包(或者war包)来进行部署,然后在服务器上使用java -jar jar包名来运行,不过也可以使用Docker来进行快速部署。

使用Docker部署

手动Docker

首先检查Java环境、MySQL,如果用到了Redis也需要配置。
在Idea中maven的lifeCycle执行clean、package打包。

创建一个Dockerfile,写入以下内容:

#依赖的基础镜像名称和ID,如果本地不存在,则自动下载
FROM 192.168.40.141:5000/centos-jdk8
#指定镜像创建者信息
MAINTAINER Bronya
#切换当前的工作目录,进入容器化,默认进入的目录
WORKDIR /
#将容器外的文件(相对、绝对路径均可)拷贝到容器内指定的目录,并自动解压
COPY app.jar /
#容器的入口命令,容器一启动就执行
ENTRYPOINT ["java", "-jar", "/app.jar"]

然后执行构建:

docker build -t example.com/base .

生成镜像之后启动容器就可以了。

Docker Maven插件半自动部署

  1. 修改宿主机的docker配置,让其可以远程访问(开启docker允许远程创建的功能)
vim /lib/systemd/system/docker.service

找到ExecStart=/usr/bin/dockerd的行,在其后面添加如下内容:

-H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

修改后的为:

ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

增加了上述内容后,就允许任意ip(0.0.0.0),通过2375的端口(maven插件访问的端口),远程的来执行docker的命令。


对于老的docker版本,这么修改

开启docker远程执行的权限(旧版本)

vi /etc/sysconfig/docker-network

找到DOCKER_NETWORK_OPTIONS=的行,在其后面添加如下内容:

-H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

修改后结果:

DOCKER_NETWORK_OPTIONS=-H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

然后重新加载配置并重启服务:

systemctl daemon-reload && systemctl restart docker

此时便可以在http://192.168.48.130:2375/info查看信息。

如果客户端安装了docker可以执行docker -H tcp://192.168.48.130:2375 images来查看镜像信息

  1. 修改项目的pom文件
<build>
    <!--打包后的项目的文件名称-->
    <finalName>app</finalName>
    <!--<finalName>${project.artifactId}</finalName>-->
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
        <!-- docker的maven插件,官网:https://github.com/spotify/docker-maven-plugin -->
        <plugin>
            <groupId>com.spotify</groupId>
            <artifactId>docker-maven-plugin</artifactId>
            <version>1.1.1</version>
            <configuration>
                <!--生成的镜像名字-->
                <imageName>192.168.48.130:5000/${project.artifactId}:${project.version}</imageName>
                <!--基础镜像名称,相当于dockerfile中的FROM centos-jdk8 -->
                <baseImage>192.168.48.130:5000/centos-jdk8</baseImage>
                <!--入口点-->
                <entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]</entryPoint>
                <!-- copy the service's jar file from target into the root directory of the image
                要将生成的微服务的jar资源拷贝到哪里,这里配置的是容器的根目录下面,相当于ADD /app.jar /-->
                <resources>
                    <resource>
                        <targetPath>/</targetPath>
                        <directory>${project.build.directory}</directory>
                        <include>${project.build.finalName}.jar</include>
                    </resource>
                </resources>
                <!--设置docker的服务地址,默认连接的docker主机为localhost:2375-->
                <dockerHost>http://192.168.48.130:2375</dockerHost>
            </configuration>
        </plugin>
    </plugins>
</build>

以上配置会自动生成Dockerfile,内容如下:

FROM 192.168.48.130:5000/centos-jdk8
WORKDIR /
ADD app.jar /
ENTRYPOINT ["java","-jar","/app.jar"]

在项目pom所在目录,cmd执行:

mvn clean package -Dmaven.test.skip=true docker:build
  • clean:清理target。
  • package -Dmaven.test.skip=true:打包并跳过测试。
  • docker:build -DpushImage:基于dockerfile创建镜像,并上传镜像到私服中。如果暂时没有私服,则去掉-DpushImage这个参数。

然后便可以在宿主机创建该镜像的容器并运行。

标签: Docker

添加新评论