Docker - Dockerfile 自定义Docker镜像

时间 2019/7/7 17:54:11 加载中...

自定义Docker镜像

我们使用 Dockerfile 来构建 Docker 镜像,Dockerfile 是一个文本文件,其中包含了若干命令,通过这些命令来构建镜像。

注意不要使用 docker commit 来自定义镜像。层过多会导致镜像臃肿。

例:修改默认的 nginx 镜像的首页,并构建一个新的镜像。

1、新建一个 Dockerfile 文件,内容如下:

  1. FROM nginx
  2. RUN echo 'hello baby' > /usr/share/nginx/html/index.html

其中的 FROM 和 RUN 均为 Dockerfile 的命令。
FROM 指定基础镜像。
RUN 执行命令。

2、执行命令

  1. docker build -t mynginx . #注意后面有一个点
  2. [root@localhost myfile]# docker build -t mynginx .
  3. Sending build context to Docker daemon 2.048kB
  4. Step 1/2 : FROM nginx
  5. ---> f68d6e55e065
  6. Step 2/2 : RUN echo 'hello baby' > /usr/share/nginx/html/index.html
  7. ---> Running in 3cccffe916b2
  8. Removing intermediate container 3cccffe916b2
  9. ---> 09e0996b7d21
  10. Successfully built 09e0996b7d21
  11. Successfully tagged mynginx:latest

-t 后面的 mynginx 为镜像名称,后面的 . 为 镜像构建上下文(Context)。

3、创建一个新的容器

  1. docker run -d -p 8080:80 mynginx
  2. [root@localhost myfile]# docker run -d -p 8080:80 mynginx
  3. ed8003c1d7f0564195291fb52bdbeceb1da796d0ee7b6b30fd15513dc0654a56

4、打开防火墙 8080 端口

  1. firewall-cmd --add-port=8080/tcp --permanent
  2. 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 的内容如下:

  1. FROM debian:stretch
  2. RUN apt-get update
  3. RUN apt-get install -y gcc libc6-dev make wget
  4. RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
  5. RUN mkdir -p /usr/src/redis
  6. RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
  7. RUN make -C /usr/src/redis
  8. RUN make -C /usr/src/redis install

