Docker Dockerfile文件详解(制作自己的镜像)
HDUZN

镜像的定制,实际上就是定制每一层所添加的配置、文件。我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像。这个脚本就是 Dockerfile。

Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

一、举例:创建Flask项目环境镜像

比如,我自己有个Python的项目,是个Flask项目,我想把它放到服务器上,就是个Web项目。

那我们怎么在一台服务器上快速、方便的把本地的Flask开发环境部署上去呢?比如本地用的Python版本、以前Flask相关的库,以及其它用到的库。就可以用Docker制作一个自己的镜像。

Flask开发环境的镜像制作好了,其实就是一个Dockerfile 文件,之后就可以随意部署到带Docker的服务器上去了。贼方便。

还有,就是我想制作的是一个Flask项目的环境镜像,所以代码这些无所谓的,只要环境能制作好就行,所以在建这个镜像的时候没有复制项目文件这些,也用不着运行。
因为这样一个通用的Flask镜像,后续项目文件自己上传就行。

Flask项目结构(样例)

1
2
3
4
5
6
7
flask_proj_demo
├── app
│ ├── templates
│ └── index.html
│ ├── app.py
│ ├── Dockerfile
│ └── requirements.txt

1.生成 requirements.txt(本地)

进入flask_proj_demo项目目录的app目录下,运行如下命令,就可以生成项目环境的 requirements.txt 文件。

1
pip freeze > requirements.txt

requirements.txt 举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
click==8.0.1
colorama==0.4.4
et-xmlfile==1.1.0
Flask==2.0.1
Flask-MySQLdb==0.2.0
Flask-SQLAlchemy==2.5.1
Flask-WTF==0.15.1
greenlet==1.1.0
itsdangerous==2.0.1
Jinja2==3.0.1
MarkupSafe==2.0.1
mysqlclient==2.0.3
numpy==1.22.2
openpyxl==3.0.9
pandas==1.4.1
PyMySQL==1.0.2
python-dateutil==2.8.2
pytz==2021.3
six==1.16.0
SQLAlchemy==1.4.21
Werkzeug==2.0.1
WTForms==2.3.3
xlrd==2.0.1
pandas==1.4.1
requests==2.27.1
selenium==4.1.3
urllib3==1.26.9

可以看到除了带Flask中常用的库,也带了mysql和selenium、pandas这些,这种随你自己就行,你自己项目经常用哪些就放哪些。生成的requirements.txt文件中如果有些用不着,也可以删除的。

2.用Dockerfile 文件创建镜像

Dockerfile文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Use an official Python runtime as an image
# FROM:依赖的基础镜像 python3.9版本
FROM python:3.9

# Sets the working directory for following COPY and CMD instructions
# Notice we haven’t created a directory by this name - this instruction
# creates a directory with this name if it doesn’t exist
# WORKDIR:设置容器启动后的默认运行目录 /app
WORKDIR /app

# copy code
# COPY . /app
# COPY:复制文件到容器的/app目录
COPY requirements.txt /app

# RUN python -m pip install --upgrade pip
# RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host=files.pythonhosted.org --no-cache-dir -r requirements.txt
# RUN:运行命令,安装依赖 requirements.txt中的库
RUN pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple --upgrade pip
RUN pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt

# The EXPOSE instruction indicates the ports on which a container
# EXPOSE:端口暴露 我这里app.py中run的port=5020,所以暴露的是这个端口
EXPOSE 5020

# Run app.py when the container launches
# COPY app.py /app
# CMD指令只能一个,是容器启动后执行的命令,算是程序的入口;如果需要运行其他命令可以用&&连接
# CMD ["python", "app.py"]

然后,就把这整个项目的文件上传到服务器上。我这直接用Xftp上传到了 /root/flask/flask_proj_demo/app目录。

最后用SSH连接服务器,进入到 /root/flask/flask_proj_demo/app 目录,运行以下命令生成镜像:

1
docker build -t flask_demo:v1.0 .

-t 设置镜像名字和版本号

执行完,就可以用命令 docker images看到以下镜像了(当然也有python3.9的镜像)。

二、Dockerfile 指令详解

1.常用指令

常用的基本上就是上面用到的那几个指令:

FROM python:3.9:依赖的基础镜像 python3.9版本
WORKDIR /app:设置容器启动后的默认工作运行目录 /app
COPY . /app:复制当前目录下文件到容器的/app目录
RUN:执行命令
EXPOSE 5020:暴露5020端口
CMD:CMD指令只能一个,是容器启动后执行的命令,算是程序的入口

1.COPY 复制文件

格式:COPY [--chown=<user>:<group>] <源路径>... <目标路径>
--chown=<user>:<group> 参数来改变文件的所属用户及所属组。

COPY指令复制,源文件的各种元数据都会保留。

例子:

1
2
3
COPY . /app:复制当前目录下文件到容器的/app目录
COPY requirements.txt /app:复制文件到容器的/app目录
COPY --chown=abcuser:mygroup . /app

2.ADD 更高级的复制文件

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。

比如 <源路径> 可以是一个 URL
这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去。下载后的文件权限自动设置为 600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。
所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。

如果 <源路径> 为一个 tar 压缩文件的话
压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。

不过,按在Docker官方的 Dockerfile最佳实践文档中的要求,尽可能的使用COPY,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。

所以除了需要自动解压的场合,一般用COPY就行。

3.RUN 执行命令

格式有两种:
shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。
exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。

举例:

1
RUN pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple --upgrade pip

4.CMD 指令

CMD指令只能一个,是容器启动后执行的命令,算是程序的入口。

1).格式

格式和 RUN 相似,也是两种格式:
shell 格式:CMD <命令>
exec 格式:CMD ["可执行文件", "参数1", "参数2"...]

参数列表格式:CMD [“参数1”, “参数2”…]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

2).举例

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 数组,因此一定要使用双引号 “,而不要使用单引号。

1
CMD echo $HOME

在实际执行中,会将其变更为:CMD [ "sh", "-c", "echo $HOME" ]

3).容器中应用在前台执行(重点)

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 可执行文件,并且要求以前台形式运行。比如:

1
CMD ["nginx", "-g", "daemon off;"]

5.EXPOSE 暴露端口

格式为:EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。

在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。
-p,是映射宿主端口和容器端口,就是将容器的对应端口服务公开给外界访问;
而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

6.ENV 设置环境变量

格式有两种:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

比如举个简单的例子(Dockerfile中):

1
2
3
4
5
6
7
8
9
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/nodejsENV NODE_VERSION 7.2.0

这里先定义了环境变量 NODE_VERSION,其后的 RUN 这层里,多次使用 $NODE_VERSION 来进行操作定制。

那将来升级镜像构建版本的时候,只需要更新 7.2.0 即可,Dockerfile 构建维护变得更轻松了。

下列指令支持环境变量展开: ADD、COPY、ENV、EXPOSE、FROM、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD、RUN。

7.VOLUME 定义匿名卷

格式为:
VOLUME <路径>
VOLUME ["<路径1>", "<路径2>"...]

容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。

为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

举个例子(Dockerfile中):

1
VOLUME /data

这里的 /data 目录就会在容器运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行容器时可以覆盖这个挂载设置。

比如,运行以下命令,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

1
$ docker run -d -v mydata:/data xxxx

8.其它指令

还剩下一些其它的指令,这里就简单列一下了。

1
2
3
4
5
6
7
USER:指定当前用户 # 格式:USER <用户名>[:<用户组>]
ENTRYPOINT:入口点
SHELL:指令 # 格式:SHELL ["executable", "parameters"]
HEALTHCHECK:健康检查 # 格式:HEALTHCHECK [选项] CMD <命令>
ONBUILD:为他人作嫁衣裳 # 格式:ONBUILD <其它指令>
LABEL:为镜像添加元数据
ARG:构建参数 # 格式:ARG <参数名>[=<默认值>]

需要看具体的,网上一搜都有介绍,有Docker从入门到实践的文档:https://yeasy.gitbook.io/docker_practice/image/dockerfile

官方镜像的Dockerfile 参考典范:https://github.com/docker-library/docs
可以瞅瞅,参考参考。

  • 本文标题:Docker Dockerfile文件详解(制作自己的镜像)
  • 本文作者:HDUZN
  • 创建时间:2022-05-29 18:04:26
  • 本文链接:http://hduzn.cn/2022/05/29/Docker-Dockerfile文件详解(制作自己的镜像)/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论