Dockerfile 指令 - (ADD)(ENTRYPOINT)(CMD)(COPY)
ADD 指令和 COPY指令的格式和性质基本一致,但是ADD增加了自动解压缩的功能。
比如<源路径>可以是一个 URL ,Docker 引擎会试图去下载,然后放置到 <目标路径>。下载后的文件权限自动设置为 600 ,如果需要更改权限,需要在增加一层 RUN 进行权限修改。
如果下载的是一个压缩包,一般情况是 RUN 指令,然后使用wget或curl去下载,然后更改权限、解压、清理下载的源文件。
对于压缩包文件,使用 ADD 命令,将会自动解压缩文件到 <目标路径>。压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。
指定一个文件,文件位置
[root@ip-10-1-0-142 addfile]# tree
.
├── Dockerfile
└── moban
└── moban4335.rar1 directory, 2 files
查看Dockerfile 内容
[root@ip-10-1-0-142 addfile]# cat Dockerfile
FROM scratch
ADD moban4335.rar /
构建一个镜像的过程
[root@ip-10-1-0-142 addfile]# docker build -f Dockerfile -t dockeradd:v0.1 moban
Sending build context to Docker daemon 2.593MB
Step 1/2 : FROM scratch
--->
Step 2/2 : ADD moban4335.rar /
---> b8d99119a5c5
Successfully built b8d99119a5c5
Successfully tagged dockeradd:v0.1
<源文件>为 URL
Dockerfile 内容如下,
[root@ip-10-1-0-142 addfile]# cat Dockerfile
FROM nginx
ADD https://cdn.mysql.com/archives/mysql-8.0/mysql-8.0.25-linux-glibc2.12-i686.tar.xz /home/mysql
构建镜像
[root@ip-10-1-0-142 addfile]# docker build -f Dockerfile -t dockeradd:v0.1 .
Sending build context to Docker daemon 831.2MB
Step 1/2 : FROM nginx
---> 08b152afcfae
Step 2/2 : ADD https://cdn.mysql.com/archives/mysql-8.0/mysql-8.0.25-linux-glibc2.12-i686.tar.xz /home/mysql
Downloading 831.2MB/831.2MB
---> f15a33b49156
Successfully built f15a33b49156
Successfully tagged dockeradd:v0.1
运行一个容器
[root@ip-10-1-0-142 addfile]# docker run -itd --name dockeradd dockeradd:v0.1 bash
b7feda784f2e47a4077e3af089426fa2f1975807bbc0eda607b140e6ff89c741
遵循 Docker 官方的Dockerfile 最佳实践文档中的要求,尽可能使用COPY ,因为 COPY 的语义很明确,就是复制的功能,不包含其他的功能。
最适合使用 ADD 指令的场合,就是需要自动解压缩的场合。
另外,需要注意的是,ADD 指令会令镜像构建缓存失效,从而可能会镜像构建变得非常缓慢。
在使用 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/
ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式 和 shell 格式
ENTRYPOINT 的目的和 CMD 一样,都是在指定的容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。
当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:
<ENTRYPOINT> "<CMD>"
场景一:让镜像变成像命令一样使用
传参的用途
使用一个公网IP的镜像来得知自己当前的公网IP,那么可以先用 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 构建镜像
[root@ip-10-1-0-142 ipip]# docker build -t myip:v1 .
Sending build context to Docker daemon 2.048kB
Step 1/3 : FROM ubuntu:18.04
18.04: Pulling from library/ubuntu
feac53061382: Pull complete
Digest: sha256:7bd7a9ca99f868bf69c4b6212f64f2af8e243f97ba13abb3e641e03a7ceb59e8
Status: Downloaded newer image for ubuntu:18.04
---> 39a8cfeef173
Step 2/3 : RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
---> Running in 35433ebb43d4
Get:1 http://archive.ubuntu.com/ubuntu bionic InRelease [242 kB]
Get:2 http://archive.ubuntu.com/ubuntu bionic-updates InRelease [88.7 kB]
Get:3 http://security.ubuntu.com/ubuntu bionic-security InRelease [88.7 kB]
.
.
.
Running hooks in /etc/ca-certificates/update.d...
done.
Removing intermediate container 35433ebb43d4
---> 4d4e2a6ea83b
Step 3/3 : CMD [ "curl", "-s", "http://myip.ipip.net" ]
---> Running in 30a3a5f2f303
Removing intermediate container 30a3a5f2f303
---> 2c191ad0c4c5
Successfully built 2c191ad0c4c5
Successfully tagged myip:v1
运行myip
[root@ip-10-1-0-142 ipip]# docker run myip
当前 IP:52.215.213.34 来自于:爱尔兰 都柏林郡 都柏林 amazon.com
嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 CMD 中可以看到实质的命令是 curl,那么如果我们希望显示 HTTP 头信息,就需要加上 -i 参数。那么我们可以直接加 -i 参数给 docker run myip 么?
[root@ip-10-1-0-142 ipip]# docker run myip -i
docker: Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: exec: "-i": executable file not found in $PATH: unknown.
ERRO[0000] error waiting for container: context canceled
我们可以看到可执行文件找不到的报错,executable file not found。之前我们说过,跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的 CMD,而不是添加在原来的 curl -s http://myip.ipip.net 后面。而 -i 根本不是命令,所以自然找不到。
那么如果我们希望加入 -i 这参数,我们就必须重新完整的输入这个命令:
[root@ip-10-1-0-142 ipip]# docker run myip curl -s http://myip.ipip.net -i
HTTP/1.1 200 OK
Date: Mon, 02 Aug 2021 11:00:28 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 84
Connection: keep-alive
CF-Cache-Status: DYNAMIC
Report-To: {"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v3?s=RUCd8%2BCdlxsaLUf44f7INb92ZCnaIYiD4X2GYbLNyXJTiKg%2BOsAfEp3bPXOnfJrfRFnNDfyAP9p058HJ06wjUpA44ezop7eQEpLUBq37DiBP4GXI2YMaAt%2FkO%2Fz7Xx0%3D"}],"group":"cf-nel","max_age":604800}
NEL: {"report_to":"cf-nel","max_age":604800}
Server: cloudflare
CF-RAY: 6786cd23feed1e89-AMS当前 IP:52.215.213.34 来自于:爱尔兰 都柏林郡 都柏林 amazon.com
我们尝试使用 ENTRYPOINT 解决这个问题,使用 ENTRYPOINT 更新 Dockerfile 文件
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 myipe 查看输出结果,
[root@ip-10-1-0-142 ipip]# docker run myipe
当前 IP:52.215.213.34 来自于:爱尔兰 都柏林郡 都柏林 amazon.com
这次我们再来尝试直接使用 docker run myip -i:
[root@ip-10-1-0-142 ipip]# docker run myipe -i
HTTP/1.1 200 OK
Date: Mon, 02 Aug 2021 11:04:59 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 84
Connection: keep-alive
CF-Cache-Status: DYNAMIC
Report-To: {"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v3?s=K2An7ZP3qbdYTYdDYzojNsNZSi2jbEPs8N%2BQ7pwalGsr6kYLF9RE2ZSU2l%2FekmmBabeN9h%2FqBSwXZ2SuW%2F9RPbSx76YnYUtOAkSFGeC1CXEu6GyUWuRM7sB4Z%2BCyG4I%3D"}],"group":"cf-nel","max_age":604800}
NEL: {"report_to":"cf-nel","max_age":604800}
Server: cloudflare
CF-RAY: 6786d416eaab53ec-LHR当前 IP:52.215.213.34 来自于:爱尔兰 都柏林郡 都柏林 amazon.com
运行 docker run myipe -i 执行成功。是因为当 ENTRYPOINT 后,CMD 的内容被作为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,因此会作为参数传给 curl,从而达到想要的结果,可以查看到HTTP 头的信息。
场景二 应用运行前的准备工作
启动容器就是启动主进程,有些时候需要在启动主进程前做一些准备工作。
比如mysql的数据库,需要做数据库的配置、初始化等,这些动作需要在启动mysql 服务之前准备好。
另外,避免使用root用户去启动服务,从提高安全性来讲,在启动前需要使用root做一些准备工作,然后在使用普通服务用户去启动服务。
这些准备工作和容器 CMD 无关,无论 CMD 为什么指令,都需要预先做一个预处理的工作。
使用脚本,然后利用 ENTRYPOINT 传参执行,脚本会接到参数作为命令,在脚本最后执行。
官方镜像 redis 是这么做的:
[root@ip-10-1-0-142 redis]# tree
.
├── docker-entrypoint.sh
└── redis-server0 directories, 2 files
FROM alpine:3.4
RUN addgroup -S redis && adduser -S -G redis redis
ENTRYPOINT ["docker-entrypoint.sh"]EXPOSE 6379
CMD [ "redis-server" ]
可以看到其中为 redis 服务创建 redis 用户,并在最后指定了 ENTRYPOINT 为 docker-entrypoint.sh 脚本。
!/bin/sh
# allow the container to be started with
--user
if [ "(id -u)" = '0' ]; then
find . ! -user redis -exec chown redis '{}' +
exec gosu redis "@"
fiexec "$@"
该脚本的内容就是根据 CMD 的内容来判断,如果是 redis-server ,则切换到 redis 用户身份启动服务器,否则依旧使用 root 身份执行。比如,
$ docker run -it redis id
uid=0(root) gid=0(root) groups=0(root)
CMD 指令的格式和 RUN 相似,也是两种格式:
- shell 格式:CMD <命令>
- exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
- 参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。
容器是一个进程。那么在启动容器的时候,就需要指定所运行的程序及参数。
CMD 指令是用于指定默认的容器主进程的启动命令的,CMD 类属于 RUN 命令,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 数组,因此一定要使用双引号 ",而不要使用单引号。
如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:
CMD echo $HOME
在实际执行中,会将其变更为:
CMD [ "sh", "-c", "echo $HOME" ]
这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。
提到 CMD 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。
Docker 不像虚拟机,在容器中的应用都应该以前台执行,不像虚拟机那样,用 systemd 去启动后台服务,容器内没有后台服务饿概念。
如果 CMD 指令写成:
CMD service nginx start
然后发现容器执行后就立即退出了。甚至在容器内发现无法执行 systemctl 的指令。
对于容器而言,启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义。
而使用 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 是生效的。
编写Dockerfile 文件
[root@ip-10-1-0-142 mynginx]# cat Dockerfile
FROM nginx:latest
COPY hello.txt home/
COPY html/index.html /usr/share/nginx/
CMD ["nginx","-g","daemon off;"]
构建镜像的步骤
[root@ip-10-1-0-142 mynginx]# docker build -f Dockerfile -t nginx:v2 app
Sending build context to Docker daemon 3.631kB
Step 1/4 : FROM nginx:latest
---> 08b152afcfae
Step 2/4 : COPY hello.txt home/
---> f471772839c9
Step 3/4 : COPY html/index.html /usr/share/nginx/
---> c6c9239fc8a9
Step 4/4 : CMD ["nginx","-g","daemon off;"]
---> Running in ef82c58b610d
Removing intermediate container ef82c58b610d
---> 808dddfc65d6
Successfully built 808dddfc65d6
Successfully tagged nginx:v2
一、Docker 介绍
简介 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器或Windows 机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。
基于内核虚拟化的技术,使用namespace进行用户隔离,利用cgroup进行资源控制。
容器与虚拟化的不同
Cgroup
Cgroup是control group,又称为控制组,它主要是做资源控制。原理是将一组进程放在放在一个控制组里,通过给这个控制组分配指定的可用资源,达到控制这一组进程可用资源的目的。
Namespace
Namespace又称为命名空间,它主要做访问隔离。其原理是针对一类资源进行抽象,并将其封装在一起提供给一个容器使用,对于这类资源,因为每个容器都有自己的抽象,而他们彼此之间是不可见的,所以就可以做到访问隔离。
容器的优势:
- 启动快
- 资源占用少
- 体积小
二、docker的安装
在CentOS上安装,可以采用以下安装方式:
yum install docker
yum install docker-engine
yum install docker-ce
docker-ce 的安装是最新的安装方式,在安装docker的时候,需要提前移除之前的旧的docker环境,
sudo yum install -y yum-utils
sudo yum install docker-ce docker-ce-cli containerd.io
三、镜像获取
镜像文件搜索
docker search centos
[root@ip-10-1-0-142 ~]# docker search centos
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
centos The official build of CentOS. 6659 [OK]
ansible/centos7-ansible Ansible on Centos7 134 [OK]
consol/centos-xfce-vnc Centos container with "headless" VNC session… 129 [OK]
jdeathe/centos-ssh OpenSSH / Supervisor / EPEL/IUS/SCL Repos - … 118 [OK]
centos/systemd systemd enabled base container. 100 [OK]
centos/mysql-57-centos7 MySQL 5.7 SQL database server 90
imagine10255/centos6-lnmp-php56 centos6-lnmp-php56 58 [OK]
tutum/centos Simple CentOS docker image with SSH access 48
镜像加载
docker load -i <image.name>
拉取镜像
docker pull centos:latest
列出镜像
$ docker images ls
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 08b152afcfae 4 days ago 133MB
centos latest 300e315adb2f 7 months ago 209MB
中间层镜像
$ docker image ls -a
- 为了加速镜像构建、重复利用资源,Docker 会利用 中间层镜像。所以在使用一段时间后,可能会看到一些依赖的中间层镜像。默认的 docker image ls 列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 -a 参数。
删除本地镜像
如果要删除本地的镜像,可以使用 docker image rm 命令
$ docker image rm [选项] <镜像1> [<镜像2> ...]
[root@ip-10-1-0-142 ~]# docker rmi nginx
Untagged: nginx:latest
Untagged: nginx@sha256:8f335768880da6baf72b70c701002b45f4932acae8d574dedfddaf967fc3ac90
Deleted: sha256:08b152afcfae220e9709f00767054b824361c742ea03a9fe936271ba520a0a4b
Deleted: sha256:97386f823dd75e356afac10af0def601f2cd86908e3f163fb59780a057198e1b
Deleted: sha256:316cd969204ae854302bc55c610698829c9f23fa6fcd4e0f69afa6f29fedfd68
Deleted: sha256:dcec23d16cb7cdbd725dc0024f38b39fd326066fc59784df92b40fc05ba3728f
Deleted: sha256:1e294000374b3a304c2bfcfe51460aa599237149ed42e3423ac2c3f155f9b4a5
Deleted: sha256:c0d318592b21711dc370e180acd66ad5d42f173d5b58ed315d08b9b09babb84a
Deleted: sha256:814bff7343242acfd20a2c841e041dd57c50f0cf844d4abd2329f78b992197f4
镜像体积
可以通过docker system df 命令来便捷查看images containers volumes所占用的空间
[root@ip-10-1-0-142 ~]# docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 2 1 342.5MB 133.2MB (38%)
Containers 1 1 0B 0B
Local Volumes 0 0 0B 0B
Build Cache 0 0 0B 0B
运行容器
docker run -itd centos:latest /bin/sh -c "while true;do echo hello world;sleep 1;done"
参数介绍:
- -it:这是两个参数,一个是 -i:交互式操作,一个是 -t 终端。我们这里打算进入 bash 执行一些命令并查看返回结果,因此我们需要交互式终端。
- --rm:这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 docker rm。我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用 --rm 可以避免浪费空间。
- ubuntu:18.04:这是指用 ubuntu:18.04 镜像为基础来启动容器。
- bash:放在镜像名后的是 命令,这里我们希望有个交互式 Shell,因此用的是 bash。
开机自动重启
docker run --restart=always -itd --name webserver nginx:latest bash
- --restart=always 在容器退出时总是重启容器
使用commit理解镜像构成
docker commit 命令除了学习之外,还有一些特殊的场合适合使用,比如被入侵后保存现场等。
定制镜像,应该使用 Dockerfile 来完成,不要使用 docker commit 定制镜像。
镜像是容器的基础,每次执行 docker run 的时候都会指定哪个镜像作为容器运行的基础。
直接使用镜像可以满足一定的需求,当镜像无法直接满足的时候,就需要定制镜像。
定制一个 web 应用服务器
[root@ip-10-1-0-142 ~]# docker run --name webserver -itd -p 80:80 nginx:latest
e829ded69fa81c0afddcce54109898e02e15a205cf07e63d101de8c572dc7a54
直接用浏览器访问
定制化web页面,可以将文字进行更改,使用docker exec 命令进入到容器,再进行内容修改
[root@ip-10-1-0-142 ~]# docker exec -it webserver bash
root@e829ded69fa8:/# echo '<h1>Hello, Word!</h1>' > /usr/share/nginx/html/index.html
修改了容器的文件,也就是改动了容器的存储层。使用 docker diff 命令查看具体的改动
修改之前
[root@ip-10-1-0-142 ~]# docker diff e8
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
C /run
A /run/nginx.pid
C /etc
C /etc/nginx
C /etc/nginx/conf.d
C /etc/nginx/conf.d/default.conf
修改之后
[root@ip-10-1-0-142 ~]# docker diff webserver
C /run
A /run/nginx.pid
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
C /usr/share/nginx/html/index.html
C /root
A /root/.bash_history
C /etc
C /etc/nginx
C /etc/nginx/conf.d
C /etc/nginx/conf.d/default.conf
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
镜像定制好,然后进行镜像保存。
我们运行在一个容器的时候(不使用卷),我们的任何文件修改都会被记录于容器存储层里。而Docker 提供了一个docker commit 命令,可以将容器的存储层保存下来成为镜像。也就是说,在原有的镜像基础上,再叠加容器的存储层,构建成新的镜像。
docker commit 的语法格式为:
docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]
[root@ip-10-1-0-142 ~]# docker commit
--author "BigMay"
--message "modify html"
webserver
nginx:v2
sha256:a09c32d8e04de96e40148839d9fce30f8bee17c5138c2745e6e44199576b0534
- 其中 --author 是指定修改的作者,而 --message 则是记录本次修改的内容。这点和 git 版本控制相似,不过这里这些信息可以省略留空。
查看定制好的镜像文件
[root@ip-10-1-0-142 ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx v2 a09c32d8e04d About a minute ago 133MB
nginx latest 08b152afcfae 4 days ago 133MB
httpd latest 73b8cfec1155 4 days ago 138MB
centos latest 300e315adb2f 7 months ago 209MB
还可以使用 docker history 具体查看镜像内的历史记录,对比nginx:latest 和 nginx:v2的历史记录
使用新的镜像部署容器
[root@ip-10-1-0-142 ~]# docker run -it --name webservr2 -p 8080:80 nginx:v2
慎用 docker commit
使用 docker commit 可以比较直观的理解镜像分层存储的概念,但是在实际的环境中不会这么使用。
在修改文件的过程中,由于命令的执行,可能会有多个文件被动或添加了。不仅仅是简单的操作,如果是安装软件包、编译构建,就可能会有大量的无关内容被添加进来,这样会导致镜像非常臃肿。
另外,使用 docker commit 就是对所有的镜像的操作都是黑箱操作,生成的镜像被称为 黑箱镜像 。也就是说,除了制作镜像的人知道执行了什么命令、怎么生成的镜像,别人无法知道做过哪些操作。而且,如果制作镜像的人,过一段时间后无法记清具体的操作。这种黑箱镜像的维护工作就非常痛苦。
回顾之前提及的镜像所使用的分层存储的概念,除了当前层外,之前的每一层都是不会发生改变的,任何修改的结果只是在当前层进行了标记、添加、修改,不会改动上一层。
使用Dockerfile 定制镜像
Dockerfile 是一个文本文件,其中包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
对比 docker commit,使用Dockerfile 可以吧每一层修改、安装、构建、操作的命令都写入一个脚本,利用这个脚本来构建、定制镜像。
以nginx镜像为例,使用 Dockerfile 来定制。
首先,在一个空白目录中,建立一个文本文件,命名为 Dockerfile :
[root@ip-10-1-0-142 ~]# mkdir mynginx
[root@ip-10-1-0-142 ~]# cd mynginx/
[root@ip-10-1-0-142 mynginx]# touch Dockerfile
[root@ip-10-1-0-142 mynginx]# vim Dockerfile
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
非常简单,只有两行,只涉及到两条指令,FROM 和 RUN
FROM 指定基础镜像
定制镜像就是以一个镜像为基础,在其上进行定制。
FROM 就是指定的 基础镜像,所以在一个Dockerfile 中 FROM的指令是必备的,并且必须是第一条指令。
在 Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginx
、redis
、mongo
、mysql
、httpd
、php
、tomcat
等;也有一些方便开发、构建、运行各种语言应用的镜像,如 node
、openjdk
、python
、ruby
、golang
等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntu
、debian
、centos
、fedora
、alpine
等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch
。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
FROM scratch
...
如果你以 scratch
为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch
会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。
RUN 执行命令
RUN 指令是用来执行命令行命令的。由于命令行的强大能力,RUN 指令在指定镜像时是最常用的指令之一。其格式有两种:
shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。
既然 RUN 就像shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个RUN呢?比如:
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
其实这样也是可以的,只是创建了7层。
Dockerfile 中每一个指令都会建立一层, RUN 也不例外。每一个 RUN 的行为,都会新建立一层,在其上执行这些命令,待执行结束后,commit 这一层的修改,构建新的镜像。
再部署一个应用或一个环境的时候,把每一个步骤的执行建立一层是没有必要的,很多运行时不需要的东西都单独封装在一层,这样会导致image 非常臃肿。比如编译环境、更新的软件包等等。不仅让image产生了臃肿和多层,而且还非常耗时。
Union FS是有最大层数限制的,比如AUFS,曾经是最大不超过42层,现在是不超过127层
上面的Dockerfile 正确的写法是:
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
首先,之前所有的命令只有一个目的,就是编译安装redis可执行文件。只是创建一层的需求,只使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。将7个 RUN ,简化为一个 RUN,1层。
所以,在撰写 Dockerfile 的时候,要经常提醒自己,不是在写 shell 脚本,而是在定义每一层要如何构建。
并且,这里为了格式化海进行了换行。Dockerfile 支持 shell 类的行尾添加 \ 的命令换行方式,以及可以使用 # 进行注释的格式。
良好的格式,比如换行、缩紧、注释等,会让维护、排障更为容易,是一个比较良好的习惯。
构建镜像
使用定制的 nginx 镜像的 Dockerfile来构建镜像。
在 Dockerfile 文件所在的目录执行:
[root@ip-10-1-0-142 mynginx]# docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM nginx
---> 08b152afcfae
Step 2/2 : RUN echo '<h1>Hello, Word!</h1>' > /usr/share/nginx/html/index.html
---> Running in c71a877f386f
Removing intermediate container c71a877f386f
---> 31aa55b35c46
Successfully built 31aa55b35c46
Successfully tagged nginx:v3
从命令输出的结果,可以清晰的看到镜像构建的过程。
在 Step 2 中,RUN 指令启动了一个容器 c71a877f386f ,执行了所要求的命令,并最后提交了这一层 31aa55b35c46 ,随后删除了所用到的这个容器 c71a877f386f 。
使用 docker build 命令进行镜像构建。格式为:
docker build [选项] <上下文路径/URL/->
在这里我们指定了最终镜像的名称 -t nginx:v3,构建成功后,我们可以像之前运行 nginx:v2 那样来运行这个镜像,其结果会和 nginx:v2 一样。
镜像构建上下文(Context)
在运行 docker build 命令最后有一个 . 。. 表示当前目录,而 Dockerfile 就在当前目录,这是指定上下文路径。上下文是什么?
首先我们要理解 docker build 的工作原理。Docker 在运行时分为Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,如 docker 命令这样的客户端工具,则是通过这组API 与 Docker 引擎交互来完成各种功能。
所以,看似我们是在本机执行各种 docker 功能命令,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 架构设计,让我们操作远程服务器的Docker 引擎变得简单。
当我们进行镜像构建的时候,并非所有定制都会通过 RUN 指令完成,经常会需要将一些本地文件复制到镜像,比如通过 copy 指令、add 指令等等。而docker build 命令构建镜像,是在服务端进行的,不是在本地进行的,也就是在 docker engine 中构建的。那么,如何才能让服务器端获得本地的文件呢?
这就引入了上下文的概念,当构建的时候,用户会指定构建镜像上下文的路径, docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传到 Docker 引擎。Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
如果在 Dockerfile 中这么写:
COPY ./package.json /app/
这不是要复制执行 docker build 命令所在目录下的 package.json文件,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context)目录下的 package.json 文件。
因此,COPY 这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 COPY ../package.json /app 或者 COPY /opt/xxxx /app 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。
现在就可以理解刚才的命令 docker build -t nginx:v3 . 中的这个 .,实际上是在指定上下文的目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。
如果观察 docker build 输出,我们其实已经看到了这个发送上下文的过程:
[root@ip-10-1-0-142 mynginx]# docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048kB
理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 COPY /opt/xxxx /app 不工作后,于是干脆将 Dockerfile 放到了硬盘根目录去构建,结果发现 docker build 执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 docker build 打包整个硬盘,这显然是使用错误。
一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。
那么为什么会有人误以为 . 是指定 Dockerfile 所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile。
[root@ip-10-1-0-142 mynginx]# docker build -t nginx:v5 -f demo .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM nginx
---> 08b152afcfae
Step 2/2 : RUN echo '<h1>Hello, Word!</h1>' > /usr/share/nginx/html/index.html
---> Using cache
---> 31aa55b35c46
Successfully built 31aa55b35c46
Successfully tagged nginx:v5
一般情况下都会使用默认的文件名 Dockerfile,以及会将其放置于镜像构建上下文目录中。
其他 docker build 的用法
直接用 Git repo 进行构建
[root@ip-10-1-0-142 mynginx]# docker build --help
Usage: docker build [OPTIONS] PATH | URL | -
Build an image from a Dockerfile
Options:
--add-host list Add a custom host-to-IP mapping (host:ip)
--build-arg list Set build-time variables
--cache-from strings Images to consider as cache sources
--cgroup-parent string Optional parent cgroup for the container
--compress Compress the build context using gzip
--cpu-period int Limit the CPU CFS (Completely Fair Scheduler) period
--cpu-quota int Limit the CPU CFS (Completely Fair Scheduler) quota
-c, --cpu-shares int CPU shares (relative weight)
--cpuset-cpus string CPUs in which to allow execution (0-3, 0,1)
--cpuset-mems string MEMs in which to allow execution (0-3, 0,1)
--disable-content-trust Skip image verification (default true)
-f, --file string Name of the Dockerfile (Default is 'PATH/Dockerfile')
--force-rm Always remove intermediate containers
--iidfile string Write the image ID to the file
--isolation string Container isolation technology
--label list Set metadata for an image
-m, --memory bytes Memory limit
--memory-swap bytes Swap limit equal to memory plus swap: '-1' to enable unlimited swap
--network string Set the networking mode for the RUN instructions during build (default
"default")
--no-cache Do not use cache when building the image
--pull Always attempt to pull a newer version of the image
-q, --quiet Suppress the build output and print image ID on success
--rm Remove intermediate containers after a successful build (default true)
--security-opt strings Security options
--shm-size bytes Size of /dev/shm
-t, --tag list Name and optionally a tag in the 'name:tag' format
--target string Set the target build stage to build.
--ulimit ulimit Ulimit options (default [])
[root@ip-10-1-0-142 mynginx]# docker build -f nginx:v4 ./dem
docker build 还支持从 URL 构建,比如可以直接从 Git repo 中构建:
[root@ip-10-1-0-142 mynginx]# !229
docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world
Sending build context to Docker daemon 19.46kB
Step 1/3 : FROM scratch
--->
Step 2/3 : COPY hello /
---> 73f19933b2d9
Step 3/3 : CMD ["/hello"]
---> Running in 0c6155341ab9
Removing intermediate container 0c6155341ab9
---> dd3f92cd2824
Successfully built dd3f92cd2824
Successfully tagged hello-world:latest
如果出现错误信息,如下:
[root@ip-10-1-0-142 mynginx]# docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world
unable to prepare context: unable to find 'git': exec: "git": executable file not found in $PATH
先在系统安装git,如 yum install -y git ,然后再进行镜像 build
用给定的 tar 压缩包构建
docker build http://<server-001>/context.tar.gz
如果所给出的 URL 不是个 Git repo,而是个 tar 压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。
从标准输入中读取 Dockerfile 进行构建
docker build -t nginx:v1 - < Dockerfile
[root@ip-10-1-0-142 mynginx]# docker build -t nginx:v1 - < demo
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM nginx
---> 08b152afcfae
Step 2/2 : RUN echo '<h1>Hello, Word!</h1>' > /usr/share/nginx/html/index.html
---> Running in 4a04eff723cd
Removing intermediate container 4a04eff723cd
---> a772a8d3759e
Successfully built a772a8d3759e
Successfully tagged nginx:v1
或
cat Dockerfile | docker build -
[root@ip-10-1-0-142 mynginx]# cat demo | docker build -t nginx:v2 -
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM nginx
---> 08b152afcfae
Step 2/2 : RUN echo '<h1>Hello, Word!</h1>' > /usr/share/nginx/html/index.html
---> Using cache
---> a772a8d3759e
Successfully built a772a8d3759e
Successfully tagged nginx:v2
对Docker 构建上下文的理解误区
我们都知道,构建一个 Docker 镜像非常简单,大家一般都会这么做(当然这么做是完全正确的):
- 1.跳到 Dockerfile 所在目录;
- 2.执行 docker build 构建命令:
docker build -t <imageName:imageTag> .
通过上面的工作流,很容易形成这样的理解误区:
- docker build 后面的 . 为 Dockerfile 所在的目录;
- Dockerfile 文件名 必须为 Dockerfile;
其实上面这种理解是错误的,要想准确理解其含义,首先我们需要先了解下 Docker 的架构和 docker build 的工作原理。
理解 Docker 的架构
Docker 是一个典型的 C/S 架构的应用,分为 Docker 客户端(即平时敲的 docker 命令) Docker 服务端(dockerd 守护进程)。
Docker 客户端通过 REST API 和服务端进行交互,docker 客户端每发送一条指令,底层都会转化成 REST API 调用的形式发送给服务端,服务端处理客户端发送的请求并给出响应。
Docker 镜像的构建、容器创建、容器运行等工作都是 Docker 服务端来完成的,Docker 客户端只是承担发送指令的角色。
Docker 客户端和服务端可以在同一个宿主机,也可以在不同的宿主机,如果在同一个宿主机的话,Docker 客户端默认通过 UNIX 套接字(/var/run/docker.sock)和服务端通信。
理解 docker build 的工作原理
理解了 Docker 的架构就很容易理解 docker build 构建镜像的工作原理了。docker build 构建镜像的流程大概如下:
执行 docker build -t <imageName:imageTag> . ;
Docker 客户端会将构建命令后面指定的路径(.)下的所有文件打包成一个 tar 包,发送给 Docker 服务端;
Docker 服务端收到客户端发送的 tar 包,然后解压,根据 Dockerfile 里面的指令进行镜像的分层构建;
正确理解 Docker 构建上下文
了解了 Docker 的架构和镜像构建的工作原理后,Docker 构建上下文也就容易理解了。Docker 构建上下文就是 Docker 客户端上传给服务端的 tar 文件解压后的内容,也即 docker build 命令行后面指定路径下的文件。
Docker 镜像的构建是在远程服务端进行的,所以客户端需要把构建所需要的文件传输给服务端。服务端以客户端发送的文件为上下文,也就是说 Dockerfile 中指令的工作目录就是服务端解压客户端传输的 tar 包的路径。
关于 docker build 指令的几点重要的说明:
如果构建镜像时没有明确指定 Dockerfile,那么 Docker 客户端默认在构建镜像时指定的上下文路径下找名字为 Dockerfile 的构建文件;
Dockerfile 可以不在构建上下文路径下,此时需要构建时通过 -f 参数明确指定使用哪个构建文件,并且名称可以自己任意命名。
下面通过具体的实例来理解下:
首先创建一个简单的 demo 工程,工程结构如下:
helloworld-app
├── Dockerfile
└── docker
├── app-1.0-SNAPSHOT.jar
├── hello.txt
└── html
└── index.html
Dockerfile 内容:
FROM busybox
COPY hello.txt .
COPY html/index.html .
实践1:直接进入 helloworld-app 目录进行镜像构建,以 docker 目录为构建上下文:
$ docker build -t hello-app:1.0 docker
unable to prepare context: unable to evaluate symlinks in Dockerfile path: lstat /Users/haohao/opensource/helloworld-app/docker/Dockerfile: no such file or directory
可以看出默认 docker 客户端从 docker 构建上下文路径下找名字为 Dockerfile 的构建文件。
实践2:明确指定 Dockerfile 文件进行镜像构建,还是以 docker 目录为构建上下文:
$ docker build -f Dockerfile -t hello-app:1.0 docker
Sending build context to Docker daemon 96.61MB
Step 1/3 : FROM busybox
---> d8233ab899d4
Step 2/3 : COPY hello.txt .
---> 3305fc373120
Step 3/3 : COPY html/index.html .
---> efdefc4e6eb2
Successfully built efdefc4e6eb2
Successfully tagged hello-app:1.0
从输出结果可以得知:
- 构建镜像时客户端会先给服务端发送构建上下路径下的内容(即 docker 目录下的文件);
- Dockerfile 可以不在构建上下文路径下;
- Dockerfile 中指令的工作目录是服务端解压客户端传输的 tar 包的路径;
实践3:以当前目录为构建上下文路径:
$ls
Dockerfile docker
$ docker build -t hello-app:2.0 .
Sending build context to Docker daemon 96.62MB
Step 1/3 : FROM busybox
---> d8233ab899d4
Step 2/3 : COPY hello.txt .
COPY failed: stat /var/lib/docker/tmp/docker-builder375982663/hello.txt: no such file or directory
可以看出:
- 镜像构建上下文路径并不是 Dockerfile 文件所在的路径;
- Dockerfile 中指令的工作目录是服务端解压客户端传输的 tar 包的路径,因为 COPY 指令失败了,意味着当前目录并没有 hello.txt 文件;
镜像定制都会使用 Dockerfile 的一些指令,比如 FROM、RUN、ADD、COPY等,其实利用Dockerfile定制镜像所需要的指令会比较多,Dockerfile 功能非常强大,可以提供很多的指令。
COPY 复制文件
格式:
- COPY [--chown=<user>:<group>] <源路径>... <目标路径>
- COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用
COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。如,
FROM nginx:latest
COPY hello.txt home/
COPY html/index.html /usr/share/nginx/
<源路径> 可以是多个,甚至可以是通配符,通配符要满足 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/
如果源路径是文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。
构建过程
查看Dockerfile 所在目录的结构
[root@ip-10-1-0-142 mynginx]# tree
.
├── app
│ ├── app.jar
│ ├── hello.txt
│ └── html
│ └── index.html
└── Dockerfile
2 directories, 4 files
[root@ip-10-1-0-142 mynginx]#
镜像构建
[root@ip-10-1-0-142 mynginx]# docker build -f Dockerfile -t docker:v2 app
Sending build context to Docker daemon 3.631kB
Step 1/3 : FROM nginx:latest
---> 08b152afcfae
Step 2/3 : COPY hello.txt home/
---> Using cache
---> 7d8a97c733a6
Step 3/3 : COPY html/index.html /usr/share/nginx/
---> Using cache
---> dfa69cdc3b6d
Successfully built dfa69cdc3b6d
Successfully tagged docker:v2
[root@ip-10-1-0-142 mynginx]# ls
app Dockerfile
运行一个容器
[root@ip-10-1-0-142 mynginx]# docker run -itd --name docekr3 -p 82:80 docker:v2 bash
c3660ca7af4a53ea3d6f03a8bc3a795ba3c83a9e7f1b9bcdc3cfac646fdea20d