构建Dockerfile

Leon

基础命令

名称 作用 示例
docker systen df 查看镜像、容器、数据卷所占的空间
docker images -q 产生指定范围的id列表 docker image ls -q redis
docker image rm $() 批量删除指定镜像 docker image rm $(docker image ls -q redis)
docker run 基于镜像启动容器 docker run --name webserver -d -p 80:80 nginx
docker exec 进入容器 docker exec -it webserver /bin/bash
docker diff 查看容器存储层的改动 docker diff webserver
docker commit 将容器的存储层保存为镜像 docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]
docker history 具体查看镜像历史 docker history nginx:v2
docker export 导出本地容器 docker export c422162c86da >mysql.tar
docker import 导入容器快照到本地镜像仓库 cat mysql.tar | docker import - test/mysql:v1.0
docker save 保存镜像(推荐Docker Registry方式) docker save docker.io/mysql | gzip > mysql-test.tar.gz
docker load 加载镜像(推荐Docker Registry方式) docker load -i mysql-test.tar.gz

Dockerfile指令

名称 作用 示例
FROM 指定基础镜像 FROM scratch 指定一个空白的镜像
RUN 用来执行命令行命令的 RUN ["可执行文件", "参数1", "参数2"]
COPY 从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置 COPY <源路径>... <目标路径>
ADD 更高级的复制文件 (几乎不用它)所有复制文件用COPY,仅在需要自动解压缩的场合使用 ADD
CMD 容器启动命令 推荐使用 exec 格式 CMD ["可执行文件", "参数1", "参数2"...] 注意是双引号
ENTRYPOINT 入口点 1 让镜像变成像命令一样使用; 2 应用运行前的准备工作
ENV 设置环境变量 ENV <key1>=<value1> <key2>=<value2>...
ARG 构建参数 ARG <参数名>[=<默认值>]
VOLUME 定义匿名卷 VOLUME ["<路径1>", "<路径2>"...]
EXPOSE 声明运行时容器提供服务端口 EXPOSE <端口1> [<端口2>...]
WORKDIR 指定工作目录 WORKDIR <工作目录路径>
USER 指定当前用户 USER <用户名>
HEALTHCHECK 健康检查 HEALTHCHECK [选项] CMD <命令> :设置检查容器健康状况的命令
ONBUILD 为他人做嫁衣 ONBUILD <其它指令>

Dockerfile

  • 介绍
Dockerfile 是一个文本文件,里面包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容都是描述该层如何构建。
  • 使用dockerfile的好处
避免了docker commit方式带来的臃肿,实际应用中不应采用docker commit.
  • 基本流程
mkdir mynginx
cd mynginx
touch Dockerfile

vim Dockerfile,写入
FROM nginx
RUN echo '<h1>Hello, Docker!<h1>' > /usr/share/nginx/html/index.html

构建镜像
docker build -t nginx:v3 .  # 注意最后面有一个点

or docker build - < Dockerfile #从标准输入中读取 Dockerfile 进行构建
or cat Dockerfile | docker build -

or docker build http://server/context.tar.gz  #用给定的 tar 压缩包构建

or docker build https://github.com/twang2218/gitlab-ce-zh.git#:8.14 # 直接用 Git repo 进行构建
or docker build - < context.tar.gz # 从标准输入中读取上下文压缩包进行构建

正确构建

Dockerfile 中每一个指令都会建立一层, RUN 也不例外。每一个 RUN 的行为,
就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束
后, commit 这一层的修改,构成新的镜像。

错误的示例

    FROM debian:jessie
     RUN apt-get update
     RUN apt-get install -y gcc libc6-dev make
     RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.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

正确的写法

    FROM debian:jessie
     RUN buildDeps='gcc libc6-dev make' \
        && apt-get update \
        && apt-get install -y $buildDeps \
        && wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.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

关于上下文

  COPY ./package.json /app/
  这并不是要复制执行 docker build 命令所在的目录下的 package.json ,也不是复制
  Dockerfile 所在目录下的 package.json ,而是复制 上下文(context) 目录下的
  package.json 

