1、Docker 概述

Docker 包括三个基本概念:

  • 镜像(Image):Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统。
  • ​容器(Container)​:镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
  • ​仓库(Repository):仓库可看成一个代码控制中心,用来保存镜像。

Docker 使用客户端-服务器 (C/S) 架构模式,使用远程API来管理和创建Docker容器。

Docker 容器通过 Docker 镜像来创建。

容器与镜像的关系类似于面向对象编程中的对象与类。

Docker面向对象
容器对象
镜像

jiagou

概念说明
Docker 镜像(Images)Docker 镜像是用于创建 Docker 容器的模板,比如 Ubuntu 系统。
Docker 容器(Container)容器是独立运行的一个或一组应用,是镜像运行时的实体。
Docker 客户端(Client)Docker 客户端通过命令行或者其他工具使用 Docker SDK 与 Docker 的守护进程通信。
Docker 主机(Host)一个物理或者虚拟的机器用于执行 Docker 守护进程和容器。
Docker RegistryDocker 仓库用来保存镜像,可以理解为代码控制中的代码仓库。一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。
Docker MachineDocker Machine是一个简化Docker安装的命令行工具。

2、安装 Docker(默认2375端口)

2.1、Docker

  1. Docker 要求 CentOS 系统的内核版本高于 3.10 ,运行以下命令查看自己的系统内核:uname -r
  2. 更新yum:yum update
  3. 安装需要的软件包, yum-util 提供yum-config-manager功能,另外两个是devicemapper驱动依赖的:yum install -y yum-utils device-mapper-persistent-data lvm2
  4. 卸载旧版本(如果安装过旧版本的话):yum remove docker docker-common docker-selinux docker-engine
  5. 设置yum源:yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
  6. 查看仓库中的docker版本并选择版本安装:yum list docker-ce --showduplicates | sort -r
  7. 安装最新稳定版:yum install docker-ce
  8. 启动docker并加入开机启动:
    systemctl start docker
    systemctl enable docker
  9. 验证是否安装&启动成功:docker version

2.2、开启 TCP 远程访问

1、编辑docker.service:vim /usr/lib/systemd/system/docker.service

2、修改ExecStart属性,在后面添加 -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

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

3、刷新配置:systemctl daemon-reload

4、重启服务:systemctl restart docker

5、检测成功开启docker的tcp远程访问:http://192.168.126.133:2375/version

2.3、docker-compose 离线

1、到GitHub上下载docker-compose
https://github.com/docker/compose/releases,选择 docker-compose-linux-x86_64

2、上传到linux目录,到 /usr/local/bin下,重命名 mv docker-compose-linux-x86_64 docker-compose,修改权限 chmod +x /usr/local/bin/docker-compose

3、查看版本 docker-compose version

2.4、docker-compose 在线

  • 下载2.x安装包:
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}

mkdir -p $DOCKER_CONFIG/cli-plugins

curl -SL  https://github.com/docker/compose/releases/download/v2.19.1/docker-compose-linux-x86_64 -o $DOCKER_CONFIG/cli-plugins/docker-compose
  • 更改执行权限:
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
  • 测试安装是否成功:
docker compose version

3、Docker 安装软件

3.1、安装 redis

  • 下载:docker pull redis:latest
  • 创建数据卷:docker volume create redis_data
  • 启动:
docker run -itd -p 6379:6379 --name redis --restart always -v redis_data:/data redis

3.2、安装 mysql

  • 下载:docker pull mysql:8.0.31
  • 创建数据卷:docker volume create mysql_data
  • 启动:
docker run -itd --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -v mysql_data:/data mysql:8.0.31

3.3、安装 minion

  • 下载:docker pull minio/minio
  • 创建数据卷:docker volume create minio_data
  • 启动:
docker run --name minio -p 9001:9001 -p 9090:9090 -d --restart=always -e "MINIO_ROOT_USER=lilinhan" -e "MINIO_ROOT_PASSWORD=Llh020920." -v minio_data:/data minio/minio server /data --console-address ":9090" -address ":9001"
  • 访问:http://192.168.126.133:9090

3.4、安装 nacos

  • 下载:docker pull nacos/nacos-server
  • 启动:
