有三种创建自定义镜像的方式
一、交互式创建
- 选取基础镜像,运行容器,并进入交互窗口
docker container run -it --name sample alpine /bin/sh
- 进行你所需要的修改,变更。这里假设需要安装ping命令所需的相关软件。
/ # apk update && apk add iputils
- 核实ping是否可用了,退出容器,查看容器状态
/ # ping 114.114.114.114
/ # exit
docker container ls -a | grep sample
- 查看我们的容器与基础镜像有什么不同
docker container diff sample
图片中,首字母含义:
- A代表新增文件
- C代表修改过的文件
- D代表被删除的文件
- 将修改过的容器导出为自己的镜像
docker container commit sample my-alpine
- 查看镜像
docker image ls
- 查看自定义的镜像的创建过程
docker image history my-alpine
通过IMAGE的id可以看到最后的一层是我们添加的,并且是在alpine这个镜像的基础上。
这种方式创建镜像的优缺点:
Manually creating custom images as shown in the previous section of this chapter is very helpful when doing exploration, creating prototypes, or making feasibility studies. But it has a serious drawback: it is a manual process and thus is not repeatable or scalable. It is also as error-prone as any task executed manually by humans.
二、通过Dockerfile创建镜像
一个简单的Dockerfile示例如下:
FROM python:2.7
RUN mkdir -p /app
WORKDIR /app
COPY ./requirements.txt /app/
RUN pip install -r requirements.txt
CMD ["python", "main.py"]
Dockerfile与镜像层级的关系
关键字详解:
- FROM
每一个Dockerfile文件都从这个关键字开始,它指明从哪个基础镜像开始进行你的镜像构建。通常,我们将docker hub里面提供的官方镜像作为基础镜像,例如:FROM centos:7
或者FROM python:2.7
,如果你想从空白镜像开始,可以使用FROM scratch
。 - RUN
RUN
后面可以跟随任意有效的linux命令,一般用来进行我们在基础镜像上的变更操作,由于每个RUN命令都会在镜像上增加一层,因此,建议将多个linux命令整合成一个命令。可使用类似如下方式:
- COPY 和 ADD
COPY
和ADD
都是用于将主机中的文件拷贝到镜像中的,不同之处在于,ADD
可以拷贝和解压tar文件,还可以通过URL来拷贝网络上的文件到容器中。
一些示例如下:
COPY . /app #将上下文目录中所有文件或目录递归拷贝到容器的/app目录下
COPY ./web /app/web #将上下文目录中的web目录下的内容拷贝到容器的/app/web目录下
COPY sample.txt /data/my-sample.txt #拷贝单个文件并重命名
ADD sample.tar /app/bin/ #解压tar包内的文件到指定目录
ADD http://example.com/sample.txt /data/ #拷贝远程文件到指定目录
COPY ./sample* /mydir/ #支持源路径中使用通配符
默认,通过COPY
和ADD
拷贝到镜像内的文件的UID和GID都为0,如果你需要改变,可以使用如下命令:
ADD --chown=11:22 ./data/files* /app/data/
除了指定UID和GID,你也可以指定用户的用户名称 和组名称,但是这些名称必须存在与镜像的/etc/passwd 和 /etc/group中,否则Dockerfile构建过程会失败。
- WORKDIR
WORKDIR
用来定义工作目录或者上下文目录。常见的问题:
RUN cd /app/bin
RUN touch sample.txt
由于每执行一次RUN,都是在原有镜像上添加一个新层,因此上面的命令只是在root目录下新建了sample.txt文件。
正确的切换至一个目录下,并新建文件,命令如下:
WORKDIR /app/bin
RUN touch sample.txt
- CMD 和 ENTRYPOINT
CMD
和ENTRYPOINT
命令比较特殊,因为除了这两个命令以外的其他命令,都是在构建镜像时就会运行并生效,而这两个命令是在使用镜像启动容器时,才会执行的命令。这两个命令用于告诉Docker在启动容器时,应该执行什么命令,并以何种方式去执行(指定一些参数)。
两者的区别:
先来看一个典型的Dockerfile示例
FROM alpine:latest
ENTRYPOINT ["ping"]
CMD ["8.8.8.8", "-c", "3"]
用ENTRYPOINT
来指定需要执行的命令,用CMD
来指定该命令所需的参数。
这是首选的推荐用法,也被称为exec格式。我们使用这个Dockerfile来构建一个镜像,命名为pinger,并使用该镜像运行一个容器,然后删除。
通过这种方式创建的镜像,我们还可以按照如下方式运行,可以实现修改Dockerfile中
ENTRYPOINT
的命令,也可以修改Dockerfile中CMD
的参数,非常灵活。如下:
再来看一个使用CMD
,并且没有ENTRYPOINT
的Dockerfile示例:
FROM alpine:latest
CMD wget -O - http://www.baidu.com
当没有明确定义ENTRYPOINT
参数时,其默认参数为/bin/sh -c
,而CMD
内的参数将以字符串形式,传递给该命令,因此以上Dockerfile最终执行的就是:
/bin/sh -c "wget -O - http://www.baidu.com"
一个Dockerfile文档示例
FROM node:9.4 #指定基础镜像
RUN mkdir -p /app #在镜像的文件系统内,创建一个/app目录
WORKDIR /app #切换上下文(工作目录)至/app目录下
COPY package.json /app/ #从主机上Dockerfile文件所在目录下,将package.json文件拷贝到镜像中的/app目录下
RUN npm install #npm会安装package.json内Node.js所需的依赖包
COPY . /app #与第四步一样,拷贝主机内文件(这里是应用程序文件)到镜像的/app目录下
ENTRYPOINT ["npm"] #与最后一行的命令结合,启动Node.js服务
CMD ["start"]
通过Dockerfile构造镜像的过程及相关解释
常用的通过Dockerfile构造镜像的过程:
- 创建一个空目录cmd_entrypoint。
- 进入目录中,创建一个Dockerfile文件,写入如下内容:
FROM alpine:latest
ENTRYPOINT ["ping"]
CMD ["8.8.8.8", "-c", "3"]
- 在目录下执行命令:
docker image build -t pinger .
当你的配置文件不是默认的名字时,可以使用-f参数指定:
docker image build -t pinger -f my-Dockerfile .
构造过程如下:
以上过程解释如下:
- 拉取基础镜像
alpine
,它的id为196d - 执行
ENTRYPOINT
,会通过第一步的基础镜像生成一个容器9ad9,在容器内执行ENTRYPOINT
所需的修改或变更操作等,操作完成后删除容器9ad9,然后新加一层成为新的镜像0669。 -
这个镜像将作为下一个操作的基础镜像,然后重复上面的过程,直至最后成为一个你所需的镜像。
流程也可以参考:
多阶段构建
示例如下:
C程序源文件hello.c:
#include <stdio.h>
int main (void)
{
printf ("Hello, world!\n");
return 0;
}
常用的Dockerfile编写方式如下:
FROM alpine:3.7
RUN apk update &&
apk add --update alpine-sdk
RUN mkdir /app
WORKDIR /app
COPY . /app
RUN mkdir bin
RUN gcc -Wall hello.c -o bin/hello
CMD /app/bin/hello
这种方式构造的镜像文件如下:
$ docker image ls | grep hello-world
hello-world latest e9b... 2 minutes ago 176MB
多阶段构建Dockerfile如下:
FROM alpine:3.7 AS build
RUN apk update && \
apk add --update alpine-sdk
RUN mkdir /app
WORKDIR /app
COPY . /app
RUN mkdir bin
RUN gcc hello.c -o bin/hello
FROM alpine:3.7
COPY --from=build /app/bin/hello /app/hello
CMD /app/hello
得到的镜像文件如下:
$ docker image ls | grep hello-world
hello-world-small latest f98... 20 seconds ago 4.16MB
hello-world latest 469... 10 minutes ago 176MB
多阶段构建优点:减少黑客可攻击的范围、节省内存和磁盘空间、更少的启动容器时间、减少下载镜像所需的带宽。
Dockerfile编写最佳实践
一些建议:
- 首先,我们要意识到容器的生命周期是短暂的。短暂的,意味着一个运行着的容器可能很快就会被停止以及销毁,而一个增加了新功能的容器,需要我们能够快速的构建和启用。我们需要尽可能的保证能够快速的启动、停止容器内的应用。
- 我们可能会针对同一个Dockerfile不停的构建自己的镜像。我们应该合理的组织Dockerfile内的每个命令,以尽可能的利用容器构建过程中的缓存功能。例如:
FROM node:9.4 RUN mkdir -p /app WORKIR /app COPY . /app RUN npm install CMD ["npm", "start"]
修改为:
FROM node:9.4 RUN mkdir -p /app WORKIR /app COPY package.json /app/ RUN npm install COPY . /app CMD ["npm", "start"]
以上两个Dockerfile一般来说,都会在通过npm install安装依赖包的时候花费大量时间,而针对一个开发项目来说,依赖包的变动的情况是比较少的,大部分时间都是应用程序的变动而导致重新构建镜像,因此将安装依赖包的过程独立出来,利用缓存的功能,可以加速下一次的构建过程。
- 另一个最佳实践就是尽可能减少你镜像的layer数。通常来说,layer越少,镜像越小,启动速度越快。而最好的减少layer数量的办法就是整合你的命令,因为,在dokcerfile中,每一个以FROM、COPY、RUN这样的关键字开头的行,就意味着一个新的layer。最常见的办法就是整合多个RUN行为一行。例如:
RUN apt-get update RUN apt-get install -y ca-certificates RUN rm -rf /var/lib/apt/lists/*
整合为:
RUN apt-get update \ && apt-get install -y ca-certificates \ && rm -rf /var/lib/apt/lists/*
- 通过.dockerignore文件,除去那些不需要拷贝到镜像内的文件,达到减小镜像体积的作用。用法与git中.gitignore一样。
- 在镜像的文件系统内,减少安装不必要的软件。
- 合理利用多阶段构建。
三、保存和加载镜像
第三种制作一个容器镜像的方法是,通过一个文件进行导入或加载。因为,一个容器镜像本质上就是一个tar包的文件。请看以下示例:
docker image save -o ./backup/my-alpine.tar my-alpine
上面的命令,将一个镜像保存为指定的tar包。
docker image load -i ./backup/my-alpine.tar
上面的命令,将一个tar包加载为可用的镜像。
管理镜像(分享和运送镜像)
为了能够把我们自定义的镜像运送到我们的生产环境中,我们需要给镜像一个全球唯一的名称,通常这个动作称为:tagging。通常,我们把镜像存放在一个集中管理的地方,以方便他人获取,这个地方称为:仓库(image registries)。
镜像的标签
每个镜像都有一个标签,通常用来指定镜像的版本号。
docker image pull alpine #没有指定tag时,默认为lates
docker image pull alpine:3.5 #指定tag
镜像的命名空间
一个完整的镜像命名如下:
<registry URL>/<User or Org>/<name>:<tag>
- <registry URL>
镜像存放的仓库的名字,通常是一些公用的镜像存储及管理服务器,例如docker hub。 - <User or Org>
在前面的仓库中定义的一个用户或者组织。 - <name>:<tag>
上面已经介绍过,就是你的镜像名以及标签。
一些特殊的约定:
- 如果忽略<registry URL>,那么默认是使用docker hub仓库。
- 如果忽略<tag>名称,那么默认是latest。
- 如果是docker hub上的官方镜像,那么镜像名不需要<User or Org>。
常见镜像名称解释:
Image | Description |
---|---|
alpine | Official alpine image on Docker Hub with the latest tag. |
ubuntu:16.04 | Official ubuntu image on Docker Hub with the 16.04 tag or version. |
microsoft/nanoserver | nanoserver image of Microsoft on Docker Hub with the latest tag. |
acme/web-api:12.0 | web-api image version 12.0 associated with the acme org.The image is on Docker Hub. |
gcr.io/gnschenker/sampleapp:1.1 | sample-app image with the 1.1 tag belonging to an individual with the gnschenker ID on Google's container registry. |
推送镜像到一个仓库
- 将需要推送的镜像取一个自定义的tag
docker image tag alpine:latest gnschenker/alpine:1.0
- 登录到你的仓库
docker login -u gnschenker -p <my secret password>
- 推送镜像
docker image push gnschenker/alpine:1.0
以上是默认使用docker hub仓库,你需要有相关的账号密码。
相关参考:Docker镜像上传到阿里云