ADD

FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...

CMD

shell格式: CMD echo $HOME
实际执行: CMD [ "sh", "-c", "echo $HOME" ]
Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,
用 upstart/systemd 去启动后台服务,容器内没有后台服务的概念。

CMD service nginx start
这个命令会被理解为 CMD [ "sh", "-c", "service nginx start"] ,因此主进程实际上是 sh 。
那么当 service nginx start 命令结束后, sh 也就结束了, sh 作为主进程退出了,自然就会令容器退出。

正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:
CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT

  • 场景一:让镜像变成像命令一样使用
  • 错误的示范
FROM ubuntu:16.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://ip.cn" ]

docker build -t myip .
docker run myip
docker run myip -i # 如果加上-i参数,就会报错
  • 正确的示范
FROM ubuntu:16.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]
  • 场景二:应用运行前的准备工作
启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。
官方redis镜像

FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 6379
CMD [ "redis-server" ]
docker-entrypoint.sh
#!/bin/sh
...
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
chown -R redis .
exec su-exec redis "$0" "$@"
fi
exec "$@"

ENV

官方node镜像
ENV NODE_VERSION 7.2.0
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.ta
r.xz" \
&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
&& grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
&& tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=
1 \
&& rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs
在这里先定义了环境变量 NODE_VERSION ,其后的 RUN 这层里,多次使用 $NODE_VERSION 来进行操作定制。
可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0 即可, Dockerfile 构建维护变得更轻松了。
下列指令可以支持环境变量展开:
ADD 、 COPY 、 ENV 、 EXPOSE 、 LABEL 、 USER 、 WORKDIR 、 VOLUME 、 STOPSIGNAL 、 ONBUILD 。

ARG

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

VOLUME

VOLUME /data
这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,
从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。
docker run -d -v mydata:/data xxxx
在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了Dockerfile 中定义的匿名卷的挂载配置。

EXPOSE

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

WORKDIR

  • 错误的示例
初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写
RUN cd /app
RUN echo "hello" > world.txt

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

USER

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层,用户需提前建好.
RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

HEALTHCHECK

FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
CMD curl -fs http://localhost/ || exit 1
这里我们设置了每 5 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如
果健康检查命令超过 3 秒没响应就视为失败,并且使用 curl -fs http://localhost/ || exit
1 作为健康检查命令。

查看 docker inspect --format '{{json .State.Health}}' web | python -m json.tool

ONBUILD

ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN , COPY 等,而这些指令,
在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。
Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。
  • 假设一个node.js的dockerfile如下
FROM node:slim
RUN mkdir /app
WORKDIR /app
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/
CMD [ "npm", "start" ]
  • 现在有其他的镜像依赖这个镜像,假设基础镜像叫my-node
FROM node:slim
RUN mkdir /app
WORKDIR /app
CMD [ "npm", "start" ]
  • 那么其他依赖的子项目的镜像为
FROM my-node
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/
基础镜像变化后,各个项目都用这个 Dockerfile 重新构建镜像,会继承基础镜像的更新
但是问题只解决了一半,如果这个子Dockerfile 里面有些东西需要调整呢?
比如 npm install 都需要加一些参数,那怎么办?这一行 RUN 是不可能放入基础镜像的,
因为涉及到了当前项目的 ./package.json ,难道又要一个个修改么?
所以说,这样制作基础镜像,只解决了原来的 Dockerfile 的前4条指令的变化问题,
而后面三条指令的变化则完全没办法处理。
  • 用ONBUILD重写基础镜像
FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]

在用ONBUILD构建基础镜像的时候,这三行并不会被执行
  • 那么子镜像只需
FROM my-node

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

参考文档

Docker 从入门到实践
阅读 1.2k

黑客与音乐家
音乐爱好者的计算机之旅

正确的方向比努力更重要

548 声望
16 粉丝
0 条评论

正确的方向比努力更重要

548 声望
16 粉丝
宣传栏