Docker 高级——Dockerfile和Docker Compose

675 阅读11分钟

在开始docker的终极介绍之前,大家可以去看看之前的进阶高级篇,对docker的使用有个整体的把握。

Dockerfile是什么

通常xxfile为描述文件,比如Makefile、Jenkinsfile等等。说法可能不准确,但是好理解。所谓描述,就是对一个xx过程的详细记录,不同的file使用的语法和语言可能不同,但是逻辑都是相通的,通过它能确定最终的成果。
Dockerfile也是一个描述文件,它描述了一个image的构建过程。什么是image?看到这篇文章,想必你已经知道了docker的三大组件:容器、镜像、仓库。想必你也运行过docker容器,感受到了容器实体的概念,但是镜像是什么,诸位可能还真不一定说的明白,在开始Dockerfile的介绍之前,我再通俗的说一下docker核心组件镜像--image。

image

“镜像是只读模板,其中包含创建Docker容器的说明。通常,一个镜像基于另一个镜像,并进行一些额外的自定义。”
这是docker官网关于image的描述,很抽象。但是从中我们可以提取两个关键信息:只读模板,镜像基于镜像。什么是模板?俗话说依葫芦画瓢,这里的葫芦就是一个模板。大家做ppt,都得在网上找一个模板吧!只读模板就像是字帖上的字,用印刷机印出来的,改不了,只能隔着一层宣纸去描摹。隔着宣纸你把“天”描摹成“地”都没有任何问题,最后通过宣纸看到的就是你所描摹的。你可以这么认为,字帖的原字就是image,你的描摹就是容器。如果你突发奇想,艺术就是爆炸,你又在你描摹的字体上面放上一层宣纸,再次描摹。你的字体这个时候就变成了image,这也就是上面所说的“通常,一个镜像基于另一个镜像”。
在linux中,万物皆文件。据我的调研,docker中的image就是一套只读的文件系统。每次通过底层镜像构建新的镜像时,有改动的文件会被记录出来。在运行容器的时候,通过层叠的文件系统的合并挂载,最终的镜像改动的文件会覆盖底层镜像的文件。而容器,使用的正是最终合并挂载的文件系统,在这套只读文件系统之上加入一个可写层,所以容器内部能对文件系统进行读写。想更深入研究的的同学可以去了解overlay2文件系统。

使用Dockerfile构建镜像

言归正传,Dockerfile是一个描述image构建过程的文件,使用一套指令和简单语法构成。先看一个简单的Dockerfile文件,以及构建效果,感受一下。busybox是一个集成了很多常用的linux命令的工具包,体积小,常用来测试。注意文件名必须命名为Dockerfile,大小写也得写对。

FROM busybox
WORKDIR /
RUN touch test.txt
ENTRYPOINT ["sh","-c","while true;do echo hello world>>test.txt;sleep 5;done;"]
# 构建镜像 -t 后面指定镜像名和tag版本,最后面的"."指定Dockerfile的路径
docker build -t busybox:v1 .

微信截图_20210909172028.png

22.png

# 运行容器
docker run -d --name busybox busybox:v1

# 进入容器
docker exec -it busybox sh

12.png 以上是使用Dockerfile自定义镜像,并运行容器的全部过程。理论上,几乎所有的应用和程序都能使用docker运行。得益于docker跨平台的优点,只需要一个镜像,就能在任何机器任何系统上跑起来,达到统一的运行环境。重点就是如何使用Dockerfile构建自定义镜像。

Dockerfile指令

通过上面那个例子,能很直观的感受到Dockerfile可以构建一个镜像。同时我们看到,Dockerfile文件每行都是由一个大写的指令加上参数构成。Dockerfile正是由指令加上简单语法所构成,每个指令都会被构建成一个只读的镜像中间层。每个指令都是大写,标记某个具体的功能,接下来我介绍一下Dockerfile的常用指令。 d4a07244458c9ec83610bff572145b82.jpeg FROM 其后指定基础镜像。我们知道镜像可以基于镜像,这里指定一个镜像,后续操作都在这个镜像的基础上完成。
MAINTAINER 其后指定维护者的信息。MAINTAINER zhangsan< xxx@xxx.com >,写上用户名和邮箱,没什么卵用。
WORKDIR 其后声明的工作目录。后续所有操作和相对路径都是在此目录下完成,运行的容器进入容器时,也是进入了此目录。
ENV 其后定义环境变量。ENV VERSION 1.0.0
RUN 其后运行shell命令。非常有用,常用来下载一些软件,执行shell脚本。通常用反斜杠\来换行,把多个shell命令通过&&连接成一行命令执行。减少RUN指令出现的次数,也就能减少镜像的中间层数。
ADD 将宿主机目录的文件复制到镜像中。ADD sourceDir/test.txt targetDir/,如果文件是以tar、gz结尾的压缩包,还会被自动解压。
VOLUME 其后声明数据卷。后面跟一个目录,运行容器时如果没有指定卷,默认创建一个匿名卷。
EXPOSE 其后声明端口。后面跟一个端口号,表示程序使用的端口,运行容器的时候可以设置宿主机的端口映射。
ENTRYPOINT 其后声明容器启动命令。一个容器必须要有至少一个运行的进程,不然就会自动停止。当ENTRYPOINT存在时,它就是容器的入口命令,一般用来做启动命令。
CMD 其后声明运行时命令或者参数。CMD ["sh","xx.sh"],推荐参数写成字符串数组,当然也有其他写法,可以自己去研究。当存在ENTRYPOINT时,CMD后面的内容会被当成参数追加在ENTRYPOINT原来的参数后面。此外,CMD 命令会被docker run运行容器时最后面传的启动命令和参数所覆盖。所以CMD一般用来当默认的命令或者启动参数。

