一、镜像的制作方式
Docker 镜像制作类似于虚拟机的镜像(模版)制作,即按照公司的实际业务需求将需要安装的软件、相关配置等基础环境配置完成,然后将其做成镜像,最后再批量从镜像批量生成容器实例,这样可以极大的简化相同环境下的部署工作。
Docker的镜像制作分为手动制作(基于容器,docker commit)和自动制作(基于DockerFile,再docker build)。企业通常都是基于Dockerfile制作镜像。
基于容器手动制作镜像步骤具体如下:
1. 下载一个系统的官方基础镜像
2. 基于基础镜像启动一个容器,并进入到容器
3. 在容器里面做配置操作(包括安装基础命令、配置运行环境、安装并配置服务、放业务程序代码等)
4. 提交为一个新镜像 docker commit
5. 基于自己的的镜像创建容器并测试访问
二、利用Dockerfile文件执行 docker build自动构建镜像
2.1 Dockerfile使用详解
Dockerfile介绍DockerFile是一种被Docker程序解释执行的脚本,由一条条的命令组成的,每条命令对应linux下面的一条命令,Docker程序将这些DockerFile指令再翻译成真正的linux命令,它有自己的书写方式和支持的命令,Docker程序读取DockerFile并根据指令生成Docker镜像,相比手动制作镜像的方式,DockerFile 更能直观的展示镜像是怎么产生的。有了DockerFile,当后期有额外的需求时,只要在之前DockerFile添加或者修改响应的命令即可重新生成新的Docker镜像,避免了重复手动制作镜像的麻烦。
注意:每条指令都是独立运行的,并会导致创建新镜像,比如 RUN cd /tmp 对下一条指令无任何影响。
2.2 Dockerfile制作镜像的分层结构
2.3 Dockerfile文件格式
- 每一行以Dockerfile的指令开头,指令不区分大小写,但是惯例使用大写
- 使用 # 开始作为注释
- 每一行只支持一条指令,每条指令可以携带多个参数
- 指令按文件的顺序从上至下进行执行
- 每个指令的执行会生成一个新的镜像层,为了减少分层和镜像大小,尽可能将多条指令合并成一条指令
- 制作镜像一般可能需要反复多次,每次执行dockfile都按顺序执行,从头开始,已经执行过的指令已经缓存,不需要再执行,如果后续有一行新的指令没执行过,其往后的指令将会重新执行。所以为加速镜像制作,将最常变化的内容放在Dockerfile的后面
2.4 Dockerfile的构建过程
- 从基础镜像运行一个容器
- 执行一条指令,对容器做出修改
- 执行类似docker commit的操作,提交一个新的中间镜像层(可以利用中间层镜像创建容器进行调试和排错)
- 再基于刚提交的镜像运行一个新容器
- 执行Dockerfile中的下一条指令,直至所有指令执行完毕
顺便说一下,结合到实际工作当中,整个流程可能是这样的:
开发提交代码到gitlab/github->下载源码->(针对java程序)maven编译生成war包/jar包->Dockerfile 中COPY war包/jar包->docker build生成镜像->将镜像push到harbor仓库->从仓库pull镜像->docker run运行容器
2.5 Dockerfile相关指令
2.5.1 FROM
FRPM指令用来指定基础镜像。
定制镜像,需要先有一个基础镜像,在这个基础镜像上进行定制。FROM就是指定基础镜像,此指令通常必需放在Dockerfile文件第一个非注释行。后续的指令都是运行 于此基准镜像所提供的运行环境。
基础镜像可以是任何可用镜像文件,默认情况下,docker build会在docker主机上查找指定的镜像文件,在其不存在时,则会从Docker Hub Registry上拉取所需的镜像文件。如果找不到指定的镜像文件,docker build会返回一个错误信息。
语法:
FROM <image>
FROM <image>:<tag>
FROM <image>:<digest>
三种写法,其中<tag>和<digest> 是可选的,如果没有选择,那么默认值为latest。
2.5.2 LABEL
LABEL指令用来指定镜像元数据。代替之前的MAINTAINER
语法:LABEL <key>=<value> <key>=<value> <key>=<value> ...
一个镜像可以有多个label。还可以写在一行中,即多标签写法,可以减少镜像的的大小。
#一行格式
LABEL multi.label1="value1" multi.label2="value2" other="value3"
#多行格式
LABEL multi.label1="value1" \
multi.label2="value2" \
other="value3"
2.5.3 RUN
RUN 指令用来在构建镜像阶段需要执行 FROM 指定镜像所支持的Shell命令。 通常各种基础镜像一般都支持丰富的shell命令。
RUN 可以写多个,每一个RUN指令都会建立一个镜像层,所以尽可能合并成一条指令,比如将多个 shell命令通过 && 连接一起成为在一条指令。
每个RUN都是独立运行的,和前一个RUN无关。
语法:
#shell格式
RUN <命令>
#exec格式:此种形式不支持环境变量。注意是双引号,不能是单引号
RUN ["executable","param1","param2"...]
2.5.4 ENV
ENV指令可以定义环境变量和值,会被后续指令(如:ENV、ADD、COPY、RUN等)通过$KEY或${KEY}进行引用,并在容器运行时保持。
语法:
#变量赋值格式1
ENV <key> <value> #此格式只能对一个key赋值,<key>之后的所有内容均会被视作其<value>的组成部分
#变量赋值格式2
ENV <key1>=<value1> <key2>=<value2> \ #此格式可以支持多个key赋值,定义多个变量建议使用,减少镜像层
<key3>=<value3> ...
#如果<value>中包含空格,可以以反斜线\进行转义,也可通过对<value>加引号进行标识;另外,反斜线也可用于续行
#引用变量
RUN $key …
#变量支持高级赋值格式(shell也支持)
${key:-word}
${key:+word}
即变量key有值就用原值,无值就用新的赋值(word)
Eg:以下两种格式相同
#格式1
ENV Name="Tom" Dog=Rex\ The\ Dog \
Cat=daju
#格式2
ENV Name Tom
ENV Dog Rex The Dog
ENV Cat daju
2.5.5 COPY
COPY指令用来复制文本。将文件/目录从宿主机复制到容器。
语法:
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"] #路径中有空白字符时,建议使用此格式
说明:
- 必须是build上下文中的路径(为 Dockerfile 所在目录的相对路径),不能是其父目录中的文件
- 如果是目录,则其内部文件或子目录会被递归复制,但目录自身不会被复制
- 如果指定了多个,或在中间使用了通配符,则必须是一个目录,且必须以 / 结尾
- 可以是绝对路径或者是 WORKDIR 指定的相对路径
- 使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等
- 如果事先不存在,它将会被自动创建,这包括其父目录路径,即递归创建目录
2.5.6 ADD
ADD指令用来复制和解压缩。COPY指令的加强版。
语法:
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<sr",c>"... <"]dest>
特别说明(与COPY相比的独特之处):
如果是一个本地文件系统上的打包文件,如: gz、bz2、xz ,它将被解包,其行为类似于"tar -x"命令,但是通过URL获取到的tar文件将不会自动展开
2.5.7 CMD
一个容器中需要持续运行的进程一般只有一个,CMD用来指定启动容器时默认执行的一个命令,且其运行结束后,容器也会停止。所以一般CMD 指定的命令为持续运行且为前台命令。
说明:
- 如果docker run未指定任何的执行命令或者dockerfile里面也没有ENTRYPOINT命令,那么开启容器时就会使用执行CMD指定的默认的命令
- 前面介绍过的 RUN 命令是在构建镜像时执行的命令,注意二者的区别
- 每个 Dockerfile 只能有一条 CMD 命令。如指定了多条,只有最后一条被执行
- 如果启动容器时用 docker run xxx 指定运行的命令,则会覆盖 CMD 指定的命令
语法:
# 使用 exec 执行,第一个参数必须是命令的全路径,此种形式不支持环境变量
CMD ["executable","param1","param2"]
# 在 /bin/sh 中执行,提供给需要交互的应用;此种形式支持环境变量
CMD command param1 param2
# 提供给 ENTRYPOINT 命令的默认参数
CMD ["param1","param2"]
2.5.8 ENTRYPOINT
ENTRYPOINT是容器运行的入口点。功能类似于CMD,配置容器启动后执行的命令及参数。
语法:
# 使用 exec 执行
ENTRYPOINT ["executable", "param1", "param2"...]
# shell中执行
ENTRYPOINT command param1 param2
说明:
- 如果docker run 命令有参数,则参数全部都作为ENTRYPOINT的参数
- 如果docker run 后面没有额外参数,但是dockerfile中有CMD命令(即上面CMD的第三种用法),即Dockerfile中即有CMD也有ENTRYPOINT,那么CMD的全部内容会作为ENTRYPOINT的参数
- 每个 Dockerfile 中只能有一个 ENTRYPOINT,当指定多个时,只有最后一个生效
- 如果docker run 后面有额外参数,同时Dockerfile中即有CMD也有ENTRYPOINT,则docker run 后面的参数覆盖掉CMD参数内容,最终作为ENTRYPOINT的参数
- 可以通过docker run --entrypoint string 参数在运行时替换,注意string不要加空格
Eg:某Dockerfile文件
…
#如下两条指令相当于ENTRYPOINT ["/apps/nginx/sbin/nginx","-g","daemon off;"]
CMD ["-g","daemon off;"]
ENTRYPOINT ["/apps/nginx/sbin/nginx"]
Eg:利用脚本实现指定环境变量动态生成配置文件内容
echo 'Nginx Website in Dockerfile' > index.html
vim Dockerfile
FROM nginx:1.22.0
LABEL maintainer="xd"
ENV DOC_ROOT='/data/website/'
ADD index.html ${DOC_ROOT}
ADD entrypoint.sh /bin/
EXPOSE 80
#nginx前台运行命令
CMD ["/usr/sbin/nginx","-g", "daemon off;"]
ENTRYPOINT [ "/bin/entrypoint.sh"]
vim entrypoint.sh
#!/bin/sh
cat > /etc/nginx/conf.d/www.conf <<EOF
server {
server_name ${HOSTNAME};
listen ${IP:-0.0.0.0}:${PORT:-80};
root ${DOC_ROOT:-/usr/share/nginx/html};
}
EOF
exec "$@"
chmod +x entrypoint.sh
docker build -t nginx:v1.0 .
docker run -d --name n1 -e PORT=81 -e HOSTNAME=www.xd.org nginx:v1.0
docker exec -it n1 cat /etc/nginx/conf.d/www.conf
2.5.9 VOLUME
VOLUME:匿名卷在容器中创建一个可以从本地主机或其他容器挂载的挂载点,一般用来存放数据库和需要保持的数据等,默认会将宿主机上的目录挂载至VOLUME 指令指定的容器目录。即使容器后期被删除,此宿主机的目录仍会保留,从而实现容器数据的持久保存。宿主机目录为/var/lib/docker/volumes/<volume_id>/_data。但局限性在于,这样的目录多了之后,如果删了某个容器,再想找回之前的数据就难了。而且容器后续还有可能部署到别的机器上(结合k8s的话),这样一来管理数据就更加麻烦。
语法:
VOLUME <容器内路径>
VOLUME ["<容器内路径1>", "<容器内路径2>"...]
说明:
- <容器内路径>如果在容器内不存在,在创建容器时会自动创建
- <容器内路径>如果是存在的,同时目录内有内容,将会把此目录的内容复制到宿主机的实际目录
注意:
1.Dockerfile中的VOLUME实现的是匿名数据卷,无法指定宿主机路径和容器目录的挂载关系
2.通过docker rm -fv 可以删除容器的同时删除VOLUME指定的卷
2.5.10 EXPOSE
EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会真正暴露端口,即不会自动在宿主进行端口映射。因此,即使 Dockerfile 没有 EXPOSE 端口指令,也可以通过docker run -p 临时暴露容器内程序真正监听的端口。
语法:EXPOSE <port>[/ <protocol>] [<port>[/ <protocol>] ..]
#说明
<protocol>用于指定传输层协议,可为tcp或udp二者之一,默认为TCP协议
2.5.11 WORKDIR
WORKDIR用来指定工作目录,为后续的 RUN、CMD、ENTRYPOINT 指令配置工作目录,当容器运行后,进入容器内WORKDIR指定的默认目录。
WORKDIR 指定工作目录(或称当前目录),以后各层的当前目录就被改为指定的目录,如果该目录不存在,WORKDIR 会自行创建。
可以使用多个 WORKDIR 指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如:
WORKDIR /a
WORKDIR b
则最终路径为/a/b
2.5.12 USER
USER用来指定当前用户,即指定运行容器的用户名或 UID,在后续Dockerfile中的 RUN,CMD和ENTRYPOINT指令时使用此用户。
当服务不需要管理员权限时,可以通过该命令指定运行用户。
这个用户必须是事先建立好的,否则无法切换。
如果没有指定 USER,默认是 root 身份执行。
语法:
USER <user>[:<group>]
USER <UID>[:<GID>]
2.5.13 HEALTHCHECK
HEALTHCHECK用来做健康检查。
语法:
#设置检查容器健康状况的命令,如果命令执行失败,则返回1,即unhealthy
HEALTHCHECK [选项] <CMD命令>
#如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令
HEALTHCHECK NONE
HEALTHCHECK 支持下列选项:
--interval=<间隔>两次健康检查的间隔,默认为 30 秒
--timeout=<时长>健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒
--retries=<次数>当连续失败指定次数后,则将容器状态视为 unhealthy,默认3次
检查结果返回值说明:
0 #success 这个容器是健康的,可以使用了
1 #unhealthy 容器工作异常
2 #reserved 不要使用此退出代码
#查看健康状态
docker inspect -f '{{.State.Health.Status}}' CONTAINER
#以json格式显示
docker inspect -f '{{json.State.Health}}' CONTAINER
2.5.14 .dockerignore
.dockerignore文件,说明在生成构建上下文时Docker客户端应忽略的文件和文件夹指定模式。
语法:
# #以#开头的行为注释
* #匹配任何非分隔符字符序列
? #匹配任何单个非分隔符
\\ #表示 \
** #匹配任意数量的目录(包括零)。Eg:**/*.go将排除在所有目录中以.go结尾的所有文件,包括构建上下文的根。
! #表示取反,可用于排除例外情况
Eg:
#排除 test 目录下的所有文件
test/*
#排除 md 目录下的 xttblog.md 文件
md/xttblog.md
#排除 xttblog 目录下的所有 .md 的文件
xttblog/*.md
#排除以 xttblog 为前缀的文件和文件夹
xttblog?
#排除所有目录下的 .sql 文件
**/*.sql
#除了README的md不排外,排除所有md文件,但不排除README-secret.md
*.md
!README*.md
README-secret.md
#除了所有README的md文件以外的md都排除
*.md
README-secret.md
!README*.md
2.6 构建镜像docker build命令
语法:docker build [OPTIONS] PATH | URL | -
[OPTIONS]说明:
PATH | URL | - #可以使是本地路径,也可以是URL路径。若设置为 - ,则从标准输入获取 Dockerfile的内容
-f, --file string #Dockerfile文件名,默认为 PATH/Dockerfile
--force-rm #总是删除中间层容器,创建镜像失败时,删除临时容器
--no-cache #不使用之前构建中创建的缓存
-q,--quiet=false #不显示Dockerfile的RUN运行的输出结果
--rm=true #创建镜像成功时,删除临时容器
-t --tag list #设置镜像名称、标签。格式为:<镜像名>:<标签>(标签默认为latest)
三、镜像制作实战
3.1 基于CentOS制作Base基础镜像
#按照业务类型或系统类型等方式划分创建目录环境,方便后期镜像比较多的时候进行分类
#mkdir /data/dockerfile/{web/{nginx,apache,tomcat,jdk},system/{centos,ubuntu,alpine,deb ian} –p
#下载镜像
docker pull centos:centos7.9.2009
#制作基础镜像
cd /data/dockerfile/system/centos
vim Dockerfile
FROM centos:centos7.9.2009
LABEL maintainer="wxd"
RUN yum -y install wget && rm -rf /etc/yum.repos.d/* && wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo && wget -P /etc/yum.repos.d https://mirrors.aliyun.com/repo/epel-7.repo \
&& sed -i -e '/mirrors.cloud.aliyuncs.com/d' -e '/mirrors.aliyuncs.com/d' /etc/yum.repos.d/CentOS-Base.repo \
&& yum -y install vim-enhanced tcpdump lrzsz tree telnet bash-completion net-tools wget curl bzip2 lsof zip unzip nfs-utils gcc make gcc-c++ glibc glibc-devel pcre pcre-devel openssl openssl-devel systemd-devel zlib-devel && yum clean all \
&& rm -rf /etc/localtime && ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
docker build -t centos7-base:v1 .
3.2 Dockerfile 制作基于Base基础镜像的 nginx 镜像
3.2.1 在Dockerfile目录下准备编译安装的相关文件
mkdir -p /data/dockerfile/web/nginx/1.22.1
cd /data/dockerfile/web/nginx/1.22.1
#下载nginx包
wget http://nginx.org/download/nginx-1.22.1.tar.gz
#准备测试页面
echo "Nginx test page" > index.html
#准备配置文件
vim nginx.conf
user nginx;
worker_processes 1;
daemon off;
error_log /usr/local/nginx/logs/error.log;
events {
use epoll;
worker_connections 65535;
}
http {
include mime.types;
default_type application/octet-stream;
gzip on;
server {
server_name www.test.org;
listen 80;
location / {
root /usr/local/nginx/html;
index index.html;
}
}
}
3.2.2 编写Dockerfile文件并生成镜像
vim Dockerfile
FROM centos7-base:v1
LABEL maintainer="wxd"
ENV versinotallow="1.22.1"
ADD nginx-${version}.tar.gz /usr/local/src/
RUN cd /usr/local/src/nginx-${version} && ./configure --user=nginx --group=nginx --prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_stub_status_module --with-http_gzip_static_module --with-pcre --with-stream --with-stream_ssl_module --with-stream_realip_module --with-http_perl_module && make && make install && rm -rf /usr/local/src/nginx-${version}
ADD nginx.conf /usr/local/nginx/conf/
ADD index.html /usr/local/nginx/html/
EXPOSE 80 443
CMD ["/usr/local/nginx/sbin/nginx"]
docker build -t centos.7.9.2009-nginx:1.22.1 .
用镜像生成容器并测试
3.3 基于Base基础镜像制作tomcat镜像
3.3.1 准备相关软件包
cd /data/dockerfile/web/tomcat
wget https://www.oracle.com/java/technologies/downloads/jdk-11.0.18_linux-x64_bin.tar.gz
wget https://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-10/v10.1.7/bin/apache-tomcat-10.1.7.tar.gz
3.3.2 准备相关目录&文件
mkdir test && cd test
vim index.jsp
<%@ page cnotallow="text/html;charset=UTF-8" language="java" %>
<html>
<body>
<h2>Tomcat test page</h2>
</body>
</html>
cd .. && tar czf test.tar.gz test
3.3.3 编写Dockerfile文件并生成镜像
vim Dockerfile
FROM centos7-base:v1
LABEL maintainer="wxd"
ADD jdk-11.0.18_linux-x64_bin.tar.gz /usr/local
ADD apache-tomcat-10.1.7.tar.gz /usr/local
ENV JAVA_HOME /usr/local/jdk-11.0.18
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV CATALINA_HOME /usr/local/apache-tomcat-10.1.7
ENV PATH $PATH:$JAVA_HOME/bin:$CLASSPATH:$CATALINA_HOME/lib:$CATALINA_HOME/bin
ADD test.tar.gz /usr/local/apache-tomcat-10.1.7/webapps/
EXPOSE 8080
CMD /usr/local/apache-tomcat-10.1.7/bin/startup.sh && tail -f /usr/local/apache-tomcat-10.1.7/logs/catalina.out
docker build -t centos7-tomcat:10.1.7 .
运行容器并测试结果
docker run -d -p 8081:8080 --name test-tomcat centos7-tomcat:10.1.7
3.4 基于alpine镜像制作nginx镜像
Alpine 操作系统是一个面向安全的轻型 Linux 发行版。它提供了自己的包管理工具 apk,可以通过 https://pkgs.alpinelinux.org/packages 网站上查询包信息,也可以直接通过 apk 命令直接查询和安装各种软件。
Alpine Docker 镜像也继承了 Alpine Linux 发行版的这些优势。相比于其他 Docker 镜像,它的容量非常小,仅仅只有 5 MB 左右(Ubuntu 系列镜像接近 200 MB),且拥有非常友好的包管理机制。官方镜像来自 docker-alpine 项目。
目前 Docker 官方已开始推荐使用 Alpine 替代之前的 Ubuntu 做为基础镜像环境。这样会带来多个好处:包括镜像下载速度加快、镜像安全性提高、主机之间的切换更方便、占用更少磁盘空间等。
#拉取镜像
docker pull alpine:3.17.2
##准备nginx配置文件,此步骤与基于Base基础镜像制作nginx镜像使用的配置文件相同
mkdir /data/dockerfile/web/nginx/alpine-3.17.2
cd /data/dockerfile/web/nginx/alpine-3.17.2
#准备测试页面
echo "Nginx alpine test page" > index.html
#编写Dockerfile
vim Dockerfile
FROM alpine:3.17.2
LABEL maintainer="wxd"
ENV versinotallow="1.22.1"
ADD nginx-${version}.tar.gz /usr/local/src/
RUN echo "https://mirrors.aliyun.com/alpine/v3.17/main/" > /etc/apk/repositories \
&& echo "https://mirrors.aliyun.com/alpine/v3.17/community/" >> /etc/apk/repositories \
&& apk update && apk upgrade && apk add iotop gcc libgcc libc-dev libcurl libc-utils pcre-dev zlib-dev libnfs make pcre pcre2 zip unzip net-tools pstree wget libevent libevent-dev && addgroup -S nginx && adduser -DHS -s /sbin/nologin -G nginx nginx \
&& cd /usr/local/src/nginx-${version} && ./configure --user=nginx --group=nginx --prefix=/usr/local/nginx && make && make install && rm -rf /usr/local/src/nginx-${version}
ADD nginx.conf /usr/local/nginx/conf/
ADD index.html /usr/local/nginx/html/
EXPOSE 80 443
CMD ["/usr/local/nginx/sbin/nginx"]
#生成镜像
docker build -t alpine-3.17.2:nginx.1.22.1 .
#运行容器&测试
docker run -d --name test-alpine-nginx -p 82:80 alpine-3.17.2:nginx.1.22.1