跳到主要内容

创建容器镜像

编写正确、高效的Dockerfile

写一个最简单的Dockerfile

# Dockerfile.busybox
FROM busybox # 选择基础镜像
CMD echo "hello world" # 启动容器时默认运行的命令
  • FROM 表示选择构建使用的基础镜像,相当于”打地基“,这里使用的是busybox
  • 第二条CMD,它指定docker run启动容器之后默认运行的命令,这里是用了echo命令输出hello world

使用docker build来创建出镜像

docker build -f Dockerfile .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM busybox
---> 71a676dd070f
Step 2/2 : CMD echo "hello world"
---> Running in b62810bb8826
Removing intermediate container b62810bb8826
---> e79c13b4d138
Successfully built e79c13b4d138

-f参数指定Dockerfile文件名,后面必须跟一个文件路径,叫做构建上下文,这里是一个简单的.号,表示当前路径的意思;Docker逐行读取指令,依次创建镜像层,最后生成一个完整的镜像。

root@ARM-Virtual-Machine:/home/k8s-learn# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> e79c13b4d138 4 minutes ago 1.41MB
busybox latest 71a676dd070f 9 months ago 1.41MB
nginx 1.21-alpine e08a7adafd85 9 months ago 22.1MB
redis latest f16c30136ff3 10 months ago 107MB
alpine latest 8e1d7573f448 11 months ago 5.33MB

新的镜像暂时还没有名字,可以看到是一个<none>,但我们可以直接使用IMAGE ID来查看或者运行。

wxvirus@ARM-Virtual-Machine:/etc/apt$ docker inspect e79c13b4d138
[
{
"Id": "sha256:e79c13b4d138a408dbf08336c75dab09d7e1a75663f6d02f5cbfadc99656b414",
"RepoTags": [],
"RepoDigests": [],
"Parent": "sha256:71a676dd070f4b701c3272e566d84951362f1326ea07d5bbad119d1c4f6b3d02",
"Comment": "",
"Created": "2022-10-21T14:45:28.663079802Z",
"Container": "b62810bb88264b6f9ae80c415e11f19729248c050401750bda597f935c482108",
"ContainerConfig": {
"Hostname": "b62810bb8826",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"CMD [\"/bin/sh\" \"-c\" \"echo \\\"hello world\\\"\"]"
],
"Image": "sha256:71a676dd070f4b701c3272e566d84951362f1326ea07d5bbad119d1c4f6b3d02",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {}
},
"DockerVersion": "20.10.12",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"echo \"hello world\""
],
"Image": "sha256:71a676dd070f4b701c3272e566d84951362f1326ea07d5bbad119d1c4f6b3d02",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"Architecture": "arm64",
"Variant": "v8",
"Os": "linux",
"Size": 1411612,
"VirtualSize": 1411612,
"GraphDriver": {
"Data": {
"MergedDir": "/var/lib/docker/overlay2/1fd0a027cca34b60f52f9857f1cb3fce5c8e7562ad035a484a75f7232f8d13eb/merged",
"UpperDir": "/var/lib/docker/overlay2/1fd0a027cca34b60f52f9857f1cb3fce5c8e7562ad035a484a75f7232f8d13eb/diff",
"WorkDir": "/var/lib/docker/overlay2/1fd0a027cca34b60f52f9857f1cb3fce5c8e7562ad035a484a75f7232f8d13eb/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:468ad4d964cd60bde6271569ed2459b3cd52ee8a5a621f3f5fae86efa37e390c"
]
},
"Metadata": {
"LastTagTime": "0001-01-01T00:00:00Z"
}
}
]

运行

docker run e79c13b4d138

怎么编写正确、高效的Dockerfile

第一条指令必须是FROM,所以基础镜像的选择非常关键;如果关注的是镜像的安全和代销,那么一般会选择Alpine;如果关注的是应用的运行稳定性,那么可能会选择Ubuntu、Debian、CentOS

FORM alpine:3.15 # 选择alpine 镜像
FROM ubuntu:bionic # 选择ubuntu镜像

我们在本机上开发测试时会产生一些源码、配置等文件,需要打包进镜像里,这时可以使用 COPY 命令,它的用法和 Linuxcp 差不多,不过拷贝的源文件必须是“构建上下文”路径里的,不能随意指定文件。也就是说,如果要从本机向镜像拷贝文件,就必须把这些文件放到一个专门的目录,然后在 docker build 里指定“构建上下文”到这个目录才行。

