# Dockerfile
# 一、镜像
镜像可以看成是由多个镜像层叠加起来的一个文件系统(通过 UnionFS 与 AUFS 文件联合系统实现),镜像层也可以简单理解为一个基本的镜像,而每个镜像层之间通过指针的形式进行叠加。
根据上图,镜像层的主要组成部分包括:
- 镜像层 ID
- 镜像层指针 「指向父层」
- 元数据「 Layer Metadata,包含了 Docker 构建和运行的信息和父层的层次信息」。
只读层和读写层「Top Layer」的组成部分基本一致,同时读写层可以转换成只读层「 通过docker commit 操作实现」。
**元数据(metadata)**就是关于这个层的额外信息,它不仅能够让 Docker 获取运行和构建时的信息,还包括父层的层次信息。需要注意,只读层和读写层都包含元数据。
每一层都包括了一个指向父层的指针。如果一个层没有这个指针,说明它处于最底层。
在 Docker 主机中镜像层(image layer)的元数据被保存在名为 “json” 的文件中,一个容器的元数据好像是被分成了很多文件,但或多或少能够在 /var/lib/docker/containers/ 目录下找到,就是一个可读层的 id。这个目录下的文件大多是运行时的数据,比如说网络,日志等等。
镜像是一堆只读层的统一视角,除了最底层没有指向外,每一层都指向它的父层。统一文件系统**(Union File System)**技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一的视角,这样就隐藏了多层的存在。在用户的角度看来,只存在一个文件系统。镜像每一层都是不可写的,都是只读层。
我们可以看到镜像包含多个只读层,它们重叠在一起。除了最下面一层,其它层都会有一个指针指向下一层。这些层是 Docker 内部的实现细节,并且能够在 Docker 主机的文件系统上访问到。统一文件系统(Union File System,升级版为 AUFS)技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一的视角,这样就隐藏了多层的存在,在用户的角度看来,只存在一个文件系统。
# 二、Dockerfile 介绍
Dockerfile 是由一系列命令和参数构成的脚本,这些命令应用于基础镜像并最终创建一个新的镜像。它们简化了从头到尾的流程并极大的简化了部署工作。Dockerfile 从 FROM 命令开始,紧接着跟随着各种方法,命令和参数。其产出为一个新的可以用于创建容器的镜像。
Dockerfile 语法由两部分构成
- 注释
- 命令+参数
# 三、Dockerfile 命令
# FROM
指定基础镜像,并且必须是第一条指令。
如果不以任何镜像为基础,写法为:FROM scratch
。同时意味着接下来所写的指令将作为镜像的第一层开始。
语法:
FROM 基础镜像:tag #其中 :tag 是可选的,如果不写,那么默认值是 latest
# MAINTAINER
指定作者。
语法:
MAINTAINER 作者名
# LABEL
为镜像设置标签。
语法:
LABEL key1=value1 key2=value2 ...
一个 Dockerfile 可以有多个 LABLE,如下:
LABEL "com.example.vendor"="ACME Incorporated"LABEL com.example.label-with-value="foo"LABEL version="1.0"LABEL description="This text illustrates \that label-values can span multiple lines."# 但是并不建议这样写,最好就写成一行,如太长需要换行的话则使用 \符号# 如下:LABEL multi.label1="value1" \multi.label2="value2" \other="value3"# 注意:LABEL会继承基础镜像种的 LABEL,如遇到 key 相同,则值覆盖
# RUN
运行指定的命令。
语法一:
RUN shell命令
语法二:
RUN [“executable”, “param1”, “param2”] # 可将executable理解成为可执行文件,后面就是两个参数。
示例:
RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOMERUN ["/bin/bash", “-c”, “echo hello”]
注意:
- 多行命令不要写多个 RUN,原因是 Dockerfile 中每一个指令都会建立一层。RUN 书写时的换行符是 \ ,多少个 RUN 就构建了多少层镜像,会造成镜像的臃肿、多层,不仅仅增加了构件部署的时间,还容易出错。
# ADD
一个复制命令,把文件复制到镜像中
如果把虚拟机与容器想象成两台 Linux 服务器的话,那么这个命令就类似于 scp,只是 scp 需要加用户名和密码的权限验证,而 ADD 不用。
语法:
ADD <src> <desc> # <src>可以是一个本地文件或者是一个本地压缩文件,还可以是一个url # <dest>路径的填写可以是容器内的绝对路径,也可以是相对于工作目录的相对路径
示例:
ADD test1.txt test1.txtADD test1.txt test1.txt.bakADD test1.txt /mydir/ADD data1 data1ADD zip.tar /myzip
注意:
- 如果源路径是个文件,且目标路径是以 / 结尾, 则 Docker 会把目标路径当作一个目录,会把源文件拷贝到该目录下。如果目标路径不存在,则会自动创建目标路径。
- 如果源路径是个文件,且目标路径不是以 / 结尾,则 Docker 会把目标路径当作一个文件。
- 如果目标路径不存在,会以目标路径为名创建一个文件,内容同源文件;
- 如果目标文件是个存在的文件,会用源文件覆盖它,当然只是内容覆盖,文件名还是目标文件名。
- 如果目标文件实际是个存在的目录,则会源文件拷贝到该目录下。 注意,这种情况下,最好显示的以 / 结尾,以避免混淆。
- 如果源路径是个目录,且目标路径不存在,则 Docker 会自动以目标路径创建一个目录,把源路径目录下的文件拷贝进来。如果目标路径是个已经存在的目录,则 Docker 会把源路径目录下的文件拷贝到该目录下。
- 如果源文件是个归档文件(压缩文件),则 Docker 会自动帮解压。尽量不要把 <scr> 写成一个文件夹,如果 <src> 是一个文件夹了,复制整个目录的内容,包括文件系统元数据。
# COPY
复制命令。只能用在本地文件上。
# VOLUME
可实现挂载功能,可以将本地文件夹或者其他容器中的文件夹挂在到这个容器中。
语法:
VOLUME ["/data"]
说明:["/data"]可以是一个JsonArray ,也可以是多个值。所以如下几种写法都是正确的
- VOLUME ["/var/log/"]
- VOLUME /var/log
- VOLUME /var/log /var/db
一般的使用场景为需要持久化存储数据时, 容器使用的是 AUFS,这种文件系统不能持久化数据,当容器关闭后,所有的更改都会丢失,所以当数据需要持久化时用这个命令。
# EXPOSE
功能为暴漏容器运行时的监听端口给外部。
但是 EXPOSE 并不会使容器访问主机的端口。如果想使得容器与主机的端口有映射关系,必须在容器启动的时候加上 -P
参数。
# WORKDIR
设置容器的工作目录。
语法:
WORKDIR 工作目录
# ENV
设置环境变量。
语法:
ENV # 1 次设置 1 个 ENV = ... # 1 次设置多个
# CMD
设置容器启动时要运行的命令。
语法:
CMD [“executable”,“param1”,“param2”] CMD [“param1”,“param2”] CMD command param1 param2
示例:
CMD [ “sh”, “-c”, “echo $HOME” CMD [ “echo”, “$HOME” ]
注意:
这里边包括参数的一定要用双引号,就是 " 不能是单引号,原因是参数传递后,Docker 解析的是一个JSON Array。
不要把 RUN 和 CMD 搞混了。
RUN:是构件容器时就运行的命令以及提交运行结果。
CMD:是容器启动时执行的命令,在构件时并不运行。
# ENTRYPOINT
设置启动时的默认命令。
语法:
ENTRYPOINT [“executable”, “param1”, “param2”] ENTRYPOINT command param1 param2
注意:
# 与 CMD 比较说明:
1. 相同点:
只能写一条,如果写了多条,那么只有最后一条生效,容器启动时才运行,运行时机相同。
2. 不同点:
ENTRYPOINT 不会被运行的 command 覆盖,而 CMD 则会被覆盖
# 举例 1
如果我们在 Dockerfile 时同时写了 ENTRYPOINT 和 CMD ,并且 CMD 指令不是一个完整的可执行命令,那么 CMD 指定的内容将会作为 ENTRYPOINT 的参数, 如下:
FROM centos
ENTRYPOINT ["top", "-b"]
CMD ["-c"]
# 举例 2
如果我们在 Dockerfile 种同时写了 ENTRYPOINT 和 CMD ,并且 CMD 是一个完整的指令,那么它们两个会互相覆盖,谁在最后谁生效, 如下:
FROM centos
ENTRYPOINT ["top", "-b"]
CMD ls -al
那么将执行 ls -al , top -b 不会执行
# 四、Dockerfile 案例
基本步骤:
- 创建目录,用于存放 dockerfile 所使用的文件
- 在此目录中创建 dockerfile 文件
- 在此目录中使用 docker build 创建镜像
- 使用创建的镜像启动容器
# 1. 在 Docker 中部署 Go Web 项目
1. 先写一个简单的 go web 项目
进入到目录:/usr/local/gopath/src/docker_go
mkdir -p /usr/local/gopath/src/docker_go
cd /usr/local/gopath/src/docker_go
写一个 main.go
package main
import (
"fmt"
"net/http"
)
func main(){
http.HandleFunc("/", hello)
server := &http.Server{
Addr: ":8888",
}
fmt.Println("server startup ....")
if err := server.ListenAndServe(); err != nil {
panic(err)
}
}
func hello (w http.ResponseWriter, _ *http.Request){
w.Write([]byte("hello hedon."))
}
2. 本地测试 go web 项目是否没问题
go build .
/docker_go
3. 编写 Dockerfile
vim Dockerfile
写入:
FROM golang
#设置环境变量
ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64
#设置容器工作目录
WORKDIR /build
#将代码从本机复制到容器中
ADD . .
#先删除到之前的 go.mod 文件,不然会报错
RUN rm -rf ./*.mod
#执行 go mod init
RUN go mod init docker_go
#执行 go mod tidy 更新依赖
RUN go build -o app .
#容器中:移动到用于存放生成的二进制文件的 /dist 目录
WORKDIR /dist
#容器中:将二进制文件 app 从 /build 目录复制到 /dist 目录下
RUN cp /build/app .
#声明服务端口
EXPOSE 8888
#启动容器时运行 go web 项目
CMD ["/dist/app"]
4. 构建镜像
docker build -t goweb_app .
出现下面信息就表示构建成功了:
....
....
Successfully built 62879d6fbd56
5. 查看镜像
[root@VM-12-10-centos docker_go]docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEgoweb_app latest 62879d6fbd56 2 minutes ago 894 MB
6. 运行镜像
docker run -p 8888:8888 goweb_app
7. 访问项目