docker run -d --name nacos -p 8848:8848 -e PREFER_HOST_MODE=hostname -e MODE=standalone nacos/nacos-server
  • 访问:http://192.168.126.133:8848/nacos
  • 查看当前运行的容器(找nacos):docker ps
  • 进入容器的内部:docker exec -it 91ec4fe31468 /bin/bash
  • 修改配置文件:vim conf/application.properties
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://192.168.126.133:3306/nacos?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&nullCatalogMeansCurrent=true
db.user=root
db.password=123456
  • 退出容器:exit
  • 重启容器:docker restart 91ec4fe31468

3.5、安装 portainer-ce

  • 下载:docker pull portainer/portainer-ce
  • 创建数据卷:docker volume create portainer_data
  • 启动:
docker run -d -p 8000:8000 -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce
  • 访问:http://192.168.126.133:9000
  • 第一次访问需要创建用户,密码长度必须12位及以上,详情可以查看官方文档*

4、集成 IDEA

4.1、连接 Docker

  • 进到setting中搜索docker,确保已经安装docker插件
  • 再进到docker设置界面,新建连接,选择tcp连接,输入 tcp://虚拟机ip:2375,等待下方显示 connection successful即可

chajian

  • 在侧边栏选择services,就能看到刚刚连接的docker详情了

docker-service

4.2、连接 Docker Registry

  • 进到docker设置界面,展开设置,在Registry选项选择 Docker Hub,输入用户名和密码(需要注意的是,国内访问docker官方较慢,建议挂梯子访问,不然一直连接超时),等待下方显示 connect successful 即可
  • 在侧边栏选择services,就能看到刚刚连接的 Docker Registry 详情了
  • 可以把适合自己项目的镜像文件上传到自己账号的仓库里,以便下次直接pull拉取使用

4.3、Dockerfile 定制镜像

4.3.1、概念简介
  • 我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
  • Dockerfile 是一个文本文件,其内包含了一条条的 ​指令(Instruction)​,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
4.3.2、FROM 指定基础镜像
  • 所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。而 FROM 就是指定 ​基础镜像​,因此一个 DockerfileFROM 是必备的指令,并且必须是第一条指令。
  • Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginxredismongomysqlhttpdphptomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 nodeopenjdkpythonrubygolang 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
  • 如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntudebiancentosfedoraalpine 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
  • 除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
  • 如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
4.3.3、RUN 执行命令
  • RUN 指令是用来执行命令行命令的。由于命令行的强大能力,RUN 指令在定制镜像时是最常用的指令之一。其格式有两种:

    • shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。
    • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。
  • 既然 RUN 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:
FROM debian:stretch

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install
  • 之前说过,Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。
  • 而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。
  • Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。
  • 上面的 Dockerfile 正确的写法应该是这样:
FROM debian:stretch

RUN set -x; buildDeps='gcc libc6-dev make wget' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps
  • 首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN 一一对应不同的命令,而是仅仅使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。
  • 并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以及行首 # 进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。
  • 此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。
4.3.4、COPY 复制文件
  • 格式:
COPY [--chown=<user>:<group>] <源路径>... <目标路径>

COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
  • RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。
  • COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。比如:COPY package.json /usr/src/app/
  • <源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:
COPY hom* /mydir/
COPY hom?.txt /mydir/
  • <目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
  • 此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。
  • 在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。
COPY --chown=55:mygroup files* /mydir/
COPY --chown=bin files* /mydir/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/
  • 如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。
4.3.5、ADD 更高级的复制文件
  • ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。
  • 比如 <源路径> 可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去。下载后的文件权限自动设置为 600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。
  • 如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。
  • 在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu 中:
FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...
  • 但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD 命令了。
  • 另外需要注意的是,ADD 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。
  • 因此在 COPYADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD
4.3.6、CMD 容器启动命令
  • CMD 指令的格式和 RUN 相似,也是两种格式:
shell 格式:CMD <命令>

exec 格式:CMD ["可执行文件", "参数1", "参数2"...]