COPY ./a.txt  /tmp/a.txt    # 把构建上下文里的a.txt拷贝到镜像的/tmp目录
COPY /etc/hosts /tmp # 错误!不能使用构建上下文之外的文件

接下来是一个RUN命令,它可以执行任意的Shell命令,比如更新系统、安装应用、下载文件、创建目录、编译程序等。实现任意的镜像构建步骤,非常灵活。

但是Dockerfile里指令只能是一行,所以所有的RUN指令会在每行末尾加上续行符\,命令之间也是由&&来连接,这样保证逻辑上是一行。

RUN apt-get update \
&& apt-get install -y \
build-essential \
curl \
make \
unzip \
&& cd /tmp \
&& curl -fSL xxx.tar.gz -o xxx.tar.gz\
&& tar xzf xxx.tar.gz \
&& cd xxx \
&& ./config \
&& make \
&& make clean

我们如果觉得太长,可以使用上面的COPY命令,将内容全部写到一个.sh脚本里,使用COPY命令拷贝进去再进行执行。

COPY setup.sh  /tmp/                # 拷贝脚本到/tmp目录

RUN cd /tmp && chmod +x setup.sh \ # 添加执行权限
&& ./setup.sh && rm setup.sh # 运行脚本然后再删除

RUN 指令实际上就是 Shell 编程,如果你对它有所了解,就应该知道它有变量的概念,可以实现参数化运行,这在 Dockerfile 里也可以做到,需要使用两个指令 ARGENV

它们区别在于 ARG 创建的变量只在镜像构建过程中可见,容器运行时不可见,而 ENV 创建的变量不仅能够在构建镜像的过程中使用,在容器运行时也能够以环境变量的形式被应用程序使用。

使用ARG定义基础 镜像名称,和使用ENV来定义环境变量:

ARG IMAGE_BASE="node"
ARG IMAGE_TAG="alpine"

ENV PATH=$PATH:/tmp
ENV DEBUG=OFF

还有一个重要的指令是 EXPOSE,它用来声明容器对外服务的端口号,对现在基于 Node.js、Tomcat、Nginx、Go 等开发的微服务系统来说非常有用:

注意

每个指令都会生成一个镜像层,所以 Dockerfile 里最好不要滥用指令,尽量精简合并,否则太多的层会导致镜像臃肿不堪。


docker也有不需要打包的东西,我们可以使用.dockerignore.gitignore一样,排除那些不需要的文件

# docker ignore
*.swp # 排除以 .swp 结尾的文件
*.sh # 排除以 .sh 结尾的文件

构建的时候一般使用-f来显式指定构建文件,如果省略这个参数则docker build会在当前目录下找名字是Dockerfile的文件,所以只有一个构建文件时,直接叫Dockerfile是最省事的。

总结

  1. 创建镜像需要编写 Dockerfile,写清楚创建镜像的步骤,每个指令都会生成一个 Layer
  2. Dockerfile 里,第一个指令必须是 FROM,用来选择基础镜像,常用的有 AlpineUbuntu 等。其他常用的指令有:COPYRUNEXPOSE,分别是拷贝文件,运行 Shell 命令,声明服务端口号。
  3. docker build 需要用 -f 来指定 Dockerfile,如果不指定就使用当前目录下名字是“Dockerfile”的文件。
  4. docker build 需要指定“构建上下文”,其中的文件会打包上传到 Docker daemon,所以尽量不要在“构建上下文”中存放多余的文件。
  5. 创建镜像的时候应当尽量使用 -t 参数,为镜像起一个有意义的名字,方便管理。

案例demo

# Dockerfile
# docker build -t ngx-app .
# docker build -t ngx-app:1.0 .

ARG IMAGE_BASE="nginx"
ARG IMAGE_TAG="1.21-alpine"

FROM ${IMAGE_BASE}:${IMAGE_TAG}

COPY ./default.conf /etc/nginx/conf.d/

RUN cd /usr/share/nginx/html \
&& echo "hello nginx" > a.txt

EXPOSE 8081 8082 8083

基本意思就是构建一个nginx轻量级镜像,并配置一个默认配置,简单写了一个htm,并暴露一些端口。