Docker - Dockerfile 自定义Docker镜像
自定义Docker镜像
我们使用 Dockerfile 来构建 Docker 镜像,Dockerfile 是一个文本文件,其中包含了若干命令,通过这些命令来构建镜像。
注意不要使用 docker commit 来自定义镜像。层过多会导致镜像臃肿。
例:修改默认的 nginx 镜像的首页,并构建一个新的镜像。
1、新建一个 Dockerfile 文件,内容如下:
FROM nginx
RUN echo 'hello baby' > /usr/share/nginx/html/index.html
其中的 FROM 和 RUN 均为 Dockerfile 的命令。
FROM 指定基础镜像。
RUN 执行命令。
2、执行命令
docker build -t mynginx . #注意后面有一个点
[root@localhost myfile]# docker build -t mynginx .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM nginx
---> f68d6e55e065
Step 2/2 : RUN echo 'hello baby' > /usr/share/nginx/html/index.html
---> Running in 3cccffe916b2
Removing intermediate container 3cccffe916b2
---> 09e0996b7d21
Successfully built 09e0996b7d21
Successfully tagged mynginx:latest
-t 后面的 mynginx 为镜像名称,后面的 . 为 镜像构建上下文(Context)。
3、创建一个新的容器
docker run -d -p 8080:80 mynginx
[root@localhost myfile]# docker run -d -p 8080:80 mynginx
ed8003c1d7f0564195291fb52bdbeceb1da796d0ee7b6b30fd15513dc0654a56
4、打开防火墙 8080 端口
firewall-cmd --add-port=8080/tcp --permanent
systemctl restart firewalld
5、访问 http://192.168.0.109:8080/ 会输出 hello baby
Dockerfile 中的指令
上面简单的介绍了如何使用 Dockerfile 来构建一个新的镜像。
下面我们主要说下 Dockerfile 中用到的指令。
FROM
FROM 是指定基础镜像。自定义镜像就需要有一个基础镜像,这是条必需的指令,且必须放在第一条。
比如上面的 FROM nginx
表示以 nginx 镜像为基础镜像来构建新镜像。
另外,还有一个空白镜像,名为 scratch
。如果想不以任何镜像为基础,则可以写成 FROM scratch
RUN
RUN 用来执行命令行命令
比如上面的 RUN echo 'hello baby' > /usr/share/nginx/html/index.html
这是 RUN 的一种格式 shell 格式
,即 RUN <命令>
还有一种格式 exec 格式
,即RUN ["可执行文件", "参数1", "参数2"]
RUN的误区
某一 Dockerfile 的内容如下:
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
此内容中有多个 RUN 指令。这里的用法是不推荐的。推荐写法如下:
FROM debian:stretch
RUN 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
为什么要把多个 RUN 指令写成一个呢?
因为镜像是一层一层的,每一条 RUN 指令都会生成一层。上面 7 个 RUN 指令,就会生成 7 层,就会导致镜像非常的臃肿。
而下面的写法,只有一个 RUN 指令,就只会有一层,且第二种写法还把用完没用的文件删除掉了,就更减少了层的大小,避免了最终镜像的臃肿。
COPY
COPY 用于复制文件
比如某 Dockerfile 文件内容如下:
FROM ubuntu
COPY app.config /usr
COPY ./src /usr/mysrc
Dockerfile 所在路径 /usr/deo/dockerfile2
如果执行的构建命令为
docker build -t demo03 .
点(.) 表示构建的上下文目录是当前目录,即 /usr/deo/dockerfile2
所以
COPY app.config /usr
表示将 /usr/deo/dockerfile2/app.config 拷贝到 ubuntu 下的 /usr 目录
COPY ./src /usr/mysrc
表示将 /usr/deo/dockerfile2/src 目录拷贝到 ubuntu 下的 /usr/ 目录并重命名为 mysrc
#当前目录是 /usr/deo/dockerfile2
[root@localhost dockerfile2]# pwd
/usr/deo/dockerfile2
#目录下有 app.config Dockerfile src目录
[root@localhost dockerfile2]# ls
app.config Dockerfile src
#src下有一个 1.tt 的文件
[root@localhost dockerfile2]# cd src
[root@localhost src]# ls
1.tt
# 切回 Dockerfile所在目录
[root@localhost src]# cd ..
[root@localhost dockerfile2]# ls
app.config Dockerfile src
# 构建新的镜像
[root@localhost dockerfile2]# docker build -t demo03 .
Sending build context to Docker daemon 4.608kB
Step 1/3 : FROM ubuntu
---> 4c108a37151f
Step 2/3 : COPY app.config /usr
---> Using cache
---> e466f7eaa7f3
Step 3/3 : COPY ./src /usr/mysrc
---> Using cache
---> bfeef57181e3
Successfully built bfeef57181e3
Successfully tagged demo03:latest
# 根据 demo03 镜像启动一个新的容器,并进行交互
[root@localhost dockerfile2]# docker run -it --rm demo03 bash
#进入到容器的 /usr 目录
root@cdc8b2b1e6be:/# cd /usr
#可以看到 app.config 文件
root@cdc8b2b1e6be:/usr# ls
app.config bin games include lib local mysrc sbin share src
#进入到 mysrc 目录,可以看到 1.tt 文件
root@cdc8b2b1e6be:/usr# cd mysrc
root@cdc8b2b1e6be:/usr/mysrc# ls
1.tt
ADD
ADD 和 COPY 类似,都有复制的功能,但 ADD 还可以实现其他的功能。
比如:
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
ADD 后面如果是个压缩包的话,会自动解压此压缩包到目录路径
另外
ADD 后面还可以是个 URL 地址,那么就会自动下载此 URL 的文件到目录路径
注意:如果 URL 是个压缩文件,也不会自动解压。
最佳实践
能用 COPY,别用 ADD,只有需要自解压时,可以使用 ADD
CMD
CMD 推荐格式
exec 格式:CMD [“可执行文件”, “参数1”, “参数2”…]
CMD 指令就是用于指定默认的容器主进程的启动命令
docker run 命令中如果指定新的命令,那么 Dockerfile 中的命令会失效。
比如 Dockerfile 内容如下:
FROM ubuntu
#使用阿里云,加快 apt-get 的速度
COPY sources.list /etc/apt/sources.list
RUN apt-get update && apt-get install -y curl
CMD ["curl","-s","https://ip.cn"]
上面的 sources.list 内容如下:
deb http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ trusty-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ trusty-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ trusty-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ trusty-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ trusty-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ trusty-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ trusty-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ trusty-backports main restricted universe multiverse
执行 build 命令
docker build -t cmddemo .
构建了一个名为 cmddemo 的镜像
可以查看下我们新建的镜像:
[root@localhost dockerfile3]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
cmddemo latest 51c80ce0e75b 2 hours ago 117MB
以 cmddemo 镜像启动一个容器
docker run -i cmddemo
gl> [root@localhost dockerfile3]# docker run -i cmddemo
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en-US"> <![endif]-->
<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en-US"> <![endif]-->
<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en-US"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en-US"> <!--<![endif]-->
<head>
<title>Access denied | ip.cn used Cloudflare to restrict access</title>
<meta charset="UTF-8" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1" />
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />
<link rel="stylesheet" id="cf_styles-css" href="/cdn-cgi/styles/cf.errors.css" type="text/css" media="screen,projection" />
<!--[if lt IE 9]><link rel="stylesheet" id='cf_styles-ie-css' href="/cdn-cgi/styles/cf.errors.ie.css" type="text/css" media="screen,projection" /><![endif]-->
<style type="text/css">body{margin:0;padding:0}</style>
<!--[if gte IE 10]><!--><script type="text/javascript" src="/cdn-cgi/scripts/zepto.min.js"></script><!--<![endif]-->
<!--[if gte IE 10]><!--><script type="text/javascript" src="/cdn-cgi/scripts/cf.common.js"></script><!--<![endif]-->
</head>
<body>
<div id="cf-wrapper">
<div class="cf-alert cf-alert-error cf-cookie-error" id="cookie-alert" data-translate="enable_cookies">Please enable cookies.</div>
<div id="cf-error-details" class="cf-error-details-wrapper">
<div class="cf-wrapper cf-header cf-error-overview">
<h1>
<span class="cf-error-type" data-translate="error">Error</span>
<span class="cf-error-code">1020</span>
<small class="heading-ray-id">Ray ID: 4f3821d838af9869 • 2019-07-09 06:09:59 UTC</small>
</h1>
<h2 class="cf-subheadline">Access denied</h2>
</div><!-- /.header -->
<section></section><!-- spacer -->
<div class="cf-section cf-wrapper">
<div class="cf-columns two">
<div class="cf-column">
<h2 data-translate="what_happened">What happened?</h2>
<p>This website is using a security service to protect itself from online attacks.</p>
</div>
</div>
</div><!-- /.section -->
<div class="cf-error-footer cf-wrapper">
<p>
<span class="cf-footer-item">Cloudflare Ray ID: <strong>4f3821d838af9869</strong></span>
<span class="cf-footer-separator">•</span>
<span class="cf-footer-item"><span>Your IP</span>: 124.193.98.146</span>
<span class="cf-footer-separator">•</span>
<span class="cf-footer-item"><span>Performance & security by</span> <a href="https://www.cloudflare.com/5xx-error-landing?utm_source=error_footer" id="brand_link" target="_blank">Cloudflare</a></span>
</p>
</div><!-- /.error-footer -->
</div><!-- /#cf-error-details -->
</div><!-- /#cf-wrapper -->
<script type="text/javascript">
window._cf_translation = {};
</script>
</body>
</html>
可以看到,容器启动后执行了命令 curl -s https://ip.cn
。
我们查看下容器
[root@localhost dockerfile3]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f092015c299b cmddemo "curl -s https://ip.…" 3 minutes ago Exited (0) 3 minutes ago unruffled_dhawan
这个容器启动后,就又关闭了,如果我们想再看下效果,可以重新启动一下。
docker start -i f092015c299b # f092015c299b是上面的 CONTAINER ID
docker run 命令是可以指定 COMMAND 的
docker run 的格式: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
所以我们还可以这样写
docker run -i cmddemo ls
[root@localhost dockerfile3]# docker run -i cmddemo ls
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
输出结果和上次的不一样了。我们查看一下容器,发现容器的 COMMAND 和上一个的不一样了。
[root@localhost dockerfile3]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f1afb4d5bb37 cmddemo "ls" 37 seconds ago Exited (0) 36 seconds ago recursing_shirley
f092015c299b cmddemo "curl -s https://ip.…" 13 minutes ago Exited (0) 6 minutes ago unruffled_dhawan
所以,如果我们在 docker run 的时候指定 COMMAND 后,Dockerfile 中的 COMMAND 就会被覆盖。
Dockerfile 中只有最后一个 CMD 有效。
比如 Dockerfile 如下 :
FROM ubuntu
COPY sources.list /etc/apt/sources.list
RUN apt-get update && apt-get install -y curl
CMD ["curl","-s","https://ip.cn"]
CMD ["ls"]
自己可以试一下,和上面的操作大同小异。
ENTRYPOINT
入口点
<ENTRYPOINT> "<CMD>"
容器启动后,执行 <CMD>
命令
ENTRYPOINT ["JAVA","-JAR","/APP.JAR"]
容器启动后,执行 java -jar /app.jar
命令。
另外,docker run 中指定的指令会加在 ENTRYPOINT 的指令后面。
比如某 Dockerfile 内容如下:
FROM ubuntu
COPY sources.list /etc/apt/sources.list
RUN apt-get update && apt-get install -y curl
ENTRYPOINT ["curl","-s","https://ip.cn"]
构建镜像 entrydemo
docker build -t entrydemo .
执行命令
docker run -i entrydemo >> 2.txt
就会相当于在指令 curl -s https://ip.cn
后面加上了 >> 2.txt
执行命令就变成了 curl -s https://ip.cn >> 2.txt
ENTRYPOINT 还可以执行脚本文件
比如
ENTRYPOINT ["docker-entrypoint.sh"]
ENV
ENV 是环境变量。且将来会在容器中,也就是容器的环境变量。
比如有一 Dockerfile 文件如下:
RUN curl -SLO "https://nodejs.org/dist/v7.2.0/node-v7.2.0-linux-x64.tar.xz" \
&& curl -SLO "https://nodejs.org/dist/v7.2.0/SHASUMS256.txt.asc" \
其中,node的版本 7.2.0 反复用到,我们可以单独提出来,将其声明为环境变量。
修改如下:
ENV NODE_VERSION 7.2.0
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
ENV 的使用方法如下:
#声明
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2> ...
#使用
$NODE_VERSION
ARG
ARG 也是环境变量,只不过是只存活在构建脚本中,并不会存活在容器中。
VOLUME
VOLUME 定义匿名卷, VOLUME /data
表示容器中的 /data 目录挂载了一个匿名卷,也就是映射到了 Host 主机的一个位置。在容器 /data 中的数据不会随容器的丢失而丢失。
当然,也可以在 docker run 命令中指定卷。
docker run -d -v mydata:/data xxxx
格式:
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
比如,某 Dockerfile 内容如下:
FROM ubuntu
RUN mkdir /mydata
VOLUME /mydata
构建镜像
docker build -t voldemo .
运行容器,并进入容器,执行命令
docker run -it voldemo bash
# 通过 ls 命令,可以看到 mydata 目录。
root@8b00c65392a7:/# ls
bin boot dev etc home lib lib64 media mnt mydata opt proc root run sbin srv sys tmp usr var
root@8b00c65392a7:/# cd mydata/
# 在mydata目录中新建一个新的文件 new.txt
root@8b00c65392a7:/mydata# touch new.txt
# 退出
root@8b00c65392a7:/mydata# exit
我们查看一下容器ID
[root@localhost _data]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8b00c65392a7 voldemo "bash" 16 minutes ago Exited (130) 11 minutes ago adoring_feistel
然后,我们查看下此容器的信息
docker inspect 8b00c65392a7
或者 docker inspect -f='{{json .Mounts}}' 8b00c65392a7
[root@localhost _data]# docker inspect -f='{{json .Mounts}}' 8b00c65392a7
[{"Type":"volume","Name":"d71f19cae15aa50883b8fb2d12bca56f34eb7ad5053cd866433cbfca1693dee9","Source":"/var/lib/docker/volumes/d71f19cae15aa50883b8fb2d12bca56f34eb7ad5053cd866433cbfca1693dee9/_data","Destination":"/mydata","Driver":"local","Mode":"","RW":true,"Propagation":""}]
我们可以看到我们之前的挂载卷 /mydata,此卷的目的地是 /mydata
名字是 d71f19cae15aa50883b8fb2d12bca56f34eb7ad5053cd866433cbfca1693dee9
对应的 Host 主机地址是 /var/lib/docker/volumes/d71f19cae15aa50883b8fb2d12bca56f34eb7ad5053cd866433cbfca1693dee9/_data
我们之前在容器的 /mydata 中新增了一个 new.txt 文件。
现在我们进入 /var/lib/docker/volumes/d71f19cae15aa50883b8fb2d12bca56f34eb7ad5053cd866433cbfca1693dee9/_data
查看下是否有 new.txt 。
[root@localhost _data]# cd /var/lib/docker/volumes/d71f19cae15aa50883b8fb2d12bca56f34eb7ad5053cd866433cbfca1693dee9/_data
[root@localhost _data]# ls
new.txt
说明 new.txt 文件存在于我们的 Host 主机中。
现在我们删除容器 8b00c65392a7
,看 new.txt 文件是否消失。
#删除
docker rm 8b00c65392a7
#查看容器是否还有
docker ps -a
#进入映射的文件夹
cd /var/lib/docker/volumes/d71f19cae15aa50883b8fb2d12bca56f34eb7ad5053cd866433cbfca1693dee9/_data
#查看文件
ls
事实证明,文件还存在。
另外 docker run 中还可以指定卷。
docker run -it -v test:/mydata voldemo bash
此容器的 /mydata 对应的 Host 主机地址为: /var/lib/docker/volumes/test/_data
也就是一个 名为 test 的卷
docker run -it -v /usr/test:/mydata voldemo bash
此容器的 /mydata 对应的地址为 /usr/test,这个没有卷名。
EXPOSE
声明端口
格式 EXPOSE <端口1> [<端口2>...]
值得注意的是:这里只是声明,并不会自动去映射。
WORKDIR
指定工作目录,或当前目录,作用在各个层上面。
格式 WORKDIR <工作目录路径>
USER
指定当前用户
格式 USER <用户名>[:<用户组>]
比如
RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]
如果以 root 执行的脚本,在执行期间希望改变身份,推荐使用 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.7/gosu-amd64" \
&& chmod +x /usr/local/bin/gosu \
&& gosu nobody true
# 设置 CMD,并以另外的用户执行
CMD [ "exec", "gosu", "redis", "redis-server" ]
HEALTHCHECK
HEALTHCHECK 用来检测容器是否正常。
当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy
格式为 HEALTHCHECK [选项] CMD <命令>
选项有:
--interval=<间隔>:两次健康检查的间隔,默认为 30 秒
--timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
--retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。
新建 Dockerfile 为
FROM nginx
COPY sources.list /etc/apt/sources.list #sources.list 内容和上面的一样
RUN apt-get update && apt-get install -y curl --allow-unauthenticated && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
CMD curl -fs http://localhost/ || exit 1
这里使用的健康检查命令为 curl -fs http://localhost/ || exit 1
,每5秒执行一次(实际项目中可设置长些),超过3s认为服务有问题了。
构建镜像
docker build -t myweb:v1 .
运行容器
docker run -d --name web -p 80:80 myweb:v1
查看状态
docker ps
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c12c88d0e875 myweb:v2 "nginx -g 'daemon of…" 16 minutes ago Up 15 minutes (healthy) 0.0.0.0:80->80/tcp mywebv2
我们可以看到 STATUS 列有一个 healthy 标识。
如果有问题,可通过以下命令来查看日志
docker inspect --format '{{json .State.Health}}' web | python -m json.tool
*昵称:
*邮箱:
个人站点:
*想说的话: