Dockerfile&Docker-Compose之基础
使用了很久的docker,之前却从来没有总结过, 于是开此篇来记录平常使用Dockerfile和docker-compose.yaml的点滴, 先从基础命令开始哦
[Dockerfile]
Dockerfile是一个文本文件,包含一条一条的指令, 每一条指令构建一层.所以为了减少层数, 可以将将多条语句一次性进行执行,而不是使用多条执行指令
官方镜像仓库:https://hub.docker.com/search?q=&type=image&image_filter=official
FROM:依赖的基础镜像
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制, 所以基础镜像是必须的,并且必须是第一条指令
scratch
。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。FROM scratch
...
scratch
为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。FROM scratch
会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是有人认为 Go 是特别适合容器微服务架构的语言的原因之一。
ENV: 设置环境变量
-
ENV <key> <value>
-
ENV <key1>=<value1> <key2>=<value2>...
RUN
,还是运行时的应用,都可以直接使用这里定义的环境变量。ENV VERSION=1.0 DEBUG=on \ NAME="Happy Feet"
这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。
定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方 node
镜像 Dockerfile
中,就有类似这样的代码:
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
在这里先定义了环境变量 NODE_VERSION
,其后的 RUN
这层里,多次使用 $NODE_VERSION
来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0
即可,Dockerfile
构建维护变得更轻松了。
WORKDIR: 指定工作目录
WORKDIR <工作目录路径>
。WORKDIR
指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR
会帮你建立目录。WORKDIR /app RUN echo "hello" > world.txt
如果你的 WORKDIR
指令使用的相对路径,那么所切换的路径与之前的 WORKDIR
有关:
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
COPY: 复制文件
格式:
-
COPY [--chown=<user>:<group>] <源路径>... <目标路径>
-
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
RUN
指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。COPY
指令将从构建上下文目录中 <源路径>
的文件/目录复制到新的一层的镜像内的 <目标路径>
位置。比如:COPY package.json /usr/src/app/ <源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如: COPY hom* /mydir/ COPY hom?.txt /mydir/
<目标路径>
可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR
指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
COPY
指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。在使用该指令的时候还可以加上 --chown=<user>:<group>
选项来改变文件的所属用户及所属组。
COPY --chown=55:mygroup files* /mydir/ COPY --chown=bin files* /mydir/ COPY --chown=1 files* /mydir/ COPY --chown=10:11 files* /mydir/
如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。
ADD: 更高级的复制文件
ADD
指令和 COPY
的格式和性质基本一致。但是在 COPY
基础上增加了一些功能。<源路径>
可以是一个 URL
,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径>
去。下载后的文件权限自动设置为 600
,如果这并不是想要的权限,那么还需要增加额外的一层 RUN
进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN
指令进行解压缩。所以不如直接使用 RUN
指令,然后使用 wget
或者 curl
工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。<源路径>
为一个 tar
压缩文件的话,压缩格式为 gzip
, bzip2
以及 xz
的情况下,ADD
指令将会自动解压缩这个压缩文件到 <目标路径>
去。ubuntu
中:FROM scratch ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz / ...
但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD
命令了。
COPY
,因为 COPY
的语义很明确,就是复制文件而已,而 ADD
则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD
的场合,就是所提及的需要自动解压缩的场合。ADD
指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。COPY
和 ADD
指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY
指令,仅在需要自动解压缩的场合使用 ADD
。--chown=<user>:<group>
选项来改变文件的所属用户及所属组。ADD --chown=55:mygroup files* /mydir/ ADD --chown=bin files* /mydir/ ADD --chown=1 files* /mydir/ ADD --chown=10:11 files* /mydir/
RUN: 执行命令
两种格式:
一. shell格式
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
二.exec格式
RUN ["可执行文件", "参数1", "参数2"]
,这更像是函数调用中的格式
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
也不例外。每一个 RUN
的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit
这一层的修改,构成新的镜像。更推荐:
FROM debian:stretch RUN set -x; 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
指令,并使用 &&
将各个所需命令串联起来。\
的命令换行方式,以及行首 #
进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。apt
缓存文件。
CMD: 容器启动命令
CMD
指令的格式和 RUN
相似,也是两种格式:-
shell
格式:CMD <命令>
-
exec
格式:CMD ["可执行文件", "参数1", "参数2"...]
-
参数列表格式:
CMD ["参数1", "参数2"...]
。在指定了ENTRYPOINT
指令后,用CMD
指定具体的参数。
CMD
指令就是用于指定默认的容器主进程的启动命令的。ubuntu
镜像默认的 CMD
是 /bin/bash
,如果我们直接 docker run -it ubuntu
的话,会直接进入 bash
。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release
。这就是用 cat /etc/os-release
命令替换了默认的 /bin/bash
命令了,输出了系统版本信息。exec
格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 "
,而不要使用单引号。
service nginx start
命令,则是希望 upstart 来以后台守护进程形式启动 nginx
服务。而刚才说了 CMD service nginx start
会被理解为 CMD [ "sh", "-c", "service nginx start"]
,因此主进程实际上是 sh
。那么当 service nginx start
命令结束后,sh
也就结束了,sh
作为主进程退出了,自然就会令容器退出。nginx
可执行文件,并且要求以前台形式运行。比如:CMD ["nginx", "-g", "daemon off;"]
cmd这是不支持参数, 如果传递参数将会覆盖cmd中的参数
ENTRYPOINT: 入口点
ENTRYPOINT
的格式和 RUN
指令格式一样,分为 exec
格式和 shell
格式。ENTRYPOINT
的目的和 CMD
一样,都是在指定容器启动程序及参数。ENTRYPOINT
在运行时也可以替代,不过比 CMD
要略显繁琐,需要通过 docker run
的参数 --entrypoint
来指定。ENTRYPOINT
后,CMD
的含义就发生了改变,不再是直接的运行其命令,而是将 CMD
的内容作为参数传给 ENTRYPOINT
指令,换句话说实际执行时,将变为:
<ENTRYPOINT> "<CMD>"
CMD
后,为什么还要有 ENTRYPOINT
呢?这种 <ENTRYPOINT> "<CMD>"
有什么好处么?让我们来看几个场景。CMD
来实现:FROM ubuntu:18.04 RUN apt-get update \ && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/* CMD [ "curl", "-s", "http://myip.ipip.net" ]
docker build -t myip .
来构建镜像的话,如果我们需要查询当前公网 IP,只需要执行:$ docker run myip
CMD
中可以看到实质的命令是 curl
,-i
参数。那么我们可以直接加 -i
参数给 docker run myip
么?$ docker run myip -i
executable file not found
。之前我们说过,跟在镜像名后面的是 command
,运行时会替换 CMD
的默认值。-i
替换了原来的 CMD
,而不是添加在原来的 curl -s http://myip.ipip.net
后面。而 -i
根本不是命令,所以自然找不到。-i
这参数,我们就必须重新完整的输入这个命令:ENTRYPOINT
就可以解决这个问题。现在我们重新用 ENTRYPOINT
来实现这个镜像:FROM ubuntu:18.04 RUN apt-get update \ && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/* ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]
docker run myip -i
:$ docker run myip
$ docker run myip -i HTTP/1.1 200 OK Server: nginx/1.8.0 Date: Tue, 22 Nov 2016 05:12:40 GMT Content-Type: text/html; charset=UTF-8 Vary: Accept-Encoding X-Powered-By: PHP/5.6.24-1~dotdeb+7.1 X-Cache: MISS from cache-2 X-Cache-Lookup: MISS from cache-2:80 X-Cache: MISS from proxy-2_6 Transfer-Encoding: chunked Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006 Connection: keep-alive 当前 IP:61.148.226.66 来自:北京市 联通
ENTRYPOINT
后,CMD
的内容将会作为参数传给 ENTRYPOINT
,而这里 -i
就是新的 CMD
,因此会作为参数传给 curl
,从而达到了我们预期的效果。mysql
类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。root
用户去启动服务,从而提高安全性,而在启动服务前还需要以 root
身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 root
身份执行,方便调试等。CMD
无关的,无论 CMD
为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 ENTRYPOINT
中去执行,而这个脚本会将接到的参数(也就是 <CMD>
)作为命令,在脚本最后执行。比如官方镜像 redis
中就是这么做的:FROM alpine:3.4 ... RUN addgroup -S redis && adduser -S -G redis redis ... ENTRYPOINT ["docker-entrypoint.sh"] EXPOSE 6379 CMD [ "redis-server" ]
ENTRYPOINT
为 docker-entrypoint.sh
脚本。#!/bin/sh ... # allow the container to be started with `--user` if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then find . \! -user redis -exec chown redis '{}' + exec gosu redis "$0" "$@" fi exec "$@"
CMD
的内容来判断,如果是 redis-server
的话,则切换到 redis
用户身份启动服务器,否则依旧使用 root
身份执行。比如:$ docker run -it redis id uid=0(root) gid=0(root) groups=0(root)
USER: 指定当前用户
USER <用户名>[:<用户组>]
USER
指令和 WORKDIR
相似,都是改变环境状态并影响以后的层。WORKDIR
是改变工作目录,USER
则是改变之后层的执行 RUN
, CMD
以及 ENTRYPOINT
这类命令的身份。USER
只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。RUN groupadd -r redis && useradd -r -g redis redis USER redis RUN [ "redis-server" ]
如果以 root
执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su
或者 sudo
,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 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.12/gosu-amd64" \ && chmod +x /usr/local/bin/gosu \ && gosu nobody true # 设置 CMD,并以另外的用户执行 CMD [ "exec", "gosu", "redis", "redis-server" ]
顺便整理下常用命令
# 示例 docker run -d \ --name roach \ --hostname db \ --network mynet \ -p 26257:26257 \ -p 8080:8080 \ -v roach:/cockroach/cockroach-data \ cockroachdb/cockroach:latest-v20.1 start-single-node \ --insecure run: 运行容器 --name: 指定容器名称 --hostname: 正常启动一个容器后,主机名称是一串编码, 可以使用该参数指定主机名(这个参数会直接将对应主机名写入机器的/etc/hostname)
--network: 指定使用的网络
-p: 端口映射
-v: 数据挂载
--insecure
[docker-compose.yml]
version:
services:
depends_on:
image:
build:
context
container_name:
hostname:
links:
networks:
ports:
environment:
deploy:
restart_policy:
condition:
volumes:
command:
volumes:
networks:
driver:
[.dockerignore]
该文件用于剔除不需要作为上下位传递给Docker引擎的
[相关命令]
1. 打包镜像
docker build -t nginx:v3 .
-t: 执行镜像的名称
.: 指定上下文目录
-f: 指定Dockerfile文件路径
build: 构建镜像, 同时还支持
1)URL构建, 例如从git repo中构建
docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world
这行命令指定了构建所需的 Git repo,并且指定分支为 master
,构建目录为 /amd64/hello-world/
,然后 Docker 就会自己去 git clone
这个项目、切换到指定分支、并进入到指定目录后开始构建。
2)从tar压缩包中构建
$ docker build http://server/context.tar.gz
如果所给出的 URL 不是个 Git repo,而是个 tar
压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。
3)从标准输入中读取Dockerfile进行构建
docker build - < Dockerfile
or
cat Dockerfile | docker build -
如果标准输入传入的是文本文件,则将其视为 Dockerfile
,并开始构建。这种形式由于直接从标准输入中读取 Dockerfile 的内容,它没有上下文,因此不可以像其他方法那样可以将本地文件 COPY
进镜像之类的事情。
4)从标准输入中读取上下文压缩包进行构建
$ docker build - < context.tar.gz
如果发现标准输入的文件格式是 gzip
、bzip2
以及 xz
的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建
2.运行容器
# docker run -it --name alpine-test1 --add-host=test.baidu.com:192.168.1.37 docker.io/alpine
-it: 使用一个伪终端执行
--name: 指定容器的名称
--add-host: 修改hosts文件中的域名解析配置 key:value
/ # cat /etc/hosts 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters 172.17.0.3 58068006c8b5 192.168.1.37 testgitlab.kuaidihelp.com / #
--restart: 指定容器的重启策略
no:
是默认的重启策略,docker容器如启动失败或意外停止后Docker Daemon不会尝试进行重启,除非手动启动容器,否则一直是stop的状态。
always:
docker run -d --restart=always --name=test-restart-always test-restart
表示永远重启,它的先决条件是如果你使用docker stop
命令停止了容器则docker不会自行启动该容器,但如果执行docker stop
命令后重启了Docker Daemon则该容器会触发always
重启策略,进而启动容器。
on-failure:
docker run -d --restart=on-failure:3 --name=test-restart-on-failure test-restart
重启策略的触发标准是当Docker Daemon
检测到容器非正常停止后则会执行该重启策略。on-failure
要求传入一个整型参数表示重启次数,当重启次数>=
该整数值则不会予以重启。
unless-stopped:
docker run -d --restart=unless-stopped --name=test-restart-unless-stopped test-restart
重启策略与always
很相似,唯一不同处在于unless-stopped
会在Docker Daemon
启动时会检测docker容器列表内的容器在上次停止时的状态,如果上一次容器停止时就是stop状态则不会启动该容器,否则启动该容器。
结论:docker daemon会在每次启动后检查容器列表中重启策略为unless-stopped
的容器,如若这些容器在docker daemon上次停止时最后状态是stop则不启动该容器,否则启动该容器。这也是它与alwarys策略的根本区别!!
文档参考:
- Dockerfile最佳实践: https://yeasy.gitbook.io/docker_practice/appendix/best_practices
- 各种官方镜像: https://github.com/docker-library/docs/tree/master/php