以上就是常用的一些Dockerfile指令和使用方法。Dockerfile就像是六脉神剑,有了它,你可以轻松构建几乎任何应用程序的镜像。结合这些指令再去看一眼上面busybox的例子,是不是很简单呢?

使用Dockerfile构建SpringBoot应用

将springboot程序打成xxx.jar包,上传到某个目录。并在该目录创建Dockerfile文件,复制如下语句。

FROM frolvlad/alpine-java
COPY xxx.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]

执行 docker build -t xxx:tag . 开始构建。当然,这里只是其中一种构建方式,你也可以将springboot程序打成war包,用tomcat进行部署,道理也是一样的。

Docker Compose

介绍完Dockerfile,接下来讲一下终极武器Docker compose

aebdad41a0f34d84f29065496857c17c.jpeg

Docker compose是docker官方提供的一个容器编排工具。容器编排是什么?有什么用?
一般我们都是通过命令去运行容器,但是命令本身有个很大的弱点,没有记忆,它是一次性的。此外,docker命令还有个致命问题,当你运行的容器变得很多时,通过命令去管理所有容器的网络,启动顺序等都是一个无法想象的工作量。使用纯docker命令,无法一次性管理所有的容器,设置容器的资源属性,卷、网络等。
docker compose的出现正是为了解决这种问题。它通过一个docker-compose.yaml文件管理所有的容器,对容器进行一个编排,依赖配置,资源配置,网络配置,卷配置等。它是单机docker部署的优雅解决方案,通过它能轻轻松松部署一套微服务,更新、修改等一键操作。

使用Docker Compose编排容器

  1. 安装docker-compose
# 下载docker-compose二进制文件
curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

# 添加执行权限
chmod +x /usr/local/bin/docker-compose

# 我习惯给docker-compose建立软连接,命令短点方便很多docker-compose -> doc
ln -s /usr/local/bin/docker-compose /usr/local/bin/doc

# 检查安装是否成功
doc -version
  1. 编写docker-compose.yaml文件,进行容器编排测试
version: '3'
services:
  busybox1:
    image: busybox  #指定镜像加版本,不指定就是默认的latest。
    # 这里的image可以换成build,然后指定Dockerfile的路径,docker-compose会自动构建镜像并使用
    container_name: busybox1   #指定创建后的容器名
    volumes:
      - busybox_data:/data     #指定数据卷,前边为卷名,后面为容器内目录。bind mount方式写法也支持
    command: ["sh","-c","while true;do echo hello world>>/data/test.txt;sleep 5;done;"] #运行容器时的执行命令
    networks:
      - test-net  #指定网络
  busybox2:
    image: busybox
    container_name: busybox2
    volumes:
      - busybox_data:/data
    command: ["tail","-f","/data/test.txt"]
    depends_on:
      - busybox1   #指定容器依赖,指定的依赖会先启动,只有当busybox1运行起来之后,才有test.txt文件。
    networks:
      - test-net
volumes:
  busybox_data:        # 声明数据卷
    name: busybox_data    # 数据卷命名
networks:
  test-net:            #声明网络
    name: test-net     #网络命名,默认桥接类型
  1. 在docker-compose.yaml文件所在目录运行docker-compose命令,创建文件中配置的容器。
# 后台运行的方式创建容器,还有很多命令,可以自行研究。
doc up -d

# 如果不在同一目录,也可指定文件运行
doc -f xxx/xxx.yaml up -d

微信截图_20210910114157.png
一个简单的docker compose例子就测试完成了。并且实现了两个容器的数据共享,网络连接通信。docker-compose.yaml使用yaml的语法,将docker命令写进配置文件。