此内容中有多个 RUN 指令。这里的用法是不推荐的。推荐写法如下:

  1. FROM debian:stretch
  2. RUN buildDeps='gcc libc6-dev make wget' \
  3. && apt-get update \
  4. && apt-get install -y $buildDeps \
  5. && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
  6. && mkdir -p /usr/src/redis \
  7. && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
  8. && make -C /usr/src/redis \
  9. && make -C /usr/src/redis install \
  10. && rm -rf /var/lib/apt/lists/* \
  11. && rm redis.tar.gz \
  12. && rm -r /usr/src/redis \
  13. && apt-get purge -y --auto-remove $buildDeps

为什么要把多个 RUN 指令写成一个呢?

因为镜像是一层一层的,每一条 RUN 指令都会生成一层。上面 7 个 RUN 指令,就会生成 7 层,就会导致镜像非常的臃肿。

而下面的写法,只有一个 RUN 指令,就只会有一层,且第二种写法还把用完没用的文件删除掉了,就更减少了层的大小,避免了最终镜像的臃肿。

COPY

COPY 用于复制文件

比如某 Dockerfile 文件内容如下:

  1. FROM ubuntu
  2. COPY app.config /usr
  3. 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

  1. #当前目录是 /usr/deo/dockerfile2
  2. [root@localhost dockerfile2]# pwd
  3. /usr/deo/dockerfile2
  4. #目录下有 app.config Dockerfile src目录
  5. [root@localhost dockerfile2]# ls
  6. app.config Dockerfile src
  7. #src下有一个 1.tt 的文件
  8. [root@localhost dockerfile2]# cd src
  9. [root@localhost src]# ls
  10. 1.tt
  11. # 切回 Dockerfile所在目录
  12. [root@localhost src]# cd ..
  13. [root@localhost dockerfile2]# ls
  14. app.config Dockerfile src
  15. # 构建新的镜像
  16. [root@localhost dockerfile2]# docker build -t demo03 .
  17. Sending build context to Docker daemon 4.608kB
  18. Step 1/3 : FROM ubuntu
  19. ---> 4c108a37151f
  20. Step 2/3 : COPY app.config /usr
  21. ---> Using cache
  22. ---> e466f7eaa7f3
  23. Step 3/3 : COPY ./src /usr/mysrc
  24. ---> Using cache
  25. ---> bfeef57181e3
  26. Successfully built bfeef57181e3
  27. Successfully tagged demo03:latest
  28. # 根据 demo03 镜像启动一个新的容器,并进行交互
  29. [root@localhost dockerfile2]# docker run -it --rm demo03 bash
  30. #进入到容器的 /usr 目录
  31. root@cdc8b2b1e6be:/# cd /usr
  32. #可以看到 app.config 文件
  33. root@cdc8b2b1e6be:/usr# ls
  34. app.config bin games include lib local mysrc sbin share src
  35. #进入到 mysrc 目录,可以看到 1.tt 文件
  36. root@cdc8b2b1e6be:/usr# cd mysrc
  37. root@cdc8b2b1e6be:/usr/mysrc# ls
  38. 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 内容如下:

  1. FROM ubuntu
  2. #使用阿里云,加快 apt-get 的速度
  3. COPY sources.list /etc/apt/sources.list
  4. RUN apt-get update && apt-get install -y curl
  5. CMD ["curl","-s","https://ip.cn"]

上面的 sources.list 内容如下:

  1. deb http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse
  2. deb http://mirrors.aliyun.com/ubuntu/ trusty-security main restricted universe multiverse
  3. deb http://mirrors.aliyun.com/ubuntu/ trusty-updates main restricted universe multiverse
  4. deb http://mirrors.aliyun.com/ubuntu/ trusty-proposed main restricted universe multiverse
  5. deb http://mirrors.aliyun.com/ubuntu/ trusty-backports main restricted universe multiverse
  6. deb-src http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse
  7. deb-src http://mirrors.aliyun.com/ubuntu/ trusty-security main restricted universe multiverse
  8. deb-src http://mirrors.aliyun.com/ubuntu/ trusty-updates main restricted universe multiverse
  9. deb-src http://mirrors.aliyun.com/ubuntu/ trusty-proposed main restricted universe multiverse
  10. deb-src http://mirrors.aliyun.com/ubuntu/ trusty-backports main restricted universe multiverse

执行 build 命令

docker build -t cmddemo . 构建了一个名为 cmddemo 的镜像

可以查看下我们新建的镜像:

  1. [root@localhost dockerfile3]# docker images
  2. REPOSITORY TAG IMAGE ID CREATED SIZE
  3. cmddemo latest 51c80ce0e75b 2 hours ago 117MB

以 cmddemo 镜像启动一个容器

docker run -i cmddemo

  1. gl> [root@localhost dockerfile3]# docker run -i cmddemo
  2. <!DOCTYPE html>
  3. <!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en-US"> <![endif]-->
  4. <!--[if IE 7]> <html class="no-js ie7 oldie" lang="en-US"> <![endif]-->
  5. <!--[if IE 8]> <html class="no-js ie8 oldie" lang="en-US"> <![endif]-->
  6. <!--[if gt IE 8]><!--> <html class="no-js" lang="en-US"> <!--<![endif]-->
  7. <head>
  8. <title>Access denied | ip.cn used Cloudflare to restrict access</title>
  9. <meta charset="UTF-8" />
  10. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  11. <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1" />
  12. <meta name="robots" content="noindex, nofollow" />
  13. <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />
  14. <link rel="stylesheet" id="cf_styles-css" href="/cdn-cgi/styles/cf.errors.css" type="text/css" media="screen,projection" />
  15. <!--[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]-->
  16. <style type="text/css">body{margin:0;padding:0}</style>
  17. <!--[if gte IE 10]><!--><script type="text/javascript" src="/cdn-cgi/scripts/zepto.min.js"></script><!--<![endif]-->
  18. <!--[if gte IE 10]><!--><script type="text/javascript" src="/cdn-cgi/scripts/cf.common.js"></script><!--<![endif]-->
  19. </head>
  20. <body>
  21. <div id="cf-wrapper">
  22. <div class="cf-alert cf-alert-error cf-cookie-error" id="cookie-alert" data-translate="enable_cookies">Please enable cookies.</div>
  23. <div id="cf-error-details" class="cf-error-details-wrapper">
  24. <div class="cf-wrapper cf-header cf-error-overview">
  25. <h1>
  26. <span class="cf-error-type" data-translate="error">Error</span>
  27. <span class="cf-error-code">1020</span>
  28. <small class="heading-ray-id">Ray ID: 4f3821d838af9869 &bull; 2019-07-09 06:09:59 UTC</small>
  29. </h1>
  30. <h2 class="cf-subheadline">Access denied</h2>
  31. </div><!-- /.header -->
  32. <section></section><!-- spacer -->
  33. <div class="cf-section cf-wrapper">
  34. <div class="cf-columns two">
  35. <div class="cf-column">
  36. <h2 data-translate="what_happened">What happened?</h2>
  37. <p>This website is using a security service to protect itself from online attacks.</p>
  38. </div>
  39. </div>
  40. </div><!-- /.section -->
  41. <div class="cf-error-footer cf-wrapper">
  42. <p>
  43. <span class="cf-footer-item">Cloudflare Ray ID: <strong>4f3821d838af9869</strong></span>
  44. <span class="cf-footer-separator">&bull;</span>
  45. <span class="cf-footer-item"><span>Your IP</span>: 124.193.98.146</span>
  46. <span class="cf-footer-separator">&bull;</span>
  47. <span class="cf-footer-item"><span>Performance &amp; security by</span> <a href="https://www.cloudflare.com/5xx-error-landing?utm_source=error_footer" id="brand_link" target="_blank">Cloudflare</a></span>
  48. </p>
  49. </div><!-- /.error-footer -->
  50. </div><!-- /#cf-error-details -->
  51. </div><!-- /#cf-wrapper -->
  52. <script type="text/javascript">
  53. window._cf_translation = {};
  54. </script>
  55. </body>
  56. </html>

可以看到,容器启动后执行了命令 curl -s https://ip.cn

我们查看下容器

  1. [root@localhost dockerfile3]# docker ps -a
  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  3. f092015c299b cmddemo "curl -s https://ip.…" 3 minutes ago Exited (0) 3 minutes ago unruffled_dhawan

这个容器启动后,就又关闭了,如果我们想再看下效果,可以重新启动一下。

  1. docker start -i f092015c299b # f092015c299b是上面的 CONTAINER ID

docker run 命令是可以指定 COMMAND 的

docker run 的格式: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

所以我们还可以这样写

docker run -i cmddemo ls

  1. [root@localhost dockerfile3]# docker run -i cmddemo ls
  2. bin
  3. boot
  4. dev
  5. etc
  6. home
  7. lib
  8. lib64
  9. media
  10. mnt
  11. opt
  12. proc
  13. root
  14. run
  15. sbin
  16. srv
  17. sys
  18. tmp
  19. usr
  20. var

输出结果和上次的不一样了。我们查看一下容器,发现容器的 COMMAND 和上一个的不一样了。

  1. [root@localhost dockerfile3]# docker ps -a
  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  3. f1afb4d5bb37 cmddemo "ls" 37 seconds ago Exited (0) 36 seconds ago recursing_shirley
  4. f092015c299b cmddemo "curl -s https://ip.…" 13 minutes ago Exited (0) 6 minutes ago unruffled_dhawan

所以,如果我们在 docker run 的时候指定 COMMAND 后,Dockerfile 中的 COMMAND 就会被覆盖。

Dockerfile 中只有最后一个 CMD 有效。

比如 Dockerfile 如下 :

  1. FROM ubuntu
  2. COPY sources.list /etc/apt/sources.list
  3. RUN apt-get update && apt-get install -y curl
  4. CMD ["curl","-s","https://ip.cn"]
  5. CMD ["ls"]

自己可以试一下,和上面的操作大同小异。

ENTRYPOINT

入口点

<ENTRYPOINT> "<CMD>" 容器启动后,执行 <CMD> 命令

ENTRYPOINT ["JAVA","-JAR","/APP.JAR"] 容器启动后,执行 java -jar /app.jar 命令。

另外,docker run 中指定的指令会加在 ENTRYPOINT 的指令后面。

比如某 Dockerfile 内容如下:

  1. FROM ubuntu
  2. COPY sources.list /etc/apt/sources.list
  3. RUN apt-get update && apt-get install -y curl
  4. 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 文件如下:

  1. RUN curl -SLO "https://nodejs.org/dist/v7.2.0/node-v7.2.0-linux-x64.tar.xz" \
  2. && curl -SLO "https://nodejs.org/dist/v7.2.0/SHASUMS256.txt.asc" \

其中,node的版本 7.2.0 反复用到,我们可以单独提出来,将其声明为环境变量

修改如下:

  1. ENV NODE_VERSION 7.2.0
  2. RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  3. && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \

ENV 的使用方法如下:

  1. #声明
  2. ENV <key> <value>
  3. ENV <key1>=<value1> <key2>=<value2> ...
  1. #使用
  2. $NODE_VERSION

ARG

ARG 也是环境变量,只不过是只存活在构建脚本中,并不会存活在容器中。

VOLUME

VOLUME 定义匿名卷, VOLUME /data 表示容器中的 /data 目录挂载了一个匿名卷,也就是映射到了 Host 主机的一个位置。在容器 /data 中的数据不会随容器的丢失而丢失。

当然,也可以在 docker run 命令中指定卷。

docker run -d -v mydata:/data xxxx

格式:

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

比如,某 Dockerfile 内容如下:

  1. FROM ubuntu
  2. RUN mkdir /mydata
  3. VOLUME /mydata

构建镜像

docker build -t voldemo .

运行容器,并进入容器,执行命令

docker run -it voldemo bash

  1. # 通过 ls 命令,可以看到 mydata 目录。
  2. root@8b00c65392a7:/# ls
  3. bin boot dev etc home lib lib64 media mnt mydata opt proc root run sbin srv sys tmp usr var
  4. root@8b00c65392a7:/# cd mydata/
  5. # 在mydata目录中新建一个新的文件 new.txt
  6. root@8b00c65392a7:/mydata# touch new.txt
  7. # 退出
  8. root@8b00c65392a7:/mydata# exit

我们查看一下容器ID

  1. [root@localhost _data]# docker ps -a
  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  3. 8b00c65392a7 voldemo "bash" 16 minutes ago Exited (130) 11 minutes ago adoring_feistel

然后,我们查看下此容器的信息

docker inspect 8b00c65392a7 或者 docker inspect -f='{{json .Mounts}}' 8b00c65392a7

  1. [root@localhost _data]# docker inspect -f='{{json .Mounts}}' 8b00c65392a7
  2. [{"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 。

  1. [root@localhost _data]# cd /var/lib/docker/volumes/d71f19cae15aa50883b8fb2d12bca56f34eb7ad5053cd866433cbfca1693dee9/_data
  2. [root@localhost _data]# ls
  3. new.txt

说明 new.txt 文件存在于我们的 Host 主机中。
现在我们删除容器 8b00c65392a7,看 new.txt 文件是否消失。

  1. #删除
  2. docker rm 8b00c65392a7
  3. #查看容器是否还有
  4. docker ps -a
  5. #进入映射的文件夹
  6. cd /var/lib/docker/volumes/d71f19cae15aa50883b8fb2d12bca56f34eb7ad5053cd866433cbfca1693dee9/_data
  7. #查看文件
  8. 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 <用户名>[:<用户组>]

比如

  1. RUN groupadd -r redis && useradd -r -g redis redis
  2. USER redis
  3. RUN [ "redis-server" ]

如果以 root 执行的脚本,在执行期间希望改变身份,推荐使用 gosu

  1. # 建立 redis 用户,并使用 gosu 换另一个用户执行命令
  2. RUN groupadd -r redis && useradd -r -g redis redis
  3. # 下载 gosu
  4. RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.7/gosu-amd64" \
  5. && chmod +x /usr/local/bin/gosu \
  6. && gosu nobody true
  7. # 设置 CMD,并以另外的用户执行
  8. CMD [ "exec", "gosu", "redis", "redis-server" ]

HEALTHCHECK

HEALTHCHECK 用来检测容器是否正常。

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

格式为 HEALTHCHECK [选项] CMD <命令>

选项有:

--interval=<间隔>:两次健康检查的间隔,默认为 30 秒

--timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;

--retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。

新建 Dockerfile 为

  1. FROM nginx
  2. COPY sources.list /etc/apt/sources.list #sources.list 内容和上面的一样
  3. RUN apt-get update && apt-get install -y curl --allow-unauthenticated && rm -rf /var/lib/apt/lists/*
  4. HEALTHCHECK --interval=5s --timeout=3s \
  5. 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

  1. [root@localhost ~]# docker ps
  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  3. 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

扫码分享
版权说明
作者:SQBER
文章来源:http://www.sqber.com/articles/docker-other.html
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。