参数列表格式:`CMD ["参数1", "参数2"...]`。在指定了 `ENTRYPOINT` 指令后,用 `CMD` 指定具体的参数。
  • 介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。
  • 在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu 镜像默认的 CMD/bin/bash,如果我们直接 docker run -it ubuntu 的话,会直接进入 bash。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release。这就是用 cat /etc/os-release 命令替换了默认的 /bin/bash 命令了,输出了系统版本信息。
  • 在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。
  • 如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:CMD echo $HOME
  • 在实际执行中,会将其变更为:CMD [ "sh", "-c", "echo $HOME" ]
  • 这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。
  • Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 systemd 去启动后台服务,容器内没有后台服务的概念。
  • 对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。
  • 正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:CMD ["nginx", "-g", "daemon off;"]
4.3.7、ENTRYPOINT 入口点
  • ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。
  • ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。
  • 当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:<ENTRYPOINT> "<CMD>"

让镜像变成像命令一样使用:

  • 假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:
FROM ubuntu:18.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://myip.ipip.net" ]
  • 假如我们使用 docker build -t myip . 来构建镜像的话,如果我们需要查询当前公网 IP,只需要执行:
$ docker run myip
当前 IP:61.148.226.66 来自:北京市 联通
  • 嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 CMD 中可以看到实质的命令是 curl,那么如果我们希望显示 HTTP 头信息,就需要加上 -i 参数。那么我们可以直接加 -i 参数给 docker run myip 么?
$ docker run myip -i
docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".
  • 我们可以看到可执行文件找不到的报错,executable file not found。之前我们说过,跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的 CMD,而不是添加在原来的 curl -s http://myip.ipip.net 后面。而 -i 根本不是命令,所以自然找不到。
  • 那么如果我们希望加入 -i 这参数,我们就必须重新完整的输入这个命令:docker run myip curl -s http://myip.ipip.net -i
  • 这显然不是很好的解决方案,而使用 ENTRYPOINT 就可以解决这个问题。现在我们重新用 ENTRYPOINT 来实现这个镜像:
FROM ubuntu:18.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]
  • 这次我们再来尝试直接使用 docker run myip -i
$ docker run myip
当前 IP:61.148.226.66 来自:北京市 联通

$ docker run myip -i
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 22 Nov 2016 05:12:40 GMT
Content-Type: text/html; charset=UTF-8
Vary: Accept-Encoding
X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
X-Cache: MISS from cache-2
X-Cache-Lookup: MISS from cache-2:80
X-Cache: MISS from proxy-2_6
Transfer-Encoding: chunked
Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
Connection: keep-alive

当前 IP:61.148.226.66 来自:北京市 联通
  • 可以看到,这次成功了。这是因为当存在 ENTRYPOINT 后,CMD 的内容将会作为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,因此会作为参数传给 curl,从而达到了我们预期的效果
4.3.8、ENV 设置环境变量

格式有两种:

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

4.3.9、ARG 构建参数

格式:ARG <参数名>[=<默认值>]

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。

灵活的使用 ARG 指令,能够在不修改 Dockerfile 的情况下,构建出不同的镜像。

ARG 指令有生效范围,如果在 FROM 指令之前指定,那么只能用于 FROM 指令中。

ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

RUN set -x ; echo ${DOCKER_USERNAME}

使用上述 Dockerfile 会发现无法输出 ${DOCKER_USERNAME} 变量的值,要想正常输出,你必须在 FROM 之后再次指定 ARG

# 只在 FROM 中生效
ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

# 要想在 FROM 之后使用,必须再次指定
ARG DOCKER_USERNAME=library

RUN set -x ; echo ${DOCKER_USERNAME}

对于多阶段构建,尤其要注意这个问题:

# 这个变量在每个 FROM 中都生效
ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

RUN set -x ; echo 1

FROM ${DOCKER_USERNAME}/alpine

RUN set -x ; echo 2
4.3.10、VOLUME 定义匿名卷

格式为:

  • VOLUME ["<路径1>", "<路径2>"...]
  • VOLUME <路径>

之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节我们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

VOLUME /data

这里的 /data 目录就会在容器运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行容器时可以覆盖这个挂载设置。比如:

docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

4.3.11、EXPOSE 暴露端口

格式为 EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

4.3.12、WORKDIR 指定工作目录

格式为 WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。

之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:

RUN cd /app
RUN echo "hello" > world.txt