简单说一下docker compose是如何对容器进行编排的。

  • docker compose执行命令时,如果没有通过-f指定文件,默认会使用当前目录中的docker-compose.yaml,文件名要写完整,小写。
  • docker compose会解析文件中的每个信息,用来启动容器。文件的写法非常固定,这里我第一层中写了四个关键信息。
version: 声明docker-compose.yaml版本,2.x和3.x有很多写地方不同。
services: 声明docker容器。
volumes: 声明数据卷,docker-compose会在启动的时候自动创建。
networks:声明网络,docker-compose会在启动时候自动创建,并将所有容器加入进该网络。
如果不显示指定网络,默认情况下,docker-compose会创建一个以 当前目录名_default 的桥接网络。所有容器都会加入进这个网络,彼此之间可以通过容器名进行访问。
  • 在services层中,定义了两个容器。busybox1和busybox2,并配置了各种属性,详见上面文件中的注释。所有的容器和配置都定义在services层,通过yaml文件管理,可以很方便地就行修改和卷挂载,并使用统一网络进行互相通信。
  • 更多详细的参数信息可以查看官方文档,用法写的非常详细。

简单介绍一下docker-compose命令

# 后台启动,有配置更新会删掉旧容器,创建新容器
docker-compose up -d

# 关闭并删除容器和网络等资源,卷不会被删除
docker-compose down

# docker-compose命令查看。其他的功能自行研究吧,跟docker原生命令大同小异
docker-compose -h

# 上面的演示中,我直接使用的是docker-comopose的软连接doc,你可以自己创建顺手的软连接。

以上就是docker compose一键启动的整个过程,依葫芦画瓢,赶快动手试试吧!

一些常用应用

附上我测试环境的docker-compose.yaml,涵盖java开发的常用应用,同鞋们请低调使用cv。

version: '3'
services:
  mysql:
    image: mysql:5.7
    container_name: mysql
    ports:
      - "3306:3306"
    command: --lower_case_table_names=1
    volumes:
      - $PWD/mysql_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: 123456
    restart: always
    networks:
      - my-net
      
  redis:
    image: redis
    container_name: redis
    ports:
      - "6379:6379"
    volumes:
      - $PWD/redis_data:/data
    command: --appendonly yes
    restart: always
    networks:
      - my-net

  mongo:
    image: mongo
    container_name: mongo
    ports:
      - "27017:27017"
    volumes:
      - $PWD/mongo_data:/data/db
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: 123456
    restart: always
    networks:
      - my-net  
  
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.13.2
    container_name: elasticsearch
    environment:
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - "discovery.type=single-node"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    privileged: true
    volumes:
      - $PWD/elasticsearch-data:/usr/share/elasticsearch/data
      - $PWD/elasticsearch-plugins:/usr/share/elasticsearch/plugins
    ports:
      - "9200:9200"
    networks:
      - my-net

  kibana:
    image: docker.elastic.co/kibana/kibana:7.13.2
    container_name: kibana
    ports:
      - "5601:5601"
    environment:
      - "ELASTICSEARCH_HOSTS=http://elasticsearch:9200"
    networks:
      - my-net

  zookeeper:
    image: wurstmeister/zookeeper
    container_name: zookeeper
    ports: 
      - "2181:2181"
    networks:
      - my-net

  kafka:
    image: wurstmeister/kafka
    container_name: kafka
    ports:
      - "9092:9092"
    environment:
      KAFKA_ADVERTISED_HOST_NAME: 192.168.1.234
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
    depends_on: 
      - zookeeper
    networks:
      - my-net

  # ip代理池 https://github.com/jhao104/proxy_pool
  proxy_pool:
    image: jhao104/proxy_pool
    container_name: proxy_pool
    ports:
      - "5010:5010"
    environment:
      DB_CONN: redis://redis:6379/0
    depends_on:
      - redis
    networks:
      - my-net

networks:
  my-net:
    name: my-net

注意,使用elasticsearch的童鞋,需要将elasticsearch-data目录的权限修改为777,chmod 777 elasticsearch-data

总结

终于写完了Dockerfile和Docker Compose,也是docker系列的最后一篇,如释重负。相信通过以上的介绍,能给童鞋们带来一点帮助。Docker Compose适合在单机进行操作,往往和Dockerfile结合使用,部署自己的一套微服务。至于集群,企业中使用的K8s底层也是docker容器,自定义程序也需要Dockerfile来构建镜像。企业常常会搭建自己的一整套环境,git仓库,maven仓库,docker镜像仓库,甚至helm仓库。后续我会k8s和一些常用仓库的搭建。