[TOC] > [参考](https://bingohuang.gitbooks.io/docker_practice/content/image/build.html) ## 概述 `docker commit`中,了解到镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 `Dockerfile` ## demo Dockerfile ``` FROM nginx RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html ``` ``` docker build -t nginx:v3 ./ 注释: -t 指定仓库与标签 ./ 为上下文的目录,所以不能那个绝对路劲,或者 ../ Dockerfile 无法理解 ``` ## Dockerfile 命令 ### RUN 执行命令 1. 方式一: shell 格式 命令: `RUN <命令>` `RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html` 2. 方式二: exec 格式 `RUN ["可执行文件", "参数1", "参数2"]` ### COPY 复制文件 ``` COPY <源路径>... <目标路径> COPY ["<源路径1>",... "<目标路径>"] ``` 如: ``` COPY package.json /usr/src/app/ COPY hom* /mydir/ COPY hom?.txt /mydir/ ``` 可是使用通配符,源路径可为绝对路径或者工作路径的相对路径 ### ADD 更高级的复制文件 尽可能的使用 COPY,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能 最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合 ``` FROM scratch //解压 tar.gz 并放到 / 目录 ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz / ... ``` ### CMD 容器启动命令 CMD 指令的格式和 RUN 相似,也是两种格式 在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 `"`,而不要使用单引号 ``` shell 格式:CMD <命令> exec 格式:CMD ["可执行文件", "参数1", "参数2"...] ``` 指定新的命令来替代镜像设置中的这个默认命令, 比如,ubuntu 镜像默认的 CMD 是` /bin/bash`,如果我们直接 `docker run -it ubuntu` 的话,会直接进入 bash。我们也可以在运行时指定运行别的命令,如 `docker run -it ubuntu cat /etc/os-release`。这就是用 `cat /etc/os-release` 命令替换了默认的 /bin/bash 命令了,输出了系统版本信息 如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行 ``` CMD echo $HOME //实际 CMD [ "sh", "-c", "echo $HOME" ] ``` Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 `upstart/systemd` 去启动后台服务,容器内没有后台服务的概念 **正确的守护进程** ``` //错误 CMD service nginx start //正确 CMD ["nginx", "-g", "daemon off;"] ``` ### ENTRYPOINT 入口点 查看 `ENTRYPOINT 入口点`目录 ### ENV 设置环境变量 格式有两种: ``` ENV <key> <value> ENV <key1>=<value1> <key2>=<value2>... ``` 如: ``` ENV VERSION=1.0 DEBUG=on \ NAME="Happy Feet" ``` 如何使用变量 `$NODE_VERSION` ``` 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" \ && 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 ``` ### VOLUME 定义匿名卷 对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些录挂载为匿名卷. **匿名卷** `VOLUME /data` 任何向 `/data` 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化 可通过运行时候指定,进行替换 `docker run -d -v mydata:/data xxxx ### EXPOSE 声明端口 `EXPOSE <端口1> [<端口2>...]` 只是用来申明端口,运行时并不会因为这个声明应用就会开启这个端口的服务, 另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口 ### WORKDIR 指定工作目录 `WORKDIR <工作目录路径>` ``` FROM python:2.7 WORKDIR /code ADD . /code ``` ### USER 指定当前用户 `USER <用户名>` USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份,用户需事先建立 ``` 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" ] ``` ## 其他说明 ### FROM scratch 空镜像 ``` FROM scratch ... ``` 如果你以 `scratch` 为基础镜像的话,意味着你不以任何镜像为基础,使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一 ### 不要过多使用 RUN 不好 ``` 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 ``` > 每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像 > 要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建 > 命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。这是很重要的一步,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉 ### 上下文路径 `COPY ./package.json /app/` 这并不是要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json。 因此,COPY 这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 `COPY ../package.json /app` 或者 `COPY /opt/xxxx /app` 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去