如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是 hello。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。

因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。

WORKDIR /app

RUN echo "hello" > world.txt

如果你的 WORKDIR 指令使用的相对路径,那么所切换的路径与之前的 WORKDIR 有关:

WORKDIR /a
WORKDIR b
WORKDIR c

RUN pwd

RUN pwd 的工作目录为 /a/b/c

4.3.13、USER 指定当前用户

格式:USER <用户名>[:<用户组>]

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。

注意,USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

如果以 root 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su 或者 sudo,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu

# 建立 redis 用户,并使用 gosu 换另一个用户执行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \
    && chmod +x /usr/local/bin/gosu \
    && gosu nobody true
# 设置 CMD,并以另外的用户执行
CMD [ "exec", "gosu", "redis", "redis-server" ]
4.3.14、HEALTHCHECK 健康检查

格式:

  • HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
  • HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。

在没有 HEALTHCHECK 指令前,Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。在 1.12 以前,Docker 不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。

而自 1.12 之后,Docker 提供了 HEALTHCHECK 指令,通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。

当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy

HEALTHCHECK 支持下列选项:

  • --interval=<间隔>:两次健康检查的间隔,默认为 30 秒;
  • --timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
  • --retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。

CMD, ENTRYPOINT 一样,HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。

HEALTHCHECK [选项] CMD 后面的命令,格式和 ENTRYPOINT 一样,分为 shell 格式,和 exec 格式。命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留,不要使用这个值。

4.3.15、ONBUILD 为他人作嫁衣裳

格式:ONBUILD <其它指令>

ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。

Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。

假设我们要制作 Node.js 所写的应用的镜像。我们都知道 Node.js 使用 npm 进行包管理,所有依赖、配置、启动信息等会放到 package.json 文件里。在拿到程序代码后,需要先进行 npm install 才可以获得所有需要的依赖。然后就可以通过 npm start 来启动应用。因此,一般来说会这样写 Dockerfile

FROM node:slim
RUN mkdir /app
WORKDIR /app
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/
CMD [ "npm", "start" ]

把这个 Dockerfile 放到 Node.js 项目的根目录,构建好镜像后,就可以直接拿来启动容器运行。但是如果我们还有第二个 Node.js 项目也差不多呢?好吧,那就再把这个 Dockerfile 复制到第二个项目里。那如果有第三个项目呢?再复制么?文件的副本越多,版本控制就越困难,让我们继续看这样的场景维护的问题。

这里我们把项目相关的构建指令拿出来,放到子项目里去。假设这个基础镜像的名字为 my-node 的话,各个项目内的自己的 Dockerfile 就变为:

FROM my-node
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/

基础镜像变化后,各个项目都用这个 Dockerfile 重新构建镜像,会继承基础镜像的更新。

那么,问题解决了么?没有。准确说,只解决了一半。如果这个 Dockerfile 里面有些东西需要调整呢?比如 npm install 都需要加一些参数,那怎么办?这一行 RUN 是不可能放入基础镜像的,因为涉及到了当前项目的 ./package.json,难道又要一个个修改么?所以说,这样制作基础镜像,只解决了原来的 Dockerfile 的前4条指令的变化问题,而后面三条指令的变化则完全没办法处理。

ONBUILD 可以解决这个问题。让我们用 ONBUILD 重新写一下基础镜像的 Dockerfile:

FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]

这次我们回到原始的 Dockerfile,但是这次将项目相关的指令加上 ONBUILD,这样在构建基础镜像的时候,这三行并不会被执行。然后各个项目的 Dockerfile 就变成了简单地:

FROM my-node

是的,只有这么一行。当在各个项目目录中,用这个只有一行的 Dockerfile 构建镜像时,之前基础镜像的那三行 ONBUILD 就会开始执行,成功的将当前项目的代码复制进镜像、并且针对本项目执行 npm install,生成应用镜像。

4.3.16、LABEL 为镜像添加元数据

LABEL 指令用来给镜像以键值对的形式添加一些元数据(metadata)。

LABEL <key>=<value> <key>=<value> <key>=<value> ...

我们还可以用一些标签来申明镜像的作者、文档地址等:

LABEL org.opencontainers.image.authors="yeasy"

