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基础镜像。
- 首先创建一个目录,用来存放脚本资源等:
mkdir -p ~/dockerjdk8
-
下载jdk-8u241-linux-x64.tar.gz并上传到服务器(虚拟机)的该文件夹中,这里放个百度云的链接,提取码:14a1
-
创建文件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交互了。
- 执行构建命令:
docker build -t com.example/centos-jdk8 .
#或
docker build -t 'com.example/centos-jdk8' .
不要忘记后面的空格和点
- 查看镜像是否完成
docker images
- 创建容器:
#退出容器命令行后,自动会删除该容器
docker run -it --rm com.example/centos-jdk8 bash
#退出容器命令后,容器自动停止但不会被删除
docker run -it --name=mycentosjdk8 com.example/centos-jdk8 /bin/bash
然后可以使用执行java
、javac
、java -version
等命令测试环境。
Docker私有注册中心
私有注册中心搭建
- 拉取私有仓库镜像(此步可省略)
docker pull registry:2.7.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守护进程启动时就已经停止了的容器
- 查看检验是否安装启动成功,打开一下网址:
//IP为虚拟机(服务器地址)
http://192.168.48.130:5000/v2/_catalog
看到{"repositories":[]}
即代表仓库搭建成功,并且内容为空。
上传镜像至私有中心
- 修改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.
- 重启docker服务和私服容器:
systemctl restart docker
#如果创建容器时带有--restart always,下面这句可以省略
docker start registry
- 修改镜像的相关信息,重打标记
#创建标记(tag)此镜像为私有仓库的镜像
docker tag com.example/centos-jdk8 192.168.48.130:5000/centos-jdk8
打标记从某种意义上说就是给一个镜像再加一个名字。
重启私服容器:
docker start registry
- 上传镜像:
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 -v
和docker 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 /foo
与docker 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插件半自动部署
- 修改宿主机的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
来查看镜像信息
- 修改项目的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这个参数。
然后便可以在宿主机创建该镜像的容器并运行。