Docker

容器技术的介绍

注意我们这里所说的容器container是指的一种技术,而Docker只是一个容器技术的实现,或者说让容器技术普及开来的最成功的实现

容器正在引领基础架构的一场新的革命

  • 90年代的PC
  • 00年代的虚拟化
  • 10年代的cloud
  • 11年代的container

什么是container(容器)?

容器是一种快速的打包技术

Package Software into Standardized Units for Development, Shipment and Deployment

  • 标准化
  • 轻量级
  • 易移植

为什么容器技术会出现?

容器技术出现之前

Docker.assets/why_container_1.png

容器技术出现之后

Docker.assets/why_container_2.png

容器 vs 虚拟机

Docker.assets/container_vs_vm.png

Linux Container容器技术的诞生于2008年(Docker诞生于2013年),解决了IT世界里“集装箱运输”的问题。Linux Container(简称LXC)它是一种内核轻量级的操作系统层虚拟化技术。Linux Container主要由Namespace 和Cgroups 两大机制来保证实现

  • Namespace命名空间主要用于资源的隔离(诞生于2002年)
  • Cgroups(Control Groups)就负责资源管理控制作用,比如进程组使用CPU/MEM的限制,进程组的优先级控制,进程组的挂起和恢复等等。(由Google贡献,2008年合并到了Linux Kernel)

容器的标准化

1
docker != container

在2015年,由Google,Docker、红帽等厂商联合发起了OCI(Open Container Initiative)组织,致力于容器技术的标准化

容器运行时标准 (runtime spec)

简单来讲就是规定了容器的基本操作规范,比如如何下载镜像,创建容器,启动容器等。

容器镜像标准(image spec)

主要定义镜像的基本格式。

容器是关乎“速度”

  • 容器会加速你的软件开发
  • 容器会加速你的程序编译和构建
  • 容器会加速你的测试
  • 容器会速度你的部署
  • 容器会加速你的更新
  • 容器会速度你的故障恢复

容器的快速发展和普及

到2020年,全球超过50%的公司将在生产环境中使用container —— Gartner

Docker.assets/dockerhub-2020.png

在 Linux 系统上安装 Docker

使用脚本快速安装

1
2
3
4
5
6
7
8
#下载安装脚本到get-dcoker.sh,version为可选
curl -fsSL get.docker.com -o get-docker.sh
sh get-docker.sh --version <VERSION>
#或者快速安装最新版本
curl -fsSL get.docker.com|sh
#确认是否安装成功
sudo systemctl start docker
sudo docker version
Docker.assets/image-20230613194139663.png

2.容器的快速上手

docker命令行的认识

docker + 管理的对象(比如容器,镜像) + 具体操作(比如创建,启动,停止,删除)

例如

  • docker image pull nginx 拉取一个叫nginx的docker image镜像
  • docker container stop web 停止一个叫web的docker container容器
1
2
3
4
5
6
7
8
docker version 或docker info #查看版本
docker help #查看docker的命令
docker container --help
docker container ps #当前运行的容器
docker container ps -a #当前所有的容器
docker container ls #与ps效果一样,ps是老版的命令
docker image ls
docker image rm

理解Image vs Container 镜像 vs 容器

image镜像

  • Docker image是一个 read-only 文件
  • 这个文件包含文件系统,源码,库文件,依赖,工具等一些运行application所需要的文件
  • 可以理解成一个模板
  • docker image具有分层的概念

container容器

  • “一个运行中的docker image”
  • 实质是复制image并在image最上层加上一层 read-write 的层 (称之为 container layer ,容器层)
  • 基于同一个image可以创建多个container
Docker.assets/container-layers.png

docker image的获取

  • 自己制作
  • 从registry拉取(比如docker hub)

配置国内镜像仓库加速拉取,跟maven配置镜像源一个道理

  • 为docker配置国内镜像地址,用于在pull镜像下载加速

    1. 创建配置文件daemon.json

    在目录/etc/docker/daemon.json下,如果没,则创建该文件

    1. 按如下格式化添加镜像地址
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "registry-mirrors": [
    "https://registry.docker-cn.com",
    "http://hub-mirror.c.163.com",
    "https://docker.mirrors.ustc.edu.cn"
    ]
    }
    #docker中国镜像地址: "https://registry.docker-cn.com",
    #网络docker镜像地址 "http://hub-mirror.c.163.com",
    #ustc大学镜像地址 "https://docker.mirrors.ustc.edu.cn"
    1. 添加完重启docker,并使用docker info 命令查看
    1
    2
    3
    sudo systemctl daemon-reload
    sudo systemctl restart docker
    sudo docker info

    docker信息:

Docker.assets/image-20230613203418772.png

容器的基本操作

Docker.assets/image-20230704213534149.png Docker.assets/image-20230613212427849.png Docker.assets/image-20230613212507558.png

docker container 命令小技巧

  • 批量停止

1
2
3
4
5
6
$ docker container ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cd3a825fedeb nginx "/docker-entrypoint.…" 7 seconds ago Up 6 seconds 80/tcp mystifying_leakey
269494fe89fa nginx "/docker-entrypoint.…" 9 seconds ago Up 8 seconds 80/tcp funny_gauss
34b68af9deef nginx "/docker-entrypoint.…" 12 seconds ago Up 10 seconds 80/tcp interesting_mahavira
7513949674fc nginx "/docker-entrypoint.…" 13 seconds ago Up 12 seconds 80/tcp kind_nobel

方法1

1
$ docker container stop cd3 269 34b 751

方法2

1
2
3
4
5
6
7
8
9
10
11
12
$ docker container stop $(docker container ps -aq)
cd3a825fedeb
269494fe89fa
34b68af9deef
7513949674fc
$ docker container ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cd3a825fedeb nginx "/docker-entrypoint.…" 30 seconds ago Exited (0) 2 seconds ago mystifying_leakey
269494fe89fa nginx "/docker-entrypoint.…" 32 seconds ago Exited (0) 2 seconds ago funny_gauss
34b68af9deef nginx "/docker-entrypoint.…" 35 seconds ago Exited (0) 2 seconds ago interesting_mahavira
7513949674fc nginx "/docker-entrypoint.…" 36 seconds ago Exited (0) 2 seconds ago kind_nobel
$
  • 批量删除

和批量停止类似,可以使用 docker container rm $(docker container ps -aq)

docker system prune -a -f 可以快速对系统进行清理,删除停止的容器,不用的image,等等

Container Mode 容器运行的各种模式

attach 模式

范例指令: docker container run -p 80:80 nginx

  • 透过这种方式创建容器的话,容器在前台执行
  • 容器的输入输出结果会反映到本地端,本地端的输入输出也会反映到容器,例如能在终端机看到网页浏览器的 log,ctrl + c 会让容器停止执行
  • 一般情况不推荐使用

detach 模式(一般生产中使用)

范例指令:

1
docker container run -d -p 80:80 nginx
  • 容器会在后台执行

  • 想进入查看log 可通过

  • #这种方式进入 ctl+c退出,容器也会stop
    docker container attach id
    #故查看日志,推荐docker logs
    docker container logs id
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    <img src="https://onedrive.ipfscan.us.kg/_layouts/52/download.aspx?share=ESK2VbJqyeVNkVkJpTYGcLcBkg_TWv_72-smdrgNymDulg" alt="Docker.assets/image-20230613221008147.png" title="image-20230613221008147.png">

    <img src="https://onedrive.ipfscan.us.kg/_layouts/52/download.aspx?share=EaS_e1lUnfNEmdwCBwvdlesBknySmY8znRZ2d6mwKPZmPg" alt="Docker.assets/image-20230613221825728.png" title="image-20230613221825728.png">

    ### 连接容器的 shell

    **创建一个容器并进入交互式模式**

    ```shell
    docker container run -it
1
2
3
4
5
6
7
8
9
10
~ docker container run -it busybox sh
/ #
/ #
/ # ls
bin dev etc home proc root sys tmp usr var
/ # ps
PID USER TIME COMMAND
1 root 0:00 sh
8 root 0:00 ps
/ # exit

在一个已经运行的容器里执行一个额外的command

1
docker container exec -it
1
2
3
4
5
6
7
8
9
10
11
➜  ~ docker container run -d nginx
33d2ee50cfc46b5ee0b290f6ad75d724551be50217f691e68d15722328f11ef6
➜ ~
➜ ~ docker container exec -it 33d sh
#
#
# ls
bin boot dev docker-entrypoint.d docker-entrypoint.sh etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
#
# exit
➜ ~

容器和虚拟机 Container vs VM

Docker.assets/containers-vs-virtual-machines.jpg
  • 容器不是Mini虚拟机

  • 容器其实是进程Containers are just processes

  • 容器中的进程被限制了对CPU内存等资源的访问

  • 当进程停止后,容器就退出了

Docker.assets/image-20230613232708883.png

docker container run 背后发生了什么?

1
$ docker container run -d --publish 80:80 --name webhost nginx
  • 在本地查找是否有nginx这个image镜像,但是没有发现
  • 去远程的image registry查找nginx镜像(默认的registry是Docker Hub)
  • 下载最新版本的nginx镜像 (nginx:latest 默认)
  • 基于nginx镜像来创建一个新的容器,并且准备运行
  • docker engine分配给这个容器一个虚拟IP地址
  • 在宿主机上打开80端口并把容器的80端口转发到宿主机上
  • 启动容器,运行指定的命令(这里是一个shell脚本去启动nginx)

3. 镜像的获取的三种方式

  • pull from registry (online) 从registry拉取
    • public(公有)
    • private(私有)
  • build from Dockerfile (online) 从Dockerfile构建
  • load from file (offline) 文件导入 (离线)
Docker.assets/docker-stages.png

镜像的命令查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ docker image

Usage: docker image COMMAND

Manage images

Commands:
build Build an image from a Dockerfile
history Show the history of an image
import Import the contents from a tarball to create a filesystem image
inspect Display detailed information on one or more images
load Load an image from a tar archive or STDIN
ls List images
prune Remove unused images
pull Pull an image or a repository from a registry
push Push an image or a repository to a registry
rm Remove one or more images
save Save one or more images to a tar archive (streamed to STDOUT by default)
tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE

Run 'docker image COMMAND --help' for more information on a command.

从registry拉取pull

1
2
3
4
5
docker pull nginx #默认从Docker Hub拉取,如果不指定版本,会拉取最新版
docker pull nginx:1.20.0 # 指定版本
docker pull quay.io/bitnami/nginx #从指定仓库Quay上拉取镜像
docker image ls #镜像的查看
docker image rm id #镜像的删除

从离线导入load

模拟生成一个离线的镜像

1
docker image save nginx:1.20.0 -o nginx.myself
Docker.assets/image-20230614002229573.png
1
docker image load -i nginx.myself
Docker.assets/image-20230614002610129.png

从dockerfile build

Dockerfile 介绍

Docker can build images automatically by reading the instructions from a Dockerfile. A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. Using docker build users can create an automated build that executes several command-line instructions in succession.

https://docs.docker.com/engine/reference/builder/

  • Dockerfile是用于构建docker镜像的文件
  • Dockerfile里包含了构建镜像所需的“指令”
  • Dockerfile有其特定的语法规则

Dockerfile的构建

示例:使用centos 脚本打印”hello docker !”

hello.sh

1
echo "hello docker !"

Dockerfile

1
2
3
FROM centos:7
ADD hello.sh /
CMD ["sh", "/hello.sh"]
Docker.assets/image-20230614065355333.png

示例:部署一个Hello Docker java项目

新建HelloWorld,并将项目打包

1
2
3
4
5
6
public class HelloDocker {
public static void main(String[] args) {
System.out.println("Hello Docker !");
}
}

新建一个Dockerfile文件

1
2
3
FROM openjdk:8-jdk-alpine
ADD *.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

将文件上传到linux服务器上

Docker.assets/image-20230614043947938.png

构建镜像

1
docker image build -t docker-demo-java .
Docker.assets/image-20230614044514049.png

重新打标签

1
2
3
docker image tag docker-demo-java docker-demo-java:2.0
#实际应按照远程仓库的规则打标签
docker tag [ImageId] registry.cn-shenzhen.aliyuncs.com/gordonchan/quickstart:[镜像版本号]
Docker.assets/image-20230614050147206.png

推送镜像到远程仓库进行共享

1
2
docker login --username=XXXXXXX registry.cn-shenzhen.aliyuncs.com
docker push registry.cn-shenzhen.aliyuncs.com/gordonchan/quickstart:[镜像版本号]
Docker.assets/image-20230614054131643.png Docker.assets/image-20230614053824763.png

拉取镜像并构建容器运行

Docker.assets/image-20230614055202954.png Docker.assets/image-20230614055347798.png

示例:执行一个Python程序

容器及进程,所以镜像就是一个运行这个进程所需要的环境。

假如我们要在centos:7上运行下面这个hello.py的Python程序

hello.py的文件内容:

1
print("hello docker")

第一步,准备Python环境

1
yum install -y python3.9 python3-pip python3.9-dev

第二步,运行hello.py

1
2
$ python3 hello.py
hello docker

把上面的程序构建成一个Dockerfile

Dockerfile

1
2
3
4
5
ARG image=centos:7
FROM ${image}
RUN yum install -y python3.9 python3-pip python3.9-dev
ADD hello.py /
CMD ["python3", "/hello.py"]

4.Dockerfile命令详解

基础镜像的选择 (FROM)

基本原则

  • 官方镜像优于非官方的镜像,如果没有官方镜像,则尽量选择Dockerfile开源的
  • 固定版本tag而不是每次都使用latest
  • 尽量选择体积小的镜像
1
2
3
4
5
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
bitnami/nginx 1.18.0 dfe237636dde 28 minutes ago 89.3MB
nginx 1.21.0-alpine a6eb2a334a9f 2 days ago 22.6MB
nginx 1.21.0 d1a364dc548d 2 days ago 133MB

Build一个Nginx镜像

假如我们有一个 index.html 文件

1
<h1>Hello Docker</h1>

准备一个Dockerfile

1
2
3
FROM nginx:1.21.0-alpine

ADD ../其他/index.html /usr/share/nginx/html/index.html

在image里RUN 执行指令

RUN 主要用于在build Image里执行指令,比如安装软件,下载文件等。

1
2
3
4
5
6
$ apt-get update
$ apt-get install wget
$ wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz
$ tar zxf ipinfo_2.0.1_linux_amd64.tar.gz
$ mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo
$ rm -rf ipinfo_2.0.1_linux_amd64.tar.gz

Dockerfile

1
2
3
4
5
6
7
FROM ubuntu:20.04
RUN apt-get update
RUN apt-get install -y wget
RUN wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz
RUN tar zxf ipinfo_2.0.1_linux_amd64.tar.gz
RUN mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo
RUN rm -rf ipinfo_2.0.1_linux_amd64.tar.gz

镜像的大小和分层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ipinfo latest 97bb429363fb 4 minutes ago 138MB
ubuntu 21.04 478aa0080b60 4 days ago 74.1MB
$ docker image history 97b
IMAGE CREATED CREATED BY SIZE COMMENT
97bb429363fb 4 minutes ago RUN /bin/sh -c rm -rf ipinfo_2.0.1_linux_amd… 0B buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c mv ipinfo_2.0.1_linux_amd64 /… 9.36MB buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c tar zxf ipinfo_2.0.1_linux_am… 9.36MB buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c wget https://github.com/ipinf… 4.85MB buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c apt-get install -y wget # bui… 7.58MB buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c apt-get update # buildkit 33MB buildkit.dockerfile.v0
<missing> 4 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 4 days ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 4 days ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 4 days ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 811B
<missing> 4 days ago /bin/sh -c #(nop) ADD file:d6b6ba642344138dc… 74.1MB

每一行的RUN命令都会产生一层image layer, 导致镜像的臃肿。

改进版Dockerfile

1
2
3
4
5
6
7
FROM ubuntu:20.04
RUN apt-get update && \
apt-get install -y wget && \
wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz && \
tar zxf ipinfo_2.0.1_linux_amd64.tar.gz && \
mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo && \
rm -rf ipinfo_2.0.1_linux_amd64.tar.gz
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ipinfo-new latest fe551bc26b92 5 seconds ago 124MB
ipinfo latest 97bb429363fb 16 minutes ago 138MB
ubuntu 21.04 478aa0080b60 4 days ago 74.1MB
$ docker image history fe5
IMAGE CREATED CREATED BY SIZE COMMENT
fe551bc26b92 16 seconds ago RUN /bin/sh -c apt-get update && apt-get… 49.9MB buildkit.dockerfile.v0
<missing> 4 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 4 days ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 4 days ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 4 days ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 811B
<missing> 4 days ago /bin/sh -c #(nop) ADD file:d6b6ba642344138dc… 74.1MB
$

文件复制 (ADD,COPY)

往镜像里复制文件有两种方式,COPYADD , 我们来看一下两者的不同。

复制普通文件

COPYADD 都可以把local的一个文件复制到镜像里,如果目标目录不存在,则会自动创建

1
2
FROM python:3.9.5-alpine3.13
COPY hello.py /app/hello.py

比如把本地的 hello.py 复制到 /app 目录下。 /app这个folder不存在,则会自动创建

复制压缩文件

ADD 比 COPY高级一点的地方就是,如果复制的是一个gzip等压缩文件时,ADD会帮助我们自动去解压缩文件。

1
2
FROM python:3.9.5-alpine3.13
ADD hello.tar.gz /app/

如何选择

因此在 COPY 和 ADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD。

构建参数和环境变量 (ARG vs ENV)

ARGENV 是经常容易被混淆的两个Dockerfile的语法,都可以用来设置一个“变量”。 但实际上两者有很多的不同。

1
2
3
4
5
6
7
FROM ubuntu:20.04
RUN apt-get update && \
apt-get install -y wget && \
wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz && \
tar zxf ipinfo_2.0.1_linux_amd64.tar.gz && \
mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo && \
rm -rf ipinfo_2.0.1_linux_amd64.tar.gz

ENV

1
2
3
4
5
6
7
8
FROM ubuntu:20.04
ENV VERSION=2.0.1
RUN apt-get update && \
apt-get install -y wget && \
wget https://github.com/ipinfo/cli/releases/download/ipinfo-${VERSION}/ipinfo_${VERSION}_linux_amd64.tar.gz && \
tar zxf ipinfo_${VERSION}_linux_amd64.tar.gz && \
mv ipinfo_${VERSION}_linux_amd64 /usr/bin/ipinfo && \
rm -rf ipinfo_${VERSION}_linux_amd64.tar.gz

ARG

1
2
3
4
5
6
7
8
FROM ubuntu:20.04
ARG VERSION=2.0.1
RUN apt-get update && \
apt-get install -y wget && \
wget https://github.com/ipinfo/cli/releases/download/ipinfo-${VERSION}/ipinfo_${VERSION}_linux_amd64.tar.gz && \
tar zxf ipinfo_${VERSION}_linux_amd64.tar.gz && \
mv ipinfo_${VERSION}_linux_amd64 /usr/bin/ipinfo && \
rm -rf ipinfo_${VERSION}_linux_amd64.tar.gz

区别

Docker.assets/docker_environment_build_args.png

ARG 可以在镜像build的时候动态修改value, 通过 --build-arg

1
2
3
4
5
6
7
8
9
$ docker image build -f .\Dockerfile-arg -t ipinfo-arg-2.0.0 --build-arg VERSION=2.0.0 .
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ipinfo-arg-2.0.0 latest 0d9c964947e2 6 seconds ago 124MB
$ docker container run -it ipinfo-arg-2.0.0
root@b64285579756:/#
root@b64285579756:/# ipinfo version
2.0.0
root@b64285579756:/#

ARG 用于构建时传递参数,而 ENV 用于在镜像中设置环境变量供容器运行时使用。

容器启动命令 CMD

CMD可以用来设置容器启动时默认会执行的命令。

  • 容器启动时默认执行的命令
  • 如果docker container run启动容器时指定了其它命令,则CMD命令会被忽略
  • 如果定义了多个CMD,只有最后一个会被执行。
1
2
3
4
5
6
7
8
FROM ubuntu:20.04
ENV VERSION=2.0.1
RUN apt-get update && \
apt-get install -y wget && \
wget https://github.com/ipinfo/cli/releases/download/ipinfo-${VERSION}/ipinfo_${VERSION}_linux_amd64.tar.gz && \
tar zxf ipinfo_${VERSION}_linux_amd64.tar.gz && \
mv ipinfo_${VERSION}_linux_amd64 /usr/bin/ipinfo && \
rm -rf ipinfo_${VERSION}_linux_amd64.tar.gz
1
2
3
4
5
6
7
8
$ docker image build -t ipinfo .
$ docker container run -it ipinfo
root@8cea7e5e8da8:/#
root@8cea7e5e8da8:/#
root@8cea7e5e8da8:/#
root@8cea7e5e8da8:/# pwd
/
root@8cea7e5e8da8:/#

默认进入到shell是因为在ubuntu的基础镜像里有定义CMD

1
2
3
4
5
6
7
8
9
$docker image history ipinfo
IMAGE CREATED CREATED BY SIZE COMMENT
db75bff5e3ad 24 hours ago RUN /bin/sh -c apt-get update && apt-get… 50MB buildkit.dockerfile.v0
<missing> 24 hours ago ENV VERSION=2.0.1 0B buildkit.dockerfile.v0
<missing> 7 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 7 days ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 7 days ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 7 days ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 811B
<missing> 7 days ago /bin/sh -c #(nop) ADD file:d6b6ba642344138dc… 74.1MB

容器启动命令 ENTRYPOINT

ENTRYPOINT 也可以设置容器启动时要执行的命令,但是和CMD是有区别的。

  • CMD 设置的命令,可以在docker container run 时传入其它命令,覆盖掉 CMD 的命令,但是 ENTRYPOINT 所设置的命令是一定会被执行的。
  • ENTRYPOINTCMD 可以联合使用,ENTRYPOINT 设置执行的命令,CMD传递参数
1
2
FROM ubuntu:20.04
CMD ["echo", "hello docker"]

把上面的Dockerfile build成一个叫 demo-cmd 的镜象

1
2
3
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
demo-cmd latest 5bb63bb9b365 8 days ago 74.1MB
1
2
FROM ubuntu:20.04
ENTRYPOINT ["echo", "hello docker"]

build成一个叫 demo-entrypoint 的镜像

1
2
3
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
demo-entrypoint latest b1693a62d67a 8 days ago 74.1MB

CMD的镜像,如果执行创建容器,不指定运行时的命令,则会默认执行CMD所定义的命令,打印出hello docker

1
2
$ docker container run -it --rm demo-cmd
hello docker

但是如果我们docker container run的时候指定命令,则该命令会覆盖掉CMD的命令,如:

1
2
$ docker container run -it --rm demo-cmd echo "hello world"
hello world

但是ENTRYPOINT的容器里ENTRYPOINT所定义的命令则无法覆盖,一定会执行

1
2
3
4
5
$ docker container run -it --rm demo-entrypoint
hello docker
$ docker container run -it --rm demo-entrypoint echo "hello world"
hello docker echo hello world
$

Shell 格式和 Exec 格式

容器的启动命令中带有参数传递的处理方式

CMD和ENTRYPOINT同时支持shell格式和Exec格式。

Shell格式

1
CMD echo "hello docker"
1
ENTRYPOINT echo "hello docker"

Exec格式

以可执行命令的方式

1
ENTRYPOINT ["echo", "hello docker"]
1
CMD ["echo", "hello docker"]

注意shell脚本的问题

1
2
3
FROM ubuntu:20.04
ENV NAME=docker
CMD echo "hello $NAME"

假如我们要把上面的CMD改成Exec格式,下面这样改是不行的, 大家可以试试。

1
2
3
FROM ubuntu:20.04
ENV NAME=docker
CMD ["echo", "hello $NAME"]

它会打印出 hello $NAME , 而不是 hello docker ,那么需要怎么写呢? 我们需要以shell脚本的方式去执行

1
2
3
FROM ubuntu:20.04
ENV NAME=docker
CMD ["sh", "-c", "echo hello $NAME"]

实战:构建一个 Python Flask 镜像

Python 程序

1
2
3
4
5
6
7
8
from flask import Flask

app = Flask(__name__)


@app.route('/')
def hello_world():
return 'Hello, World!'

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
FROM python:3.9.5-slim

COPY app.py /src/app.py

RUN pip install flask

WORKDIR /src
ENV FLASK_APP=app.py

EXPOSE 5000

CMD ["flask", "run", "-h", "0.0.0.0"]
1
2
docker image build -t flask .
docker run -d -p 5000:5000 flask
Docker.assets/image-20230614183643911.png Docker.assets/image-20230614183813272.png

Dockerfile 技巧——合理使用缓存

如果按照上述Dockerfile,修改app.py后,之后的操作比如install flask都不会使用cache,这样影响构建效率,因此,把经常改变的内容放在后面,尽可能的使用缓存,提高构建速度。

1
2
3
4
5
6
7
FROM python:3.9.5-slim
RUN pip install flask
WORKDIR /src
ENV FLASK_APP=app.py
EXPOSE 5000
COPY app.py /src/app.py
CMD ["flask", "run", "-h", "0.0.0.0"]
Docker.assets/image-20230614185305649.png Docker.assets/image-20230614185826404.png

Dockerfile 技巧——合理使用 .dockerignore

什么是Docker build context

Docker是client-server架构,理论上Client和Server可以不在一台机器上。

在构建docker镜像的时候,需要把所需要的文件由CLI(client)发给Server,这些文件实际上就是build context

加入文件夹下有些文件不是必须的,可以过滤掉。

Dockerfile

1
2
3
4
5
6
7
FROM python:3.9.5-slim
RUN pip install flask
WORKDIR /src
ENV FLASK_APP=app.py
EXPOSE 5000
COPY ./ /src/
CMD ["flask", "run", "-h", "0.0.0.0"]
Docker.assets/image-20230614204910784.png Docker.assets/image-20230614205051618.png

增加vim .dockerignore

1
./hello.image

有了.dockerignore文件后,我们再build, build context就小了很多

Docker.assets/image-20230614205127406.png Docker.assets/image-20230614205156437.png

Dockerfile 技巧——镜像的多阶段构建

这一节来聊聊多阶段构建,以及为什么要使用它。

  • C语言例子

假如有一个C的程序,我们想用Docker去做编译,然后执行可执行文件。

1
2
3
4
5
6
#include <stdio.h>

void main(int argc, char *argv[])
{
printf("hello %s\n", argv[argc - 1]);
}

本地测试(如果你本地有C环境)

1
2
3
4
5
6
7
8
9
10
$ gcc --static -o hello hello.c
$ ls
hello hello.c
$ ./hello docker
hello docker
$ ./hello world
hello world
$ ./hello friends
hello friends
$

构建一个Docker镜像,因为要有C的环境,所以我们选择gcc这个image

1
2
3
4
5
6
7
8
9
10
11
FROM gcc:9.4

COPY hello.c /src/hello.c

WORKDIR /src

RUN gcc --static -o hello hello.c

ENTRYPOINT [ "/src/hello" ]

CMD []

build和测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ docker build -t hello .
Sending build context to Docker daemon 5.12kB
Step 1/6 : FROM gcc:9.4
---> be1d0d9ce039
Step 2/6 : COPY hello.c /src/hello.c
---> Using cache
---> 70a624e3749b
Step 3/6 : WORKDIR /src
---> Using cache
---> 24e248c6b27c
Step 4/6 : RUN gcc --static -o hello hello.c
---> Using cache
---> db8ae7b42aff
Step 5/6 : ENTRYPOINT [ "/src/hello" ]
---> Using cache
---> 7f307354ee45
Step 6/6 : CMD []
---> Using cache
---> 7cfa0cbe4e2a
Successfully built 7cfa0cbe4e2a
Successfully tagged hello:latest
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
hello latest 7cfa0cbe4e2a 2 hours ago 1.14GB
gcc 9.4 be1d0d9ce039 9 days ago 1.14GB
$ docker run --rm -it hello docker
hello docker
$ docker run --rm -it hello world
hello world
$ docker run --rm -it hello friends
hello friends
$

可以看到镜像非常的大,1.14GB

实际上当我们把hello.c编译完以后,并不需要这样一个大的GCC环境,一个小的alpine镜像就可以了。

这时候我们就可以使用多阶段构建了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM gcc:9.4 AS builder

COPY hello.c /src/hello.c

WORKDIR /src

RUN gcc --static -o hello hello.c



FROM alpine:3.13.5

COPY --from=builder /src/hello /src/hello

ENTRYPOINT [ "/src/hello" ]

CMD []

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ docker build -t hello-apline -f Dockerfile-new .
Sending build context to Docker daemon 5.12kB
Step 1/8 : FROM gcc:9.4 AS builder
---> be1d0d9ce039
Step 2/8 : COPY hello.c /src/hello.c
---> Using cache
---> 70a624e3749b
Step 3/8 : WORKDIR /src
---> Using cache
---> 24e248c6b27c
Step 4/8 : RUN gcc --static -o hello hello.c
---> Using cache
---> db8ae7b42aff
Step 5/8 : FROM alpine:3.13.5
---> 6dbb9cc54074
Step 6/8 : COPY --from=builder /src/hello /src/hello
---> Using cache
---> 18c2bce629fb
Step 7/8 : ENTRYPOINT [ "/src/hello" ]
---> Using cache
---> 8dfb9d9d6010
Step 8/8 : CMD []
---> Using cache
---> 446baf852214
Successfully built 446baf852214
Successfully tagged hello-apline:latest
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-alpine latest 446baf852214 2 hours ago 6.55MB
hello latest 7cfa0cbe4e2a 2 hours ago 1.14GB
demo latest 079bae887a47 2 hours ago 125MB
gcc 9.4 be1d0d9ce039 9 days ago 1.14GB
$ docker run --rm -it hello-alpine docker
hello docker
$ docker run --rm -it hello-alpine world
hello world
$ docker run --rm -it hello-alpine friends
hello friends
$

可以看到这个镜像非常小,只有6.55MB

  • Java 语言例子

使用的openjdk:8-jdk-alpine就是瘦身过的镜像,不过要注意时区问题,要自己设置。

Dockerfile 技巧——尽量使用非root用户

Root的危险性

docker的root权限一直是其遭受诟病的地方,docker的root权限有那么危险么?我们举个例子。

假如我们有一个用户,叫demo,它本身不具有sudo的权限,所以就有很多文件无法进行读写操作,比如/root目录它是无法查看的。

1
2
3
4
[demo@docker-host ~]$ sudo ls /root
[sudo] password for demo:
demo is not in the sudoers file. This incident will be reported.
[demo@docker-host ~]$

但是这个用户有执行docker的权限,也就是它在docker这个group里。

1
2
3
4
5
6
[demo@docker-host ~]$ groups
demo docker
[demo@docker-host ~]$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox latest a9d583973f65 2 days ago 1.23MB
[demo@docker-host ~]$

这时,我们就可以通过Docker做很多越权的事情了,比如,我们可以把这个无法查看的/root目录映射到docker container里,你就可以自由进行查看了。

1
2
3
4
5
6
7
8
9
[demo@docker-host vagrant]$ docker run -it -v /root/:/root/tmp busybox sh
/ # cd /root/tmp
~/tmp # ls
anaconda-ks.cfg original-ks.cfg
~/tmp # ls -l
total 16
-rw------- 1 root root 5570 Apr 30 2020 anaconda-ks.cfg
-rw------- 1 root root 5300 Apr 30 2020 original-ks.cfg
~/tmp #

更甚至我们可以给我们自己加sudo权限。我们现在没有sudo权限

1
2
3
4
[demo@docker-host ~]$ sudo vim /etc/sudoers
[sudo] password for demo:
demo is not in the sudoers file. This incident will be reported.
[demo@docker-host ~]$

但是我可以给自己添加。

1
2
3
4
[demo@docker-host ~]$ docker run -it -v /etc/sudoers:/root/sudoers busybox sh
/ # echo "demo ALL=(ALL) ALL" >> /root/sudoers
/ # more /root/sudoers | grep demo
demo ALL=(ALL) ALL

然后退出container,bingo,我们有sudo权限了。

1
2
3
[demo@docker-host ~]$ sudo more /etc/sudoers | grep demo
demo ALL=(ALL) ALL
[demo@docker-host ~]$

如何使用非root用户

我们准备两个Dockerfile,第一个Dockerfile如下,

1
2
3
4
5
6
7
8
9
10
11
12
FROM python:3.9.5-slim

RUN pip install flask

COPY app.py /src/app.py

WORKDIR /src
ENV FLASK_APP=app.py

EXPOSE 5000

CMD ["flask", "run", "-h", "0.0.0.0"]

假设构建的镜像名字为 flask-demo

第二个Dockerfile,使用非root用户来构建这个镜像,名字叫 flask-no-root Dockerfile如下:

  • 通过groupadd和useradd创建一个flask的组和用户
  • 通过USER指定后面的命令要以flask这个用户的身份运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM python:3.9.5-slim

RUN pip install flask && \
groupadd -r flask && useradd -r -g flask flask && \
mkdir /src && \
chown -R flask:flask /src

USER flask

COPY app.py /src/app.py

WORKDIR /src
ENV FLASK_APP=app.py

EXPOSE 5000

CMD ["flask", "run", "-h", "0.0.0.0"]
1
2
3
4
5
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
flask-no-root latest 80996843356e 41 minutes ago 126MB
flask-demo latest 2696c68b51ce 49 minutes ago 125MB
python 3.9.5-slim 609da079b03a 2 weeks ago 115MB

分别使用这两个镜像创建两个容器

1
2
3
4
5
6
7
8
$ docker run -d --name flask-root flask-demo
b31588bae216951e7981ce14290d74d377eef477f71e1506b17ee505d7994774
$ docker run -d --name flask-no-root flask-no-root
83aaa4a116608ec98afff2a142392119b7efe53617db213e8c7276ab0ae0aaa0
$ docker container ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
83aaa4a11660 flask-no-root "flask run -h 0.0.0.0" 4 seconds ago Up 3 seconds 5000/tcp flask-no-root
b31588bae216 flask-demo "flask run -h 0.0.0.0" 16 seconds ago Up 15 seconds 5000/tcp f

5.Docker的存储

Data Volume

示例:保存数据了解数据是如何持久化?

环境准备

准备一个Dockerfile 和一个 my-cron的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ ls
Dockerfile my-cron
$ more Dockerfile
FROM alpine:latest
RUN apk update
RUN apk --no-cache add curl
ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.1.12/supercronic-linux-amd64 \
SUPERCRONIC=supercronic-linux-amd64 \
SUPERCRONIC_SHA1SUM=048b95b48b708983effb2e5c935a1ef8483d9e3e
RUN curl -fsSLO "$SUPERCRONIC_URL" \
&& echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - \
&& chmod +x "$SUPERCRONIC" \
&& mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" \
&& ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic
COPY my-cron /app/my-cron
WORKDIR /app

VOLUME ["/app"]

# RUN cron job
CMD ["/usr/local/bin/supercronic", "/app/my-cron"]
$
$ more my-cron
*/1 * * * * date >> /app/test.txt

工具supercronic:https://github.com/aptible/supercronic/ 这个专为容器而生的计划任务工具。

my-cron的意思是使用的my-cron就是一个crontab格式的计划任务,比如, 每隔一分钟时间输出到一个文件里

构建镜像

1
2
3
4
$ docker image build -t my-cron .
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
my-cron latest e9fbd9a562c9 4 seconds ago 24.7MB

创建容器(不指定-v参数),在这个Volume的mountpoint可以发现容器创建的文件。此时Docker会自动创建一个随机名字的volume,去存储我们在Dockerfile定义的volume VOLUME ["/app"]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ docker run -d my-cron
9a8fa93f03c42427a498b21ac520660752122e20bcdbf939661646f71d277f8f
$ docker volume ls
DRIVER VOLUME NAME
local 043a196c21202c484c69f2098b6b9ec22b9a9e4e4bb8d4f55a4c3dce13c15264
$ docker volume inspect 043a196c21202c484c69f2098b6b9ec22b9a9e4e4bb8d4f55a4c3dce13c15264
[
{
"CreatedAt": "2021-06-22T23:06:13+02:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/043a196c21202c484c69f2098b6b9ec22b9a9e4e4bb8d4f55a4c3dce13c15264/_data",
"Name": "043a196c21202c484c69f2098b6b9ec22b9a9e4e4bb8d4f55a4c3dce13c15264",
"Options": null,
"Scope": "local"
}
]

创建容器(指定-v参数)

在创建容器的时候通过 -v 参数我们可以手动的指定需要创建Volume的名字,以及对应于容器内的路径,这个路径是可以任意的,不必需要在Dockerfile里通过VOLUME定义

比如我们把上面的Dockerfile里的VOLUME删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FROM alpine:latest
RUN apk update
RUN apk --no-cache add curl
ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.1.12/supercronic-linux-amd64 \
SUPERCRONIC=supercronic-linux-amd64 \
SUPERCRONIC_SHA1SUM=048b95b48b708983effb2e5c935a1ef8483d9e3e
RUN curl -fsSLO "$SUPERCRONIC_URL" \
&& echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - \
&& chmod +x "$SUPERCRONIC" \
&& mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" \
&& ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic
COPY my-cron /app/my-cron
WORKDIR /app

# RUN cron job
CMD ["/usr/local/bin/supercronic", "/app/my-cron"]

重新build镜像,然后创建容器,加-v参数 容器外路径:容器内路径

1
2
docker image build -t my-cron .
docker container run -d -v cron-data:/app my-cron
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
43c6d0357b0893861092a752c61ab01bdfa62ea766d01d2fcb8b3ecb6c88b3de
$ docker volume ls
DRIVER VOLUME NAME
local cron-data
$ docker volume inspect cron-data
[
{
"CreatedAt": "2021-06-22T23:25:02+02:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/cron-data/_data",
"Name": "cron-data",
"Options": null,
"Scope": "local"
}
]
$ ls /var/lib/docker/volumes/cron-data/_data
my-cron
$ ls /var/lib/docker/volumes/cron-data/_data
my-cron test.txt

环境清理

强制删除所有容器,系统清理和volume清理

1
2
3
$ docker rm -f $(docker container ps -aq) #强制删除所有容器
$ docker system prune -f #删除不在运行的contianer cache
$ docker volume prune -f #Remove all unused local volumes

Data Volume 练习 MySQL

使用MySQL官方镜像,tag版本5.7

Dockerfile可以在这里查看 https://github.com/docker-library/mysql/tree/master/5.7

准备镜像

1
docker pull mysql:5.7

创建容器

关于MySQL的镜像使用,可以参考dockerhub https://hub.docker.com/_/mysql?tab=description&page=1&ordering=last_updated

关于Dockerfile Volume的定义,可以参考 https://github.com/docker-library/mysql/tree/master/5.7

1
2
#--name 容器名docker-mysql  -e 编辑密码 -v磁盘映射到docker下的mysql-data目录下
docker container run --name docker-mysql -e MYSQL_ROOT_PASSWORD=123456 -d -v mysql-data:/var/lib/mysql mysql:5.7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

$ docker volume ls
DRIVER VOLUME NAME
local mysql-data
$ docker volume inspect mysql-data
[
{
"CreatedAt": "2021-06-21T23:55:23+02:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/mysql-data/_data",
"Name": "mysql-data",
"Options": null,
"Scope": "local"
}
]
$

数据库写入数据

进入MySQL的shell

1
2
docker container exec -it 022 sh
mysql -u root -p
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.34 MySQL Community Server (GPL)

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.00 sec)

mysql> create database demo;
Query OK, 1 row affected (0.00 sec)

mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| demo |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.00 sec)

mysql> exit
Bye
# exit

创建了一个叫 demo的数据库

查看data volume

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ docker volume inspect mysql-data
[
{
"CreatedAt": "2021-06-22T00:01:34+02:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/mysql-data/_data",
"Name": "mysql-data",
"Options": null,
"Scope": "local"
}
]
$ ls /var/lib/docker/volumes/mysql-data/_data
auto.cnf client-cert.pem ib_buffer_pool ibdata1 performance_schema server-cert.pem
ca-key.pem client-key.pem ib_logfile0 ibtmp1 private_key.pem server-key.pem
ca.pem demo ib_logfile1 mysql public_key.pem sys
$

多个机器之间的容器共享数据

Docker.assets/volumes-shared-storage.png

官方参考链接 https://docs.docker.com/storage/volumes/#share-data-among-machines

Docker的volume支持多种driver。默认创建的volume driver都是local

1
2
3
4
5
6
7
8
9
10
11
12
$ docker volume inspect vscode
[
{
"CreatedAt": "2021-06-23T21:33:57Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/vscode/_data",
"Name": "vscode",
"Options": null,
"Scope": "local"
}
]

这一节我们看看一个叫sshfs的driver,如何让docker使用不在同一台机器上的文件系统做volume

环境准备

准备三台Linux机器,之间可以通过SSH相互通信。

hostname ip ssh username ssh password
docker-host1 192.168.200.10 vagrant vagrant
docker-host2 192.168.200.11 vagrant vagrant
docker-host3 192.168.200.12 vagrant vagrant

安装plugin

在其中两台机器上安装一个plugin vieux/sshfs

1
2
3
4
5
[vagrant@docker-host1 ~]$ docker plugin install --grant-all-permissions vieux/sshfs
latest: Pulling from vieux/sshfs
Digest: sha256:1d3c3e42c12138da5ef7873b97f7f32cf99fb6edde75fa4f0bcf9ed277855811
52d435ada6a4: Complete
Installed plugin vieux/sshfs
1
2
3
4
5
[vagrant@docker-host2 ~]$ docker plugin install --grant-all-permissions vieux/sshfs
latest: Pulling from vieux/sshfs
Digest: sha256:1d3c3e42c12138da5ef7873b97f7f32cf99fb6edde75fa4f0bcf9ed277855811
52d435ada6a4: Complete
Installed plugin vieux/sshfs

创建volume

1
2
3
4
[vagrant@docker-host1 ~]$ docker volume create --driver vieux/sshfs \
-o [email protected]:/home/vagrant \
-o password=vagrant \
sshvolume

查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[vagrant@docker-host1 ~]$ docker volume ls
DRIVER VOLUME NAME
vieux/sshfs:latest sshvolume
[vagrant@docker-host1 ~]$ docker volume inspect sshvolume
[
{
"CreatedAt": "0001-01-01T00:00:00Z",
"Driver": "vieux/sshfs:latest",
"Labels": {},
"Mountpoint": "/mnt/volumes/f59e848643f73d73a21b881486d55b33",
"Name": "sshvolume",
"Options": {
"password": "vagrant",
"sshcmd": "[email protected]:/home/vagrant"
},
"Scope": "local"
}
]

创建容器挂载Volume

创建容器,挂载sshvolume到/app目录,然后进入容器的shell,在/app目录创建一个test.txt文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[vagrant@docker-host1 ~]$ docker run -it -v sshvolume:/app busybox sh
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
b71f96345d44: Pull complete
Digest: sha256:930490f97e5b921535c153e0e7110d251134cc4b72bbb8133c6a5065cc68580d
Status: Downloaded newer image for busybox:latest
/ #
/ # ls
app bin dev etc home proc root sys tmp usr var
/ # cd /app
/app # ls
/app # echo "this is ssh volume"> test.txt
/app # ls
test.txt
/app # more test.txt
this is ssh volume
/app #
/app #

这个文件我们可以在docker-host3上看到

1
2
3
4
5
6
[vagrant@docker-host3 ~]$ pwd
/home/vagrant
[vagrant@docker-host3 ~]$ ls
test.txt
[vagrant@docker-host3 ~]$ more test.txt
this is ssh volume

如何给已创建的容器额外挂载共享文件夹

一、背景

在使用docker过程中,有时候创建容器时候没有设置挂载本地数据卷进行文件夹共享,但已经在容器中配置完了环境,此时再重新创建一个容器非常麻烦,因此需要对已有的容器挂载数据卷。

二、挂载原理

Docker中所有的容器的配置,如挂载点、运行方式等都是以json文件进行配置,修改对应的json文件参数即可挂载指定文件夹。

配置容器的json文件
/var/lib/docker/containers/<容器ID>/config.v2.json
/var/lib/docker/containers/<容器ID>/hostconfig.json

三、打开文件

  1. 使用 docker ps -a拿到需要更改的container的12位ID,然后docker inspect id,拿到64位ID(终端最上面的那个ID)

  2. 停止所有container 并使用service docker stop关闭docker服务(必须关闭Docker服务,否则无法修改成功

  3. 到目录/var/lib/docker/containers/<64位容器ID>/中复制 config.v2.jsonhostconfig.json两个文件到任意不用root权限的目录下,同时对原文件进行备份。

    4.在~/下新建两个同名文件(避免权限问题)
    cd ~/

touch config.v2.json hostconfig.json

5.新开一个终端,进入到容器目录下,打开文件
sudo -i ,提升权限
cd /var/lib/docker/containers/<64位容器ID>/

修改前一定要先备份下,否则改错了就GG!
cp config.v2.json config.v2.json.back
cp hostconfig.json hostconfig.back

四、添加共享文件夹挂载信息

打开**~/目录**下的这两个文件

1.修改config.v2.json文件

Docker.assets/image-20230624001942212.png

MountPoints参数下按照相应的格式进行添加相应的字段,注意,必须是绝对路径,且不能是/root,必须是/root/的二级子目录

2.修改hostconfig.json文件

hostconfig.json文件中的Binds参数添加宿主机和容器共享文件夹目录(注意,必须是绝对路径,且不能是/root,必须是/root/dataset这样的二级子目录

Docker.assets/20587097-181ef0aea6cb0724-1687536491371.png

五、修改容器配置

~/目录下的config.v2.jsonhostconfig.json两个文件内容,对应复制到以下文件中。
/var/lib/docker/containers/<容器ID>/config.v2.json
/var/lib/docker/containers/<容器ID>/hostconfig.json

六、启动docker 服务

1
service docker start

6.Docker网络

Bridge 网络

Docker.assets/two-container-network.png

容器间通信

两个容器都连接到了一个叫 docker0 的Linux bridge上

创建两个容器

1
2
3
docker container run -d  --name box1 busybox /bin/sh -c "while true; do sleep 3600; done"

docker container run -d --name box2 busybox /bin/sh -c "while true; do sleep 3600; done"
1
2
3
4
docker network ls
docker network inspect bridge
#查看网络
brctl show
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
NETWORK ID     NAME      DRIVER    SCOPE
1847e179a316 bridge bridge local
a647a4ad0b4f host host local
fbd81b56c009 none null local
$ docker network inspect bridge
[
{
"Name": "bridge",
"Id": "1847e179a316ee5219c951c2c21cf2c787d431d1ffb3ef621b8f0d1edd197b24",
"Created": "2021-07-01T15:28:09.265408946Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"03494b034694982fa085cc4052b6c7b8b9c046f9d5f85f30e3a9e716fad20741": {
"Name": "box1",
"EndpointID": "072160448becebb7c9c333dce9bbdf7601a92b1d3e7a5820b8b35976cf4fd6ff",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
},
"4f3303c84e5391ea37db664fd08683b01decdadae636aaa1bfd7bb9669cbd8de": {
"Name": "box2",
"EndpointID": "4cf0f635d4273066acd3075ec775e6fa405034f94b88c1bcacdaae847612f2c5",
"MacAddress": "02:42:ac:11:00:03",
"IPv4Address": "172.17.0.3/16",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]
1
2
3
4
5
brctl` 使用前需要安装, 对于CentOS, 可以通过 `sudo yum install -y bridge-utils` 安装. 对于Ubuntu, 可以通过 `sudo apt-get install -y bridge-utils
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.0242759468cf no veth8c9bb82
vethd8f9afb

查看路由

1
2
3
4
5
$ ip route
default via 10.0.2.2 dev eth0 proto dhcp metric 100
10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
192.168.200.0/24 dev eth1 proto kernel scope link src 192.168.200.10 metric 101

外部访问容器:端口转发

不同host主机之间的容器通信

创建容器

1
2
3
4
5
6
7
docker container run -d --rm --name web -p 8080:80 nginx:1.20.0
#暴露端口,外部访问容器
firewall-cmd --zone=public --add-port=80/tcp --permanent
#重启防火墙
systemctl restart firewalld
#查看已经对外暴露的端口,确认端口是否已经暴露
firewall-cmd --zone=public --list-ports
Docker.assets/image-20230615014550650.png

测试容器间是否通信,让box1去请求web,通信返回一个index文件。

1
docker container exec -it box1 wget http://172.17.0.5
Docker.assets/image-20230615014905449.png

测试外部网络是否能访问容器,访问web暴露出来的8080端口

Docker.assets/image-20230615015046323.png Docker.assets/image-20230615014928149.png

同一host暴露出来的端口需唯一,不然后起的服务报错,端口被占用 。这样就保证外部所访问的容器是唯一的。

1
docker container run -d --rm --name web2 -p 8080:80 nginx:1.20.0
Docker.assets/image-20230615031010565.png

如果network采用host模式,暴露出来是默认的80端口

1
docker container run -d --rm --name web1 --network host nginx:1.20.0
Docker.assets/image-20230615031050706.png

同一host中的不同bridge通信

创建和使用 bridge

在上面的启动的容器中,断开web的连接

1
docker network disconnect bridge web
Docker.assets/image-20230615020041753.png

创建一个自己的mybridge

1
docker network create mybridge
Docker.assets/image-20230615020459251.png

web连接mybridge,要实现box1能通信web,box1也得连接mybridge,而box2是ping不同web。

1
2
3
docker network connect mybridge web
docker container exec -it box1 ping web
docker network connect mybridge box1
Docker.assets/image-20230615022050502.png Docker.assets/image-20230615022408022.png

创建时可指定路由和ip范围

1
docker network create -d bridge --gateway 172.200.0.1 --subnet 172.200.0.0/16 detailbridge
Docker.assets/image-20230615022856154.png

容器通信的底层原理:

—————————————–网络命名空间和虚拟网络设备对

Linux的Namespace(命名空间)技术是一种隔离技术,常用的Namespace有 user namespace, process namespace, network namespace等

网络命名空间 是 Linux 内核用来隔离不同容器间的网络资源(每个 Docker 容器都拥有一个独立的网络命名空间),网络命名空间主要隔离的资源包括:

1.iptables规则表

2.路由规则表

3.网络设备列表

如下图所示,当系统中拥有 3 个网络命名空间:

Docker.assets/image-20230624000916878.png

由于不同的网络命名空间之间是相互隔离的,所以不同的网络命名空间之间并不能直接通信。比如在 网络命名空间A 配置了一个 IP 地址为 172.17.42.1 的设备,但在 网络命名空间B 里却不能访问,如下图所示:

Docker.assets/image-20230624000945278.png

就好比两台电脑,如果没有任何网线连接,它们之间是不能通信的。所以,Linux 内核提供了 虚拟网络设备对(veth) 这个功能,用于解决不同网络命名空间之间的通信。

Docker 就是使用 虚拟网络设备对 来实现不同容器之间的通信,其原理如下图:

Docker.assets/image-20230624001011324.png Docker.assets/image-20230615194209960.png

以下模拟的是容器的通信原理,即不同命名空间的通信

新建增加网络命名空间脚本

1
vim add-ns-to-br.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash

bridge=$1
namespace=$2
addr=$3

#虚拟网络设备对
#在docker0上
vethA=veth-$namespace
#在容器内
vethB=eth00-$namespace
#新建命名空间
sudo ip netns add $namespace
#创建一个veth pair
sudo ip link add $vethA type veth peer name $vethB
#设置虚拟网络设备所属命名空间
sudo ip link set $vethB netns $namespace
#给虚拟网络设备添加ip
sudo ip netns exec $namespace ip addr add $addr dev $vethB
#开启网络命令空间 namespace vethB 端口
sudo ip netns exec $namespace ip link set $vethB up

sudo ip link set $vethA up

sudo brctl addif $bridge $vethA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
brctl --help查看使用命令 

Usage: brctl [commands]
commands:
addbr <bridge> add bridge
delbr <bridge> delete bridge
addif <bridge> <device> add interface to bridge
delif <bridge> <device> delete interface from bridge
hairpin <bridge> <port> {on|off} turn hairpin on/off
setageing <bridge> <time> set ageing time
setbridgeprio <bridge> <prio> set bridge priority
setfd <bridge> <time> set bridge forward delay
sethello <bridge> <time> set hello time
setmaxage <bridge> <time> set max message age
setpathcost <bridge> <port> <cost> set path cost
setportprio <bridge> <port> <prio> set port priority
show [ <bridge> ] show a list of bridges
showmacs <bridge> show a list of mac addrs
showstp <bridge> show bridge stp info
stp <bridge> {on|off} turn stp on/off

[root@container1 docker]# ip netns help
Usage: ip netns list
ip netns add NAME
ip netns set NAME NETNSID
ip [-all] netns delete [NAME]
ip netns identify [PID]
ip netns pids NAME
ip [-all] netns exec [NAME] cmd ...
ip netns monitor
ip netns list-id

启动两个容器

1
2
3
docker container run -d --name box3 --network mybridge  busybox /bin/sh -c "while true; do sleep 3600; done"
docker container run -d --name box4 --network mybridge busybox /bin/sh -c "while true; do sleep 3600; done"

Docker.assets/image-20230615164951809.png Docker.assets/image-20230615165300217.png

获取bridge name

1
brctl show
Docker.assets/image-20230615165442594.png

设置命名空间

1
2
3
4
5
6
7
8
sh add-ns-to-br.sh br-1379584d9670 ns1 172.19.0.2/16
sh add-ns-to-br.sh br-1379584d9670 ns2 172.19.0.3/16
#查看网络命名空间list
sudo ip netns ls
#进入命令空间查看ip信息
sudo ip netns exec ns2
ip a
ping 172.19.0.2
Docker.assets/image-20230615191429985.png

以上就是模拟实现了不同网络命名空间之间的通信。

断开虚拟网络设备,不同的网络命名空间就ping不通,实现隔离。

1
sudo ip link set veth-ns1 down
Docker.assets/image-20230615204622919.png
1
2
3
4
5
6
7
8
9
10
11
12
#重启网卡
service network restart
#查看配置信息
ifconfig
#错误日志查看
cat /var/log/messages | grep network
#查看docker的情况
systemctl status docker.service
#关掉网桥
ifconfig docker0 down
#删除网桥
brctl delbr docker0

7.Docker Compose

docker compose 介绍

要启动一些容器需进行一系列操作,一般我们使用shell脚本进行一键式操作。而基于这个需求,docker compose就是专门用于解决这种组合的操作。通过配置yml文件来实现。

Docker.assets/docker-compose-intro.png

docker compose 的安装

Windows和Mac在默认安装了docker desktop以后,docker-compose随之自动安装

1
2
PS C:\Users\Peng Xiao\docker.tips> docker-compose --version
docker-compose version 1.29.2, build 5becea4c

Linux用户需要自行安装

最新版本号可以在这里查询 https://github.com/docker/compose/releases

1
2
3
4
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ docker-compose --version
docker-compose version 1.29.2, build 5becea4c

熟悉python的朋友,可以使用pip去安装docker-Compose

1
2
3
#快速安装对应版本pip
wget https://bootstrap.pypa.io/pip/3.6/get-pip.py
python3 get-pip.py
1
pip install docker-compose

docker-compose基本使用

compose 文件的结构和版本

docker compose文件的语法说明 https://docs.docker.com/compose/compose-file/

基本语法结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
version: "3.8"

services: # 容器
servicename: # 服务名字,这个名字也是内部 bridge网络可以使用的 DNS name
image: # 镜像的名字
command: # 可选,如果设置,则会覆盖默认镜像里的 CMD命令
environment: # 可选,相当于 docker run里的 --env
volumes: # 可选,相当于docker run里的 -v
networks: # 可选,相当于 docker run里的 --network
ports: # 可选,相当于 docker run里的 -p
servicename2:

volumes: # 可选,相当于 docker volume create

networks: # 可选,相当于 docker network create

以 Python Flask + Redis练习:为例子,改造成一个docker-compose文件

自定义的flask-demo

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask
from redis import Redis
import os
import socket

app = Flask(__name__)
redis = Redis(host=os.environ.get('REDIS_HOST', '127.0.0.1'), port=6379)


@app.route('/')
def hello():
redis.incr('hits')
return f"Hello Container World! I have been seen {redis.get('hits').decode('utf-8')} times and my hostname is {socket.gethostname()}.\n"

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM python:3.9.5-slim

RUN python -m pip install --upgrade pip && \
pip install flask redis && \
groupadd -r flask && useradd -r -g flask flask && \
mkdir /src && \
chown -R flask:flask /src

USER flask

WORKDIR /src

ENV FLASK_APP=app.py REDIS_HOST=redis

COPY app.py /src/app.py
EXPOSE 5000

CMD ["flask", "run", "-h", "0.0.0.0"]

未使用docker-compose之前的操作

1
2
3
4
5
6
7
8
9
docker image pull redis
docker image build -t flask-demo .

# create network
docker network create -d bridge demo-network

# create container
docker container run -d --name redis-server --network demo-network redis
docker container run -d --network demo-network --name flask-demo --env REDIS_HOST=redis-server -p 5000:5000 flask-demo

只构建flask-demo镜像,后面可以直接拉取

1
docker image build -t flask-demo .

docker-compose.yml 文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: "3.8"

services:
flask-demo:
image: flask-demo:latest
environment:
- REDIS_HOST=redis-server
networks:
- demo-network
ports:
- 8080:5000

redis-server:
image: redis:latest
networks:
- demo-network

networks:
demo-network:

基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Usage:
docker-compose [-f <arg>...] [--profile <name>...] [options] [--] [COMMAND] [ARGS...]
docker-compose -h|--help

Commands:
build Build or rebuild services
config Validate and view the Compose file
create Create services
down Stop and remove resources
events Receive real time events from containers
exec Execute a command in a running container
help Get help on a command
images List images
kill Kill containers
logs View output from containers
pause Pause services
port Print the public port for a port binding
ps List containers
pull Pull service images
push Push service images
restart Restart services
rm Remove stopped containers
run Run a one-off command
scale Set number of containers for a service
start Start services
stop Stop services
top Display the running processes
unpause Unpause services
up Create and start containers
version Show version information and quit
1
2
3
4
5
6
#确认配置
docker-compose config
# 从配置build的Dockerfile构建镜像,并不会启动容器,可以用于构建镜像或者更新镜像,没配置直接拉取镜像
docker-compose build
#获取镜像启动服务,镜像不存在,按情况从仓库拉取或build
docker-compose up -d
Docker.assets/image-20230616003716859.png Docker.assets/image-20230615233815833.png
1
2
3
4
#查看compose中的相关镜像,只显示该compose服务包含的镜像
docker-compose images
#查看容器的运行情况
docker-compose ps
Docker.assets/image-20230616003533812.png
1
2
3
4
5
6
#对容器的操作就是docker的操作
docker ps
docker exec -it 0b sh
#对redis进行读写数据
set k1 v1
get k1
Docker.assets/image-20230616005138911.png
1
2
3
4
#停止服务
docker-compose stop
#移除停止container
docker-compose rm
Docker.assets/image-20230616005633040.png

docker-compose 服务的构建、拉取与更新

带有build的yml,会视情况构建镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: "3.8"

services:
flask-demo:
image: flask-demo:latest
build:
context: .
dockerfile: Dockerfile
environment:
- REDIS_HOST=redis-server
networks:
- demo-network
ports:
- 8080:5000

redis-server:
image: redis:latest
networks:
- demo-network

networks:
demo-network:
Docker.assets/image-20230616010445855.png
1
2
3
4
#根据Dockerfile重新下载需要的镜像并构建容器,并启动
docker-compose up --build -d
#重新启动单个容器
docker-compose up -d --build flask

根据Dockerfile重新下载需要的镜像并构建容器,也就是说这句相当于是 docker-compose build –no-cache 和 docker-compose up -d 的集合体,
意味着构建镜像的时候是根据Dockerfile的最新内容来的,而不会使用缓存,这样就避免了构建镜像时由于缓存造成的影响。

docker-compose 网络

如果配置没指定网络,则创建默认的网络docker-compose_default(文件目录_default)

1
2
3
4
5
version: "3.8"

services:
box1:
image: busybox
Docker.assets/image-20230616030540760.png

多网络的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
version: "3.8"

services:
box1:
image: busybox
command: /bin/sh -c "while true; do sleep 3600; done"
networks:
- demo-network1

box2:
image: busybox
command: /bin/sh -c "while true; do sleep 3600; done"
networks:
- demo-network2
box3:
image: busybox
command: /bin/sh -c "while true; do sleep 3600; done"
networks:
- demo-network1
- demo-network2

networks:
demo-network1:
ipam:
driver: default
config:
- subnet: 172.28.0.0/16
demo-network2:
Docker.assets/image-20230616024620573.png

按照配置,box1和box2 ping不通,都能和box3通信

Docker.assets/image-20230616032013489.png

水平扩展 scale

去掉端口指定,不然端口占用,启动失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
version: "3.8"

services:
flask:
build:
context: ../flask
dockerfile: Dockerfile
image: flask-demo:latest
environment:
- REDIS_HOST=redis-server
networks:
- demo-network

redis-server:
image: redis:latest
networks:
- demo-network
client:
image: busybox
command: /bin/sh -c "while true; do sleep 3600; done"
networks:
- demo-network

networks:
demo-network:
1
docker-compose up -d --build --scale flask=3 
Docker.assets/image-20230616034818237.png

与服务器通信

1
2
3
4
5
ping flask
#busybox没有curl命令使用wget
wget -O- flask:5000
#也可以用封装好的image
xiaopeng163/net-box:latest

wget -O-以’-‘作为file参数,那么数据将会被打印到标准输出,通常为控制台。

Docker.assets/image-20230616035043531.png Docker.assets/image-20230616143909418.png

外部如何访问,flask端口不能写死暴露?

通过nginx反向代理

nginx.conf 配置文件

1
2
3
4
5
6
server {
listen 80 default_server;
location / {
proxy_pass http://flask:5000;
}
}

yml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
version: "3.8"

services:
flask:
build:
context: ../flask
dockerfile: Dockerfile
image: flask-demo:latest
environment:
- REDIS_HOST=redis-server
depends_on:
- redis-server
networks:
- frontend
- backend

redis-server:
image: redis:latest
networks:
- backend
nginx:
image: nginx:stable-alpine
ports:
- 8000:80
depends_on:
- flask
volumes:
- ../nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ../log/nginx:/var/log/nginx
networks:
- frontend

networks:
backend:
frontend:

服务的依赖:nginx依赖flask,flask依赖redis,通过depend_on配置来定义程序启动顺序

ports的区别是,expose不会将端口暴露给主机,主机无法访问expose的端口。

.dockerignore

1
2
3
4
5
6
.env
.dockerignore
Dockerfile
docker-compose.yaml
nginx.conf
var
1
2
docker-compose config
docker-compose up -d --build --scale flask=3
Docker.assets/image-20230616155516681.png Docker.assets/image-20230616155244344.png

docker compose 环境变量

上述涉及的redis若添加密码,无论写到代码里还是配置里都是不安全的,故可以写到.env里面,.env文件.dockerignore文件不上传。

.env

1
REDIS_PASSWORD=ABC123

yml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
version: "3.8"

services:
flask:
build:
context: ../flask
dockerfile: Dockerfile
image: flask-demo:latest
environment:
- REDIS_HOST=redis-server
- REDIS_PASS=${REDIS_PASSWORD}
networks:
- frontend
- backend

redis-server:
image: redis:latest
command: redis-server --requirepass ${REDIS_PASSWORD}
networks:
- backend
nginx:
image: nginx:stable-alpine
ports:
- 8000:80
depends_on:
- flask
volumes:
- ../nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ../log/nginx:/var/log/nginx
networks:
- frontend

networks:
backend:
frontend:

app.py增加获取该变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask
from redis import StrictRedis
import os
import socket

app = Flask(__name__)
redis = StrictRedis(host=os.environ.get('REDIS_HOST', '127.0.0.1'),
port=6379, password=os.environ.get('REDIS_PASS'))


@app.route('/')
def hello():
redis.incr('hits')
return f"Hello Container World! I have been seen {redis.get('hits').decode('utf-8')} times and my hostname is {socket.gethostname()}.\n"

Docker.assets/image-20230616161615653.png

docker compose 健康检查

Dockerfile healthcheck https://docs.docker.com/engine/reference/builder/#healthcheck

docker compose https://docs.docker.com/compose/compose-file/compose-file-v3/#healthcheck

健康检查是容器运行状态的高级检查,主要是检查容器所运行的进程是否能正常的对外提供“服务”,比如一个数据库容器,我们不光 需要这个容器是up的状态,我们还要求这个容器的数据库进程能够正常对外提供服务,这就是所谓的健康检查。

容器的健康检查

容器本身有一个健康检查的功能,但是需要在Dockerfile里定义,或者在执行docker container run 的时候,通过下面的一些参数指定

1
2
3
4
5
6
7
8
9
10
--health-cmd string              Command to run to check health
--health-interval duration Time between running the check
(ms|s|m|h) (default 0s)
--health-retries int Consecutive failures needed to
report unhealthy
--health-start-period duration Start period for the container to
initialize before starting
health-retries countdown
(ms|s|m|h) (default 0s)
--health-timeout duration Maximum time to allow one check to

Dockerfile healthcheck示例源码

我们以下面的这个flask容器为例,Dockerfile如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FROM python:3.9.5-slim

RUN pip install flask redis && \
apt-get update && \
apt-get install -y curl && \
groupadd -r flask && useradd -r -g flask flask && \
mkdir /src && \
chown -R flask:flask /src

USER flask

COPY app.py /src/app.py

WORKDIR /src

ENV FLASK_APP=app.py REDIS_HOST=redis

EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:5000/ || exit 1

CMD ["flask", "run", "-h", "0.0.0.0"]

上面Dockerfili里的HEALTHCHECK 就是定义了一个健康检查。 会每隔30秒检查一次,如果失败就会退出,退出代码是1

查看容器状态

1
2
3
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
059c12486019 flask-demo "flask run -h 0.0.0.0" 4 hours ago Up 8 seconds (health: starting) 5000/tcp dazzling_tereshkova

也可以通过docker container inspect 059 查看详情, 其中有有关health的

1
2
3
4
5
6
7
8
9
10
11
12
"Health": {
"Status": "starting",
"FailingStreak": 1,
"Log": [
{
"Start": "2021-07-14T19:04:46.4054004Z",
"End": "2021-07-14T19:04:49.4055393Z",
"ExitCode": -1,
"Output": "Health check exceeded timeout (3s)"
}
]
}

模拟实现故障,把redis停掉之后,经过3次检查,一直是不通的,然后health的状态会从starting变为 unhealthy

Docker.assets/image-20230616165246979.png

重启redis,经过几秒钟,我们的flask 变成了healthy

Docker.assets/image-20230616165405507.png

docker compose health示例

除了在Dockerfile中配置,还可以在docker-compose.yml文件中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
version: "3.8"

services:
flask:
build:
context: ../flask
dockerfile: Dockerfile
image: flask-demo:latest
environment:
- REDIS_HOST=redis-server
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000"]
interval: 30s
timeout: 3s
retries: 3
start_period: 40s
depends_on:
redis-server:
condition: service_healthy
networks:
- frontend
- backend

redis-server:
image: redis:latest
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 1s
timeout: 3s
retries: 30
networks:
- backend
nginx:
image: nginx:stable-alpine
ports:
- 8000:80
depends_on:
flask:
condition: service_healthy
volumes:
- ../nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ../log/nginx:/var/log/nginx
networks:
- frontend

networks:
backend:
frontend:
Docker.assets/image-20230616172342028.png

一站式hadoop集群便于测试开发

通过 docker-compose 快速部署 Hadoop 集群详细教程 - 大数据老司机 - 博客园 (cnblogs.com)

包含了zookeeper 3.8.0,mysql5.7,hadoop3.3.5,hive3.1.3,spark 3.3.2,Flink1.17.0

1、下载

1
git clone https://gitee.com/hadoop-bigdata/docker-compose-hadoop.git

2、创建网络

1
2
3
4
5
# 创建,注意不能使用hadoop_network,要不然启动hs2服务的时候会有问题!!!
docker network create hadoop-network

# 查看
docker network ls

3、部署 mysql5.7

1
2
3
4
5
6
7
8
cd docker-compose-hadoop/mysql

docker-compose -f mysql-compose.yaml up -d

docker-compose -f mysql-compose.yaml ps

#root 密码:123456,以下是登录命令,注意一般在公司不能直接在命令行明文输入密码,要不然容易被安全抓,切记,切记!!!
docker exec -it mysql mysql -uroot -p123456

4、部署 Hadoop Hive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cd docker-compose-hadoop/hadoop_hive

docker-compose -f docker-compose.yaml up -d

# 查看
docker-compose -f docker-compose.yaml ps
# 查看指定列
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Ports}}"

# hive
docker exec -it hive-hiveserver2 hive -e "show databases";

# hiveserver2
docker exec -it hive-hiveserver2 beeline -u jdbc:hive2://hive-hiveserver2:10000 -n hadoop -e "show databases;"

5.测试访问hdfs UI 和 yarn UI ,该项目设置的是http: //ip:30070 和http: //ip:30889

6.yarn ui地址在镜像中写死了,如果没有进行转发处理,宿主机无法访问,可以进入hadoop-yarn-nm进行修改,写自己的域名,docker-compose.yaml中的暴露端口与之对应,然后重启。

1
2
3
4
5
<property>
<name>yarn.web-proxy.address</name>
<value>container1:9111</value>
</property>

Docker.assets/image-20230618005439820.png Docker.assets/image-20230618010027391.png

8.Docker Swarm

docker swarm 介绍

为什么不建议在生产环境中使用docker-compose

  • 多机器如何管理?
  • 如果跨机器做scale横向扩展?
  • 容器失败退出时如何新建容器确保服务正常运行?
  • 如何确保零宕机时间?
  • 如何管理密码,Key等敏感数据?
  • 其它

容器编排 swarm

Docker.assets/docker-compose_swarm.png

Swarm的基本架构

Docker.assets/swarm_arch.png

Swarm 单节点快速上手

初始化

这个命令可以查看我们的docker engine有没有激活swarm模式, 默认是没有的,我们会看到

1
docker info
Docker.assets/image-20230616195332575.png

激活swarm,有两个方法:

  • 初始化一个swarm集群,自己成为manager
  • 加入一个已经存在的swarm集群
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Swarm Commands:
config Manage Swarm configs
node Manage Swarm nodes
secret Manage Swarm secrets
service Manage Swarm services
stack Manage Swarm stacks
swarm Manage Swarm



Usage: docker swarm COMMAND

Manage Swarm

Commands:
ca Display and rotate the root CA
init Initialize a swarm
join Join a swarm as a node and/or manager
join-token Manage join tokens
leave Leave the swarm
unlock Unlock swarm
unlock-key Manage the unlock key
update Update the swarm
Docker.assets/image-20230616201111290.png
1
2
3
4
5
6
#初始化一个集群
docker swarm init
#查看集群节点
docker node ls
#查看node的信息
docker node inspect <node id>
Docker.assets/image-20230616201211209.png

docker swarm init 背后发生了什么

主要是PKI和安全相关的自动化

  • 创建swarm集群的根证书
  • manager节点的证书
  • 其它节点加入集群需要的tokens

创建Raft数据库用于存储证书,配置,密码等数据

RAFT相关资料

看动画学会 Raft 算法

https://mp.weixin.qq.com/s/p8qBcIhM04REuQ-uG4gnbw

1
2
3
4
5
swarm join-token :可以查看或更换join token。
docker swarm join-token worker:查看加入woker的命令。
docker swarm join-token manager:查看加入manager的命令
docker swarm join-token --rotate worker:重置woker的Token。
docker swarm join-token -q worker:仅打印Token。
1
2
#打开另外的虚拟机作为worker 加入,执行以下命令
docker swarm join --token SWMTKN-1-3hocsrayh278lv0lz17g0ty5bxedekofilq3pbiaj9xskqcjmh-331n58myelyc6bej36wosd69v 192.168.8.10:2377
Docker.assets/image-20230617134719031.png
1
2
#打开manager所在的虚拟机,查看node情况
docker node ls
Docker.assets/image-20230617134833434.png
1
2
3
#创建服务
docker service create --name nginx -d --replicas 3 nginx:stable-alpine
docker service ps nginx

调节service的副本数在docker swarm中可以在创建service时,通过设置–replicas 来设置副本数,但是当服务运行起来后应该如何处理呢?docker 提供了scale命令来实现这个功能

1
2
#scale 动态扩缩容
docker service scale nginx=2
Docker.assets/image-20230617143823599.png
1
2
3
4
#动态更新服务
docker service update --image nginx:latest nginx
#动态回滚服务
docker service update --rollback nginx
Docker.assets/image-20230617144531651.png Docker.assets/image-20230617144619636.png

Docker Stack

docker 与docker-compose的区别是后者可以部署多个容器;

docker swarm与docker stack的区别是stack可以部署多个服务;

docker stack与docker-compose的区别,stack是集群式,compose是单机模式,多用于开发测试。

stack获取不到.env,找到githup上的一种方法:

docker stack deploy in 1.13 doesn’t load .env file as docker-compose up does · Issue #29133 · moby/moby (github.com)

1
env $(cat .env | grep ^[A-Z] | xargs) docker stack deploy -c stack.yaml STACK

任务的编排可以进阶学习k8s。

9.Docker整合gitlab CICD

单模块下部署

Dockerfile配置

1
2
3
4
5
6
FROM openjdk:8-jre-alpine
VOLUME /tmp
###复制文件到容器app-springboot
COPY ./target/cicd-0.0.1-SNAPSHOT.jar app.jar

ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

.gitlab-ci.yml配置

单环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#1:需要用到的镜像
#2:必须配置的一些环境变量。如果本地可不配置 DOCKER_HOST。
#3:配置缓存,配置后,maven 下载的依赖可以被缓存起来,下次不需要重复去下载了。
#4:配置需要用到的额外的服务。docker:dind,这个貌似是用于在 docker 中运行 docker 的一种东西,在项目的构建中需要。
#有时需要在容器内执行 docker 命令,比如:在 jenkins 容器内运行 docker 命令执行构建镜像
#直接在 docker 容器内嵌套安装 docker 未免太过臃肿
#更好的办法是:容器内仅部署 docker 命令行工具(作为客户端),实际执行交由宿主机内的 docker-engine(服务器)
#先启动一个docker:dind容器A,再启动一个docker容器B,容器B指定host为A容器内的docker daemon。

#5:stages,这是 Gitlab CI 中的概念,Stages 表示构建阶段,就是一些按序执行的流程,具体执行是依赖于 Jobs 的。
#6 :定义的 Jobs 之一,用于构建 jar 包。内部又引入 maven 镜像来处理,负责执行 package 这一流程。script 为具体执行的脚本。
#7:定义的 Jobs 之一,用于构建 Docker 镜像。负责执行 deploy 这一流程。具体执行 build 和 run。only 节点表示只监控 master 分支。

image: docker:latest #1
variables: #2
DOCKER_DRIVER: overlay2
DOCKER_HOST: tcp://192.168.8.10:2375 # docker host,本地可不写
DOCKER_TLS_CERTDIR: ''
TAG: root/hello-spring:v0.1 # 镜像名称
cache: #3
key: m2-repo
paths:
- .m2/repository
services: #4
- docker:dind
stages: #5
- package
- deploy
maven-package: #6
image: maven:3.5.0-jdk-8
tags:
- maven
stage: package
script:
- mvn clean package -Dmaven.test.skip=true
artifacts:
paths:
- target/*.jar
build-master: #7
tags:
- docker
stage: deploy
script:
- docker build -t $TAG .
- docker rm -f test || true
- docker run -d --name test -p 8888:8888 $TAG
only:
- master

多环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#1:需要用到的镜像
#2:必须配置的一些环境变量。如果本地可不配置 DOCKER_HOST。
#3:配置缓存,配置后,maven 下载的依赖可以被缓存起来,下次不需要重复去下载了。
#4:配置需要用到的额外的服务。docker:dind,这个貌似是用于在 docker 中运行 docker 的一种东西,在项目的构建中需要。
#有时需要在容器内执行 docker 命令,比如:在 jenkins 容器内运行 docker 命令执行构建镜像
#直接在 docker 容器内嵌套安装 docker 未免太过臃肿
#更好的办法是:容器内仅部署 docker 命令行工具(作为客户端),实际执行交由宿主机内的 docker-engine(服务器)
#先启动一个docker:dind容器A,再启动一个docker容器B,容器B指定host为A容器内的docker daemon。

#5:stages,这是 Gitlab CI 中的概念,Stages 表示构建阶段,就是一些按序执行的流程,具体执行是依赖于 Jobs 的。
#6 :定义的 Jobs 之一,用于构建 jar 包。内部又引入 maven 镜像来处理,负责执行 package 这一流程。script 为具体执行的脚本。
#7:定义的 Jobs 之一,用于构建 Docker 镜像。负责执行 deploy 这一流程。具体执行 build 和 run。only 节点表示只监控 master 分支。

image: docker:latest #1
variables: #2
DOCKER_DRIVER: overlay2
DOCKER_HOST: tcp://192.168.8.10:2375 # docker host,本地可不写
DOCKER_TLS_CERTDIR: ''
USERSERVICE_TAG: ':1.0' # 镜像版本号
USERSERVICE_NAME: cicd #镜像名称
USERSERVICE_RPORT: 8888 #镜像端口 容器内的端口与其不一致
USERSERVICE_DIRECTORY: cicd #模块目录
PROFILE_ACTIVE: prd
cache: #3
key: m2-repo
paths:
- .m2/repository
services: #4
- docker:dind
stages: #5
- package
- deploy-dev
- deploy-prd
maven-package: #6
image: maven:3.5.0-jdk-8
tags:
- maven
stage: package
script:
- mvn clean package -P $PROFILE_ACTIVE -Dmaven.test.skip=true
artifacts:
paths:
- target/*.jar
build-dev:
tags:
- docker
stage: deploy-dev
script:
- docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
- docker rm -f $USERSERVICE_NAME || true
- docker rmi -f $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG || true
- docker build -f Dockerfile -t $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG .
- docker push $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG
- docker run -d --name $USERSERVICE_NAME -v $USERSERVICE_NAME:/apps --privileged=true -p $USERSERVICE_RPORT:$USERSERVICE_RPORT $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG
- docker image prune -f
only: #限制作业在什么时候创建,定义了哪些分支或标签(branches and tags)的作业会运行
refs: #分支
- dev
#changes: # 下面的文件中任一文件发生改变
# - .gitlab-ci.yml
build-prd: #7
tags:
- docker
stage: deploy-prd
before_script:
- docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
- docker rmi -f $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG || true
- docker build -f Dockerfile -t $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG .
- docker push $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG
- docker image prune -f
- 'which ssh-agent || ( yum update -y && yum install openssh-client git -y )'
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan 192.168.8.103 >> ~/.ssh/known_hosts #ssh-keyscan 从服务器收集 SSH公钥。 ssh-keyscan 是一个收集多个主机的 SSH 公共密钥的实用工具。它被设计用来帮助构建和验证 ssh_known_hosts 文件
- chmod 644 ~/.ssh/known_hosts
script:
# -tq 打印台静默不打印
- ssh -tq 192.168.8.103 << EOF
- docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
- docker rm -f $USERSERVICE_NAME || true
- docker rmi -f $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG || true
- docker run -d --name $USERSERVICE_NAME -v $USERSERVICE_NAME:/apps --privileged=true -p $USERSERVICE_RPORT:$USERSERVICE_RPORT $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG
- docker image prune -f
- exit
- EOF
only:
variables: [ $PROFILE_ACTIVE == "prd" ]
refs:
- master

多模块合并部署

根目录.gitlab-ci.yml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#stages:
# - triggers
# - triggers1
#trigger_a:
# stage: triggers
# trigger:
# include: szbxl-service/szbxl-user-service/.gitlab-ci.yml
# rules:
# - changes:
# - szbxl-service/szbxl-user-service/*
#trigger_b:
# stage: triggers1
# trigger:
# include: szbxl-getway/.gitlab-ci.yml
# rules:
# - changes:
# - szbxl-getway/*

variables: #2
DOCKER_DRIVER: overlay2
DOCKER_HOST: tcp://192.168.8.10:2375 # docker host,本地可不写
DOCKER_TLS_CERTDIR: ''
PROFILE_ACTIVE: prd #激活属性,dev/prod,从Gitlab的变量设置,这样定时部署可以动态指定
GIT_STRATEGY: clone #Initialized empty Git repository as it did in the first run of the first job, it now says Reinitialized existing Git repository.
#https://stackoverflow.com/questions/57779750/does-gitlab-runner-or-docker-cache-the-builds-directory-by-default

# 多模块.gitlab-ci.yml
stages:
- service_first-package
- service_first-dev-deploy
- service_first-prd-deploy
- service_second-package
- service_second-dev-deploy
- service_second-prd-deploy
include:
- local: service_first/.gitlab-ci.yml
- local: service_second/.gitlab-ci.yml

#include
#全局关键字。可以在本地的gitlab-ci.yml文件中引入其他地方的gitlab-ci.yml的配置文件。
#
#include:local
#可以使用include:local来引入同一个仓库同一个分支中的其他配置文件(其他配置文件必须是.yml 或者.ymal扩展名)
#include:project
#可以通过include:project 来引入同一个gitlab 服务中其他项目中的gitlab-ci.yml配置
#主要有下面几部分构成,
#
#project:表在该gitlab服务器中的项目地址
#file:一个数组,表引入该项目中的哪些文件
#ref:非必须,表该项目中的那个分支。没有就是master(main)
#include:remote
#使用include:remote 可以通过一个完整的URL地址加载远端的gitlab-ci.yml的配置文件。
#远端的配置文件使用HTTP/GTTPS 的GET请求可以获取的到

子模块一目录Dockerfile配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 基础镜像
# 不能使用openjdk:8-jre-alpine,原因如下:
# 1、会导致easy-captcha验证码生成失败。
# 2、 Docker启动会报错:ERROR: failed to launch: exec.d: failed to execute exec.d file at path '/layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/memory-calculator': exit status 1
FROM openjdk:8-jre-alpine

# 镜像作者
LABEL author="gordon"

# 指定容器内的时区
RUN echo "Asia/Shanghai" > /etc/timezone

# 环境变量
ENV MYPATH /apps/service_first/

# 创建项目存放路径
RUN mkdir -p ${MYPATH}

# 指定工作目录
WORKDIR ${MYPATH}

# 尽量采用指定路径挂载,不然会生成很多临时挂载垃圾文件
# 匿名挂载目录(宿主机位置:/var/lib/docker/volumes/xxxxxxxxxxxxxxxx)
# 查看挂载目录:docker inspect szbxl-user-service
# 查看所有挂载卷:docker volume ls
#VOLUME ${MYPATH}

# Tomcat 默认使用/tmp作为工作目录。这个命令的效果是:在宿主机的/var/lib/docker目录下创建一个临时文件并把它链接到容器中的/tmp目录
#VOLUME /tmp
#VOLUME /log

# 添加jar包到指定路径,当前目录是WORKDIR
COPY service_first/target/*.jar app.jar

# 暴露端口
#EXPOSE 802

# 启动容器执行命令
#ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","app.jar"]
#ENTRYPOINT ["sh", "-c", "java --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED -Djava.security.egd=file:/dev/./urandom -jar app.jar"]
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","app.jar"]

子模块一目录.gitlab-ci.yml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#1:需要用到的镜像
#2:必须配置的一些环境变量。如果本地可不配置 DOCKER_HOST。
#3:配置缓存,配置后,maven 下载的依赖可以被缓存起来,下次不需要重复去下载了。
#4:配置需要用到的额外的服务。docker:dind,这个貌似是用于在 docker 中运行 docker 的一种东西,在项目的构建中需要。
#有时需要在容器内执行 docker 命令,比如:在 jenkins 容器内运行 docker 命令执行构建镜像
#直接在 docker 容器内嵌套安装 docker 未免太过臃肿
#更好的办法是:容器内仅部署 docker 命令行工具(作为客户端),实际执行交由宿主机内的 docker-engine(服务器)
#先启动一个docker:dind容器A,再启动一个docker容器B,容器B指定host为A容器内的docker daemon。

#5:stages,这是 Gitlab CI 中的概念,Stages 表示构建阶段,就是一些按序执行的流程,具体执行是依赖于 Jobs 的。
#6 :定义的 Jobs 之一,用于构建 jar 包。内部又引入 maven 镜像来处理,负责执行 package 这一流程。script 为具体执行的脚本。
#7:定义的 Jobs 之一,用于构建 Docker 镜像。负责执行 deploy 这一流程。具体执行 build 和 run。only 节点表示只监控 master 分支。

image: docker:latest #1
variables: #2
#根目录已提供
#DOCKER_DRIVER: overlay2
#DOCKER_HOST: tcp://192.168.8.10:2375 # docker host,本地可不写
#DOCKER_TLS_CERTDIR: ''
#PROFILE_ACTIVE: prd
FIRST_TAG: ':1.0' # 镜像版本号
FIRST_NAME: service_first #镜像名称
FIRST_RPORT: 8888 #镜像端口 容器内的端口与其不一致
FIRST_DIRECTORY: service_first #模块目录

cache: #3
key: m2-repo
paths:
- .m2/repository
services: #4
- docker:dind
stages: #5
- service_first-package
- service_first-dev-deploy
- service_first-prd-deploy
maven-package:service_first: #6
image: maven:3.5.0-jdk-8
tags:
- maven
stage: service_first-package
script:
#mvn clean package -pl service_first -am -Dmaven.test.skip=true -pl --projects <arg> 构建制定的模块,模块间用逗号分隔;-am --also-make 同时构建所列模块的依赖模块;
- mvn clean package -P $PROFILE_ACTIVE -Dmaven.test.skip=true -pl $FIRST_DIRECTORY -am
artifacts:
paths:
- $FIRST_DIRECTORY/target/*.jar
build-dev:service_first:
tags:
- docker
stage: service_first-dev-deploy
script:
- docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
- docker rm -f $FIRST_NAME || true
- docker rmi -f $CI_REGISTRY_IMAGE/$FIRST_NAME-$PROFILE_ACTIVE$FIRST_TAG || true
- docker build -f $FIRST_DIRECTORY/Dockerfile -t $CI_REGISTRY_IMAGE/$FIRST_NAME-$PROFILE_ACTIVE$FIRST_TAG .
- docker push $CI_REGISTRY_IMAGE/$FIRST_NAME-$PROFILE_ACTIVE$FIRST_TAG
- docker run -d --name $FIRST_NAME -v /apps/$FIRST_DIRECTORY:/apps/$FIRST_DIRECTORY/log --privileged=true -p $FIRST_RPORT:$FIRST_RPORT $CI_REGISTRY_IMAGE/$FIRST_NAME-$PROFILE_ACTIVE$FIRST_TAG
- docker image prune -f
only: #限制作业在什么时候创建,定义了哪些分支或标签(branches and tags)的作业会运行
refs: #分支
- dev
#changes: # 下面的文件中任一文件发生改变
# - .gitlab-ci.yml
build-prd:service_first: #7
tags:
- docker
stage: service_first-prd-deploy
before_script:
- docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
- docker rmi -f $CI_REGISTRY_IMAGE/$FIRST_NAME-$PROFILE_ACTIVE$FIRST_TAG || true
- docker build -f $FIRST_DIRECTORY/Dockerfile -t $CI_REGISTRY_IMAGE/$FIRST_NAME-$PROFILE_ACTIVE$FIRST_TAG .
- docker push $CI_REGISTRY_IMAGE/$FIRST_NAME-$PROFILE_ACTIVE$FIRST_TAG
- docker image prune -f
- 'which ssh-agent || ( yum update -y && yum install openssh-client git -y )'
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan 192.168.8.103 >> ~/.ssh/known_hosts #ssh-keyscan 从服务器收集 SSH公钥。 ssh-keyscan 是一个收集多个主机的 SSH 公共密钥的实用工具。它被设计用来帮助构建和验证 ssh_known_hosts 文件
- chmod 644 ~/.ssh/known_hosts
script:
# -tq 打印台静默不打印
- ssh -tq 192.168.8.103 << EOF
- docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
- docker rm -f $FIRST_NAME || true
- docker rmi -f $CI_REGISTRY_IMAGE/$FIRST_NAME-$PROFILE_ACTIVE$FIRST_TAG || true
- docker run -d --name $FIRST_NAME -v /apps/$FIRST_DIRECTORY:/apps/$FIRST_DIRECTORY/log --privileged=true -p $FIRST_RPORT:$FIRST_RPORT $CI_REGISTRY_IMAGE/$FIRST_NAME-$PROFILE_ACTIVE$FIRST_TAG
- docker image prune -f
- exit
- EOF
only:
variables: [ $PROFILE_ACTIVE == "prd" ]
refs:
- main

子模块二目录Dockerfile配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 基础镜像
# 不能使用openjdk:8-jre-alpine,原因如下:
# 1、会导致easy-captcha验证码生成失败。
# 2、 Docker启动会报错:ERROR: failed to launch: exec.d: failed to execute exec.d file at path '/layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/memory-calculator': exit status 1
FROM openjdk:8-jre-alpine

# 镜像作者
LABEL author="gordon"

# 指定容器内的时区
RUN echo "Asia/Shanghai" > /etc/timezone

# 环境变量
ENV MYPATH /apps/service_second/

# 创建项目存放路径
RUN mkdir -p ${MYPATH}

# 指定工作目录
WORKDIR ${MYPATH}

# 尽量采用指定路径挂载,不然会生成很多临时挂载垃圾文件
# 匿名挂载目录(宿主机位置:/var/lib/docker/volumes/xxxxxxxxxxxxxxxx)
# 查看挂载目录:docker inspect szbxl-user-service
# 查看所有挂载卷:docker volume ls
#VOLUME ${MYPATH}

# Tomcat 默认使用/tmp作为工作目录。这个命令的效果是:在宿主机的/var/lib/docker目录下创建一个临时文件并把它链接到容器中的/tmp目录
#VOLUME /tmp
#VOLUME /log

# 添加jar包到指定路径
COPY service_second/target/*.jar app.jar

# 暴露端口
#EXPOSE 802

# 启动容器执行命令
#ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","app.jar"]
#ENTRYPOINT ["sh", "-c", "java --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED -Djava.security.egd=file:/dev/./urandom -jar app.jar"]
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","app.jar"]

子模块二目录.gitlab-ci.yml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#1:需要用到的镜像
#2:必须配置的一些环境变量。如果本地可不配置 DOCKER_HOST。
#3:配置缓存,配置后,maven 下载的依赖可以被缓存起来,下次不需要重复去下载了。
#4:配置需要用到的额外的服务。docker:dind,这个貌似是用于在 docker 中运行 docker 的一种东西,在项目的构建中需要。
#有时需要在容器内执行 docker 命令,比如:在 jenkins 容器内运行 docker 命令执行构建镜像
#直接在 docker 容器内嵌套安装 docker 未免太过臃肿
#更好的办法是:容器内仅部署 docker 命令行工具(作为客户端),实际执行交由宿主机内的 docker-engine(服务器)
#先启动一个docker:dind容器A,再启动一个docker容器B,容器B指定host为A容器内的docker daemon。

#5:stages,这是 Gitlab CI 中的概念,Stages 表示构建阶段,就是一些按序执行的流程,具体执行是依赖于 Jobs 的。
#6 :定义的 Jobs 之一,用于构建 jar 包。内部又引入 maven 镜像来处理,负责执行 package 这一流程。script 为具体执行的脚本。
#7:定义的 Jobs 之一,用于构建 Docker 镜像。负责执行 deploy 这一流程。具体执行 build 和 run。only 节点表示只监控 master 分支。

image: docker:latest #1
variables: #2
#根目录已提供
#DOCKER_DRIVER: overlay2
#DOCKER_HOST: tcp://192.168.8.10:2375 # docker host,本地可不写
#DOCKER_TLS_CERTDIR: ''
#PROFILE_ACTIVE: prd
SECOND_TAG: ':1.0' # 镜像版本号
SECOND_NAME: service_second #镜像名称
SECOND_RPORT: 9999 #镜像端口 容器内的端口与其不一致
SECOND_DIRECTORY: service_second #模块目录

cache: #3
key: m2-repo
paths:
- .m2/repository
services: #4
- docker:dind
stages: #5
- service_second-package
- service_second-dev-deploy
- service_second-prd-deploy
maven-package:service_second: #6
image: maven:3.5.0-jdk-8
tags:
- maven
stage: service_second-package
script:
#mvn clean package -pl service_first -am -Dmaven.test.skip=true -pl --projects <arg> 构建制定的模块,模块间用逗号分隔;-am --also-make 同时构建所列模块的依赖模块;
- mvn clean package -P $PROFILE_ACTIVE -Dmaven.test.skip=true -pl $SECOND_DIRECTORY -am
artifacts:
paths:
- $SECOND_DIRECTORY/target/*.jar
build-dev:service_second:
tags:
- docker
stage: service_second-dev-deploy
script:
- docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
- docker rm -f $SECOND_NAME || true
- docker rmi -f $CI_REGISTRY_IMAGE/$SECOND_NAME-$PROFILE_ACTIVE$SECOND_TAG || true
- docker build -f $SECOND_DIRECTORY/Dockerfile -t $CI_REGISTRY_IMAGE/$SECOND_NAME-$PROFILE_ACTIVE$SECOND_TAG .
- docker push $CI_REGISTRY_IMAGE/$SECOND_NAME-$PROFILE_ACTIVE$SECOND_TAG
- docker run -d --name $SECOND_NAME -v /apps/$SECOND_DIRECTORY:/apps/$SECOND_DIRECTORY/log --privileged=true -p $SECOND_RPORT:$SECOND_RPORT $CI_REGISTRY_IMAGE/$SECOND_NAME-$PROFILE_ACTIVE$SECOND_TAG
- docker image prune -f
only: #限制作业在什么时候创建,定义了哪些分支或标签(branches and tags)的作业会运行
refs: #分支
- dev
#changes: # 下面的文件中任一文件发生改变
# - .gitlab-ci.yml
build-prd:service_second: #7
tags:
- docker
stage: service_second-prd-deploy
before_script:
- docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
- docker rmi -f $CI_REGISTRY_IMAGE/$SECOND_NAME-$PROFILE_ACTIVE$SECOND_TAG || true
- docker build -f $SECOND_DIRECTORY/Dockerfile -t $CI_REGISTRY_IMAGE/$SECOND_NAME-$PROFILE_ACTIVE$SECOND_TAG .
- docker push $CI_REGISTRY_IMAGE/$SECOND_NAME-$PROFILE_ACTIVE$SECOND_TAG
- docker image prune -f
- 'which ssh-agent || ( yum update -y && yum install openssh-client git -y )'
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan 192.168.8.103 >> ~/.ssh/known_hosts #ssh-keyscan 从服务器收集 SSH公钥。 ssh-keyscan 是一个收集多个主机的 SSH 公共密钥的实用工具。它被设计用来帮助构建和验证 ssh_known_hosts 文件
- chmod 644 ~/.ssh/known_hosts
script:
# -tq 打印台静默不打印
- ssh -tq 192.168.8.103 << EOF
- docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
- docker rm -f $SECOND_NAME || true
- docker rmi -f $CI_REGISTRY_IMAGE/$SECOND_NAME-$PROFILE_ACTIVE$SECOND_TAG || true
- docker run -d --name $SECOND_NAME -v /apps/$SECOND_DIRECTORY:/apps/$SECOND_DIRECTORY/log --privileged=true -p $SECOND_RPORT:$SECOND_RPORT $CI_REGISTRY_IMAGE/$SECOND_NAME-$PROFILE_ACTIVE$SECOND_TAG
- docker image prune -f
- exit
- EOF
only:
variables: [ $PROFILE_ACTIVE == "prd" ]
refs:
- main

多模块

部署过程中遇到的问题

Docker.assets/image-20230618211704013.png
1
2
#重新加载daemon
systemctl daemon-reload

下载的镜像重复拉取

The image pull policy: never, if-not-present or always (default).

never是从不从远端拉镜像,只用本地。if-not-present 是优先本地,然后是从网络拉取镜像。always 是从远端拉取镜像。

1
2
3
解决办法:配置先从本地拉取。
vi /etc/gitlab-runner/config.toml 添加:
pull_policy = "if-not-present"
Docker.assets/image-20230618215738518.png Docker.assets/image-20230618220336436.png

maven下载慢

maven安装到宿主机,在runner里面进行挂载,这样可以重复使用,也可以使用国内的镜像源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

[[runners]]
name = "Docker Maven Runner"
url = "http://192.168.8.10:2280"
token = "mPciztaPrpZsYSsWuRAX"
executor = "docker"
[runners.custom_build_dir]
[runners.cache]
[runners.cache.s3]
[runners.cache.gcs]
[runners.cache.azure]
[runners.docker]
tls_verify = false
image = "maven:3.5.0-jdk-8"
privileged = true
pull_policy = "if-not-present"
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cache", "/export/server/bigdata-stack/maven/apache-maven-3.9.2:/root/.m2"]
shm_size = 0

[[runners]]
name = "Docker Build Runner"
url = "http://192.168.8.10:2280"
token = "pRuA3NL6tsJsBgq-Ps65"
executor = "docker"
[runners.custom_build_dir]
[runners.cache]
[runners.cache.s3]
[runners.cache.gcs]
[runners.cache.azure]
[runners.docker]
tls_verify = false
image = "docker:latest"
privileged = true
pull_policy = "if-not-present"
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cache", "/export/server/bigdata-stack/maven/apache-maven-3.9.2:/root/.m2"]
shm_size = 0
Docker.assets/image-20230618220813431.png

10.Docker 整合Sonar进行代码质量检查,统计单元测试覆盖率

单模块的.yml配置修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#1:需要用到的镜像
#2:必须配置的一些环境变量。如果本地可不配置 DOCKER_HOST。
#3:配置缓存,配置后,maven 下载的依赖可以被缓存起来,下次不需要重复去下载了。
#4:配置需要用到的额外的服务。docker:dind,这个貌似是用于在 docker 中运行 docker 的一种东西,在项目的构建中需要。
#有时需要在容器内执行 docker 命令,比如:在 jenkins 容器内运行 docker 命令执行构建镜像
#直接在 docker 容器内嵌套安装 docker 未免太过臃肿
#更好的办法是:容器内仅部署 docker 命令行工具(作为客户端),实际执行交由宿主机内的 docker-engine(服务器)
#先启动一个docker:dind容器A,再启动一个docker容器B,容器B指定host为A容器内的docker daemon。

#5:stages,这是 Gitlab CI 中的概念,Stages 表示构建阶段,就是一些按序执行的流程,具体执行是依赖于 Jobs 的。
#6 :定义的 Jobs 之一,用于构建 jar 包。内部又引入 maven 镜像来处理,负责执行 package 这一流程。script 为具体执行的脚本。
#7:定义的 Jobs 之一,用于构建 Docker 镜像。负责执行 deploy 这一流程。具体执行 build 和 run。only 节点表示只监控 master 分支。

image: docker:latest #1
variables: #2
DOCKER_DRIVER: overlay2
DOCKER_HOST: tcp://192.168.8.10:2375 # docker host,本地可不写
DOCKER_TLS_CERTDIR: ''
USERSERVICE_TAG: ':1.0' # 镜像版本号
USERSERVICE_NAME: cicd #镜像名称
USERSERVICE_RPORT: 6666 #镜像端口 容器内的端口与其不一致
USERSERVICE_DIRECTORY: cicd #模块目录
PROFILE_ACTIVE: prd

# sonner scanner 安装目录
SCANNER_HOME: "/export/server/sonar-scanner"
# 扫描代码路径
SCAN_DIR: "src"
cache: #3
key: m2-repo
paths:
- .m2/repository
services: #4
- docker:dind
stages: #5
- package
- sonarqube-check
- deploy-dev
- deploy-prd
maven-package: #6
image: maven:3.5.0-jdk-8
tags:
- maven
stage: package
script:
- mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent install dependency:copy-dependencies
-Dmaven.test.failure.ignore=true package -P $PROFILE_ACTIVE
# Maven编译,所以会有Jar包产物,这里定义产物的过期时间
artifacts:
expire_in: 1 days
paths:
- target/
sonarqube-check:
image:
name: sonarsource/sonar-scanner-cli:latest
entrypoint: [""]
tags:
- sonar
stage: sonarqube-check
needs: [maven-package]
dependencies:
- maven-package
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache
GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task
cache:
key: "${CI_JOB_NAME}"
paths:
- .sonar/cache
script:
- sonar-scanner
-Dsonar.projectKey=sonarqube-check
-Dsonar.sources=src/main/
-Dsonar.tests=src/test/
-Dsonar.coverage.exclusions=src/main/java/com/gordon/cicd/CicdApplication.java
-Dsonar.scm.disabled=true
-Dsonar.language=java
-Dsonar.sourceEncoding=UTF-8
-Dsonar.host.url=http://192.168.8.10:9000
-Dsonar.login=b95cb41cb57230c28a79a177a9f0fc3bddf02a79
-Dsonar.java.binaries=target/classes
-Dsonar.java.test.binaries=target/test-classes
-Dsonar.java.libraries=target/dependency
-Dsonar.java.test.libraries=target/dependency
-Dsonar.java.surefire.report=target/surefire-reports
-Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
#统计结果输出
- awk -F"," '{ instructions += $4 + $5; covered += $5 } END { print 100 *covered/instructions, "% covered" }' target/site/jacoco/jacoco.csv

allow_failure: false
only:
- merge_requests
- master # or the name of your main branch
- develop
artifacts:
paths:
- target/

build-dev:
tags:
- docker
stage: deploy-dev
needs: [sonarqube-check]
script:
- docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
- docker rm -f $USERSERVICE_NAME || true
- docker rmi -f $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG || true
- docker build -f Dockerfile -t $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG .
- docker push $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG
- docker run -d --name $USERSERVICE_NAME -v $USERSERVICE_NAME:/apps --privileged=true -p $USERSERVICE_RPORT:$USERSERVICE_RPORT $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG
- docker image prune -f
only: #限制作业在什么时候创建,定义了哪些分支或标签(branches and tags)的作业会运行
refs: #分支
- dev
#changes: # 下面的文件中任一文件发生改变
# - .gitlab-ci.yml
build-prd: #7
tags:
- docker
stage: deploy-prd
needs: [sonarqube-check]
before_script:
- docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
- docker rmi -f $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG || true
- docker build -f Dockerfile -t $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG .
- docker push $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG
- docker image prune -f
- 'which ssh-agent || ( yum update -y && yum install openssh-client git -y )'
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan 192.168.8.103 >> ~/.ssh/known_hosts #ssh-keyscan 从服务器收集 SSH公钥。 ssh-keyscan 是一个收集多个主机的 SSH 公共密钥的实用工具。它被设计用来帮助构建和验证 ssh_known_hosts 文件
- chmod 644 ~/.ssh/known_hosts
script:
# -tq 打印台静默不打印
- ssh -tq 192.168.8.103 << EOF
- docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
- docker rm -f $USERSERVICE_NAME || true
- docker rmi -f $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG || true
- docker run -d --name $USERSERVICE_NAME -v $USERSERVICE_NAME:/apps --privileged=true -p $USERSERVICE_RPORT:$USERSERVICE_RPORT $CI_REGISTRY_IMAGE/$USERSERVICE_NAME-$PROFILE_ACTIVE$USERSERVICE_TAG
- docker image prune -f
- exit
- EOF
only:
variables: [ $PROFILE_ACTIVE == "prd" ]
refs:
- master
Docker.assets/image-20230621061204614.png Docker.assets/image-20230621061339592.png

为了readme能查看结果,pom中jacoco做了exclude

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<groupId>com.gordon</groupId>
<artifactId>cicd</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>cicd</name>
<description>Demo project for Spring Boot</description>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

</dependencies>


<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.7.0.1746</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<!--覆盖率测试-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.21.0</version>
<configuration>
<testFailureIgnore>true</testFailureIgnore>
<argLine>@{argLine} -Dfile.encoding=utf-8</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<configuration>
<excludes>
<exclude>**/CicdApplication.class</exclude>
</excludes>
</configuration>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>


</plugins>
</build>

</project>

覆盖率展示格式化

1
\d+.\d+ \% covered
Docker.assets/image-20230621132854431.png Docker.assets/image-20230621132548570.png

多模块的子模块配置更新如下:

这里提供一种思路,新建一个模块把其他模块作为依赖引入,然后计算总的覆盖率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|   .gitlab-ci.yml
| pom.xml
| README.md
+---report-aggregate
| .gitlab-ci.yml
| pom.xml
|
+---service_first
| | .gitlab-ci.yml
| | Dockerfile
| | pom.xml
| |
| \---src
| +---main
| | +---java
| | | \---com
| | | \---gordon
| | | | FirstApplication.java
| | | |
| | | \---controller
| | | TestController.java
| | |
| | \---resources
| | application.properties
| |
| \---test
| \---java
| \---com
| \---gordon
| \---controller
| TestControllerTest.java
|
\---service_second
| .gitlab-ci.yml
| Dockerfile
| pom.xml
|
\---src
+---main
| +---java
| | \---com
| | \---gordon
| | | SecondApplication.java
| | |
| | \---controller
| | TestController.java
| |
| \---resources
| application.properties
|
\---test
\---java
\---com
\---gordon
\---controller
TestControllerTest.java

创建第三模块

pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cicd_muil</artifactId>
<groupId>com.gordon</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>report-aggregate</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>com.gordon</groupId>
<artifactId>service_first</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.gordon</groupId>
<artifactId>service_second</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<configuration>
<excludes>
<exclude>**/FirstApplication.class</exclude>
<exclude>**/SecondApplication.class</exclude>
</excludes>
</configuration>
<executions>
<execution>
<id>jacoco-report-aggregate</id>
<!-- 触发的maven 命令 -->
<phase>install</phase>
<goals>
<goal>report-aggregate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

.gitlab-ci.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
image: docker:latest  #1
variables: #2
#根目录已提供
#DOCKER_DRIVER: overlay2
#DOCKER_HOST: tcp://192.168.8.10:2375 # docker host,本地可不写
#DOCKER_TLS_CERTDIR: ''
#PROFILE_ACTIVE: prd
REPORT_NAME: report-aggregate #镜像名称
REPORT_DIRECTORY: report-aggregate #模块目录

cache: #3
key: m2-repo
paths:
- .m2/repository
services: #4
- docker:dind
stages: #5
- report-aggregate

maven-package:report-aggregate: #6
image: maven:3.5.0-jdk-8
tags:
- maven
stage: report-aggregate
script:
#mvn clean package -pl service_first -am -Dmaven.test.skip=true -pl --projects <arg> 构建制定的模块,模块间用逗号分隔;-am --also-make 同时构建所列模块的依赖模块;
- mvn clean install dependency:copy-dependencies -Dmaven.test.failure.ignore=true -pl $REPORT_DIRECTORY -am
- awk -F"," '{ instructions += $4 + $5; covered += $5 } END { print 100 *covered/instructions, "% covered" }' $REPORT_DIRECTORY/target/site/jacoco-aggregate/jacoco.csv
artifacts:
paths:
- $REPORT_DIRECTORY/target/

查看pipeline运行情况以及扫描结果

Docker.assets/image-20230620154235530.png

test
Docker.assets/image-20230621154703183.png