LABEL org.opencontainers.image.documentation="https://yeasy.gitbooks.io"

具体可以参考

4.3.17、SHELL 指令

格式:SHELL ["executable", "parameters"]

SHELL 指令可以指定 RUN ENTRYPOINT CMD 指令的 shell,Linux 中默认为 ["/bin/sh", "-c"]

SHELL ["/bin/sh", "-c"]

RUN lll ; ls

SHELL ["/bin/sh", "-cex"]

RUN lll ; ls

两个 RUN 运行同一命令,第二个 RUN 运行的命令会打印出每条命令并当遇到错误时退出。

ENTRYPOINT CMD 以 shell 格式指定时,SHELL 指令所指定的 shell 也会成为这两个指令的 shell

SHELL ["/bin/sh", "-cex"]

# /bin/sh -cex "nginx"
ENTRYPOINT nginx
SHELL ["/bin/sh", "-cex"]

# /bin/sh -cex "nginx"
CMD nginx

4.4、Dockerfile 部署项目

4.4.1、单体简单项目
  • 在pom.xml文件导入打包插件
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <!-- 项目基于maven pom多模块的开发的,需要设置goal-repackage属性为true,否则打包后文件依赖文件没有一起打包,然后镜像内没有可以运行的程序文件,显示错误no main manifest attribute, in /app.jar -->
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <includeSystemScope>true</includeSystemScope>
            </configuration>
        </plugin>
    </plugins>
</build>
  • 执行package命令,项目大致目录如下:

starter

  • 配置文件application.yml文件:
server:
    port: 9225

spring:
    # 数据源
    datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.126.133:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&nullCatalogMeansCurrent=true
        username: root
        password: 123456

# mybatis plus配置
mybatis-plus:
    mapper-locations: classpath*:mapper/*.xml
    configuration:
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  • 在项目的根目录下,新建Dockerfile文件(层数越少,体积越小):
# 基础镜像
FROM eclipse-temurin:8-jre

# 创建目录 并使用它作为工作目录
RUN mkdir -p /docker-test
WORKDIR /docker-test

# 复制jar文件到指定路径
COPY  ./target/docker-test-1.0-SNAPSHOT.jar app.jar

ENTRYPOINT ["java", "-jar","app.jar"]
  • 在项目的根目录新建sql文件夹,导入项目的sql语句,配置自动导入sql以及构建mysql镜像:
  • 1_business.sql配置文件
CREATE DATABASE test DEFAULT CHARACTER SET utf8mb4;
USE mysql;
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
# UPDATE user SET password=PASSWORD("123456") WHERE user='root';
# FLUSH PRIVILEGES;
USE test;
  • 2_orders.sql业务表
/*
 Navicat Premium Data Transfer

 Source Server         : docker-mysql
 Source Server Type    : MySQL
 Source Server Version : 80031 (8.0.31)
 Source Host           : 192.168.126.133:3306
 Source Schema         : test

 Target Server Type    : MySQL
 Target Server Version : 80031 (8.0.31)
 File Encoding         : 65001

 Date: 24/12/2023 19:31:51
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

USE test;
-- ----------------------------
-- Table structure for orders
-- ----------------------------
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `price` decimal(10, 2) NULL DEFAULT NULL,
  `num` decimal(10, 0) NULL DEFAULT NULL,
  `create_time` datetime NULL DEFAULT NULL,
  `stat` int UNSIGNED NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of orders
-- ----------------------------
INSERT INTO `orders` VALUES (1, 'Rambatan elite', 953.36, 4, '2020-06-05 04:02:06', 1);
INSERT INTO `orders` VALUES (2, 'Apple', 7.29, 59, '2012-06-01 00:08:39', 1);
INSERT INTO `orders` VALUES (3, 'Mango', 40.08, 74, '2006-09-30 08:54:42', 1);
INSERT INTO `orders` VALUES (4, 'Raspberry', 901.84, 50, '2001-03-24 19:01:04', 1);
INSERT INTO `orders` VALUES (5, 'aherry', 79.77, 87, '2004-03-14 04:46:40', 0);
INSERT INTO `orders` VALUES (6, 'Mango', 456.48, 61, '2011-10-01 03:59:53', 0);
INSERT INTO `orders` VALUES (7, 'Mango', 427.91, 77, '2023-08-02 14:55:27', 0);
INSERT INTO `orders` VALUES (8, 'Pluots mini', 754.35, 65, '2017-09-15 18:14:02', 0);
INSERT INTO `orders` VALUES (9, 'Cherry pi', 515.90, 1, '2008-12-04 19:04:21', 0);
INSERT INTO `orders` VALUES (10, 'Strawberry', 522.84, 37, '2003-03-17 17:02:16', 0);

SET FOREIGN_KEY_CHECKS = 1;
  • 构建Dockerfile文件(如果有多个sql文件,按照前面的编号依次执行)
# 基础镜像
FROM mysql:8.0.31

# 设置 环境变量
ENV TZ=Asia/Shanghai
ENV MYSQL_ROOT_PASSWORD=123456

# 将sql文件 拷贝到docker的自动执行文件夹内
COPY 1_business.sql /docker-entrypoint-initdb.d
COPY 2_orders.sql /docker-entrypoint-initdb.d
  • 基础镜像,在idea的启动参数进行设置;mysql镜像配置同理;也可以用命令行docker run运行:

starter

4.5 使用Docker-Compose部署

  • 使用上个例子,在项目的根目录,新建docker-compose.yml文件,同时构建基础镜像和mysql镜像:
version: "3.4"

name: docker-test

services:
    mysql-test:
        container_name: mysql-test
        build:
            context: ./sql/
        image: mysql:8.0.31
        restart: always
        tty: true
        ports:
            - "3306:3306"
        environment:
            TZ: Asia/Shanghai
            MYSQL_DATABASE: test
            MYSQL_ROOT_PASSWORD: 123456
        volumes:
            - mysql_data:/data

    docker-test:
        container_name: docker-test
        build:
            context: ./
        image: eclipse-temurin:8-jre
        restart: unless-stopped
        ports:
            - "9225:9225"
        volumes:
            - test_data:/data
        depends_on:
            - mysql-test

5、Docker 常用命令

  • 进入容器:docker exec -it reids /bin/bash
  • 停止所有容器:docker stop $(docker ps -aq)
  • 启动所有容器:docker start $(docker ps -aq)
  • 从容器创建镜像:docker commit -a "lilinhan" -m "mytest" a404c6c174a2 test:v1
  • 查看镜像:docker images
  • 查看当前运行的容器:docker ps
  • 查看当前运行的以及未运行的容器:docker ps -a
  • 删除镜像:docker rmi -f redis
  • 拉取镜像:docker pull redis
  • 上传本地镜像到仓库中(需要先登录Docker Hub):docker push mytest:v1
  • 使用当前目录的Dockerfile创建镜像:docker build -t mytest
  • 指定Dockerfile的位置创建镜像:docker build -f /opt/code/Dockerfile
  • 获取镜像的元数据:docker inspect redis
  • 查看镜像的创建历史:docker history redis
  • 将指定镜像保存成tar归档文件:docker save -o mytest.tar mytest
  • 导入镜像:
    docker load --input mytest.tar
    docker load < mytest.tar.gz
  • 从归档文件中创建镜像:docker import mytest.tar hhh:v1
  • 查看端口映射情况:docker port redis
  • 登陆到Docker Hub:docker login -u 17780255297 -p Llh020920.
  • 登出Docker Hub:docker logout
  • 查看docker系统信息:docker info
  • 查看docker版本信息:docker version
  • 创建数据卷:docker volume create reids_data
  • 查看数据卷:docker volume ls
  • 删除(未使用的)数据卷:docker volume prune
  • 停止所有用docker-compose启动的容器:docker-compose -f docker-compose.yml down
  • 删除所有数据卷:docker volume rm $(docker volume list -q)
  • 从容器中把文件复制出来:docker cp tracker:/etc/fdfs/client.conf /usr/local/server/fastdfs/
  • 将文件复制到容器中:docker cp /usr/local/server/fastdfs/client.conf tracker:/etc/fdfs
标签:no tag
本文到此就结束啦
Last modification:January 24, 2024
如果觉得我的文章对你有用,请随意赞赏