资料来源:
Docker Documentation
本文关注的问题:如何多快好省地写Dockerfile,使得由此构建的image,以及基于此image而运行的container具备体积小、速度快、安全高的特点?更进一步,rebuild速度快?高可用?
构建镜像的流程:
Command Line Interface–>Dockerfile + build context–> Docker Daemon–>Build Image–>Ruturn Image
解释:
首先命令词的写法只有两种形式shell form 和 exec form:
比如用RUN命令作为一个例子:
shell form : RUN command
—>自动打开一个默认的shell来执行command
exec form :RUN ["executable", "param1", "param2"]
—>直接执行command,不开shell
区别:
# 格式:FROM [--platform=] [:] [AS ]
#linux平台/镜像名:标签名 as 别名
FROM linux/amd64:latest as amd
# 简单用法:
FROM ubuntu:18.04
每一个FROM都会清空Dockerfile里在这个From之前的所有状态量,如ARG,例子如下
docker build --build-arg =
进行参数的传递(值得注意的是变量不会在最后构建的image中保存):# dockerfile中的基础image
ARG OS_VERSION=18.04
FROM ubuntu:${OS_VERSION}
# ARG是一个有状态的变量,因此FROM清空了这个状态量
ARG VERSION=latest
# dockerfile中的第二个image
FROM busybox:$VERSION # 清空ARG变量
ARG VERSION # 想用到得重新声明变量,会继承其默认值
RUN echo $VERSION > image_version # 输出latest
docker build --build-arg user=what_user .
: 命令行传参给Dockerfile中ARG定义的user变量
FROM busybox
USER ${user:-some_user} # 输出some_user,因为没有定义user变量
ARG user # ARG定义了user变量
USER $user # 使用了定义的ARG变量,并接受命令行的传参 输出 what_user
Dokcerfile有一些预先定义好的ARG变量,不在docker history中,如HTTP_PROXY、http_proxy、HTTPS_PROXY、https_proxy、FTP_PROXY、ftp_proxy、NO_PROXY、no_proxy
可在docker build中指定这些ARG变量,来指示Docker Daemon构建这些镜像时,需要获取依赖、文件等资源时所用的代理
RUN ["executable", "param1", "param2"]
(并不会invoke一个command shell)RUN
(会自动在容器中开启一个command shell执行命令)# shell form
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo=1.3.* \
&& rm -rf /var/lib/apt/lists/*
# exec form
RUN ["/bin/bash", "-c", "echo hello"]
值得注意一点:一般不建议采用以下形式(首先会构建两层镜像,其次构建的镜像层是会缓存的,这样RUN apt-get update的时候就会使用缓存中的数据,得到过期的包版本。
RUN apt-get update
RUN apt-get install -y curl nginx
(当然构建时,可用不缓存的方式即docker build --no-cache image_name .
来运行分开的方式)
# CMD ["executable", "param1", "param2"]
CMD ["/usr/bin/wc","--help"]
# CMD
CMD /usr/bin/wc --help
ENTRYPOINT ["s3cmd"]
CMD ["--help"]
Command line arguments to docker run image will be appended after all elements in an exec form ENTRYPOINT, and will override all elements specified using CMD
CMD和ENTRY-POINT都是在容器层执行命令,其两者同时出现时,容器内执行的命令如下:
怎么用这个表?比如下面意味着在容器中执行 top -b -c
的命令
FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]
ENV PG_MAJOR=9.3 # 用来指定环境变量好管理
ENV PG_VERSION=9.3.4
RUN curl -SL https://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgres && …
ENV PATH=/usr/local/postgres-$PG_MAJOR/bin:$PATH # 添加环境变量
ENV abc=hello
ENV abc=bye efd=$abc
ENV ghi=$abc
在这例子里,变量efd的值是hello,变量ghi的值是bye,因为同一层的镜像中,只会使用上一层的变量值,因为dockerfile的构建是逐层构建的。
当然,也可以用RUN来直接添加环境变量,这样环境变量就不会留存在最终的镜像中
FROM alpine
RUN export ADMIN_USER="mark" \ # 添加环境变量
&& echo $ADMIN_USER > ./mark \
&& unset ADMIN_USER # 抹除环境变量
CMD sh # 指定运行的主命令为sh
FROM alpine
ARG ADMIN_USE="mark" #并不会像环境变量一样保存在最终的镜像中
RUN echo $ADMIN_USER > ./mark \
&& unset ADMIN_USER # 抹除环境变量
CMD sh # 指定运行的主命令为sh
技巧写法:
# 一般格式 COPY [--chown=:] ...
COPY requirements.txt /tmp/ # 先复制配置文件
RUN pip install --requirement /tmp/requirements.txt # 用了再说
COPY . /tmp/ # 再复制其它文件
不推荐写法:
COPY . /tmp/ # 每次改动当前文件夹中的文件,都需要重新pip install依赖
RUN pip install --requirement /tmp/requirements.txt
改变拷贝文件的权限:
COPY --chown=10:11 files* /somedir/
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd # 输出是/a/b/c
The WORKDIR instruction sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile
USER [:]
用户ID和组ID,一般指定运行命令的权限,管理员权限推荐用gosu,不用sudoLABEL com.example.version="0.0.1-beta"
EXPOSE 80/udp
# 每五分钟用相关指令(CMD),check一下,如果3s内没反应,则以状态1退出
HEALTHCHECK --interval=5m --timeout=3s \
CMD curl -f http://localhost/ || exit 1
docker build --build-arg CONT_IMG_VER=v2.0.1 .
FROM ubuntu
ARG CONT_IMG_VER # 值为v2.0.1
ENV CONT_IMG_VER=v1.0.0 # ENV 变量 改写 ARG变量
RUN echo $CONT_IMG_VER # 输出v1.0.0
通过ARG传参给环境变量,不然ENV变量用默认值v1.0.0
FROM ubuntu
ARG CONT_IMG_VER # 接收 docker build 命令行的传参
ENV CONT_IMG_VER=${CONT_IMG_VER:-v1.0.0} # 如果没有传参,则默认为v1.0.0
RUN echo $CONT_IMG_VER # 输出
# CMD并不增加容器大小
CMD ["perl", "-de0"]
CMD["python3"]
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["postgres"]
# 把文件abc.tar.xz复制到容器内的根目录并自动解压,创建容器层(image layer)
ADD rootfs.tar.xz /
一个简易的Dockerfile:
FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py
把这个镜像run起来,变成容器时,相当于在镜像层级结构上多加一层可写的container layer,所有数据变化都保存在容器层,而不影响镜像层。如下图所示:
镜像的内容是固定不变的,可读不可写,容器层的内容是可读写的,但如果没有数据卷即volume的话,停掉容器后,数据没了。那就是为什么有一个镜像后,run许多个容器他们并不像虚拟机那样占空间,因为docker只需要创建一个容器层进行数据的读写,镜像层是固定的,提供运行环境与依赖的。
一个镜像由一些镜像层构建。而多个镜像,会有一些镜像层重复。所以docker内部有个storage driver的方式(如aufs、overlay、overlay2)来管理镜像层重复的问题,不用我们操心。
这原理揭示了一个写Dockerfile的原则:越容易频繁改动的镜像层越靠近顶层
如果只改动上层的镜像层(修改代码),那么rebuild镜像的时候,下层没改动的会在前一层构建时放进cache缓存,这样rebuild速度就会加快。
为了更快rebuild,一些不需要改动的资源,可以写进.dockerignore文件,这样rebuild的时候就会跳过它。
根据该原则的一个实践方式就是多阶段构建。
比如描述一个go应用程序镜像构建的Dockerfile:
#最基础的镜像(最不用改动)
FROM golang:1.16-alpine AS build
#其次是一些常用工具包(如git、make等)
RUN apk add --no-cache git
#该命令要从某个github仓库clone所以需要上面的git工具
RUN go get github.com/golang/dep/cmd/dep
############# 当go的配置文件变动才需要变化 #############
# 准备go项目运行的依赖库,拷贝配置文件Gopkg.lock/Gopkg.toml
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# 根据配置文件安装go依赖包
RUN dep ensure -vendor-only
############## 项目代码发生变化才需要变化 ##################
COPY . /go/src/project/
RUN go build -o /bin/project
#以上一共新增了6层镜像层,因为只有RUN、COPY、ADD会创建新的镜像层
############## 运行项目代码 #########
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]
根据该原则及其多阶段构建的实践方式,镜像rebuild的速度、减少镜像的大小
在最新版的Docker里只有RUN、COPY、ADD三个会创建镜像层
.dockerignore的作用:RUN、COPY、ADD时会自动忽略一些文件,减少镜像大小
把一些命令集成从而减少镜像层的数目
RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversion \
&& rm -rf /var/lib/apt/lists/*
使用多阶段构建,在不增加最后镜像大小的前提下,可以在中间层包含一些tools和debug inforamation输出信息,帮助诊断。
此处选取强化一个开源库Modularized Implementation of Deep RL Algorithms in PyTorch的DockerFile进行分析,整个dockerfile的文件如下:
# 使用nvidia平台上的cuda镜像,可在docker中使用GPU并得到加速
FROM nvidia/cuda:10.0-base
# 安装基础的软件库
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y --allow-unauthenticated --no-install-recommends \
build-essential apt-utils cmake git curl vim ca-certificates \
libjpeg-dev libpng-dev \
libgtk3.0 libsm6 cmake ffmpeg pkg-config \
qtbase5-dev libqt5opengl5-dev libassimp-dev \
libboost-python-dev libtinyxml-dev bash \
wget unzip libosmesa6-dev software-properties-common \
libopenmpi-dev libglew-dev openssh-server \
libosmesa6-dev libgl1-mesa-glx libgl1-mesa-dev patchelf libglfw3
# 清除上一步安装留下的文件,尽可能维持image体积小
RUN rm -rf /var/lib/apt/lists/*
# 添加用户变量,可在docker build时传参指定具体值
ARG UID
RUN useradd -u $UID --create-home user
USER user
# 切换工作路径为/home/user
WORKDIR /home/user
# 安装miniconda
RUN wget -q https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && \
bash Miniconda3-latest-Linux-x86_64.sh -b -p miniconda3 && \
rm Miniconda3-latest-Linux-x86_64.sh
# 设置miniconda的环境变量来使用conda指令
ENV PATH /home/user/miniconda3/bin:$PATH
# 安装mujoco150/200
RUN mkdir -p .mujoco \
&& wget https://www.roboti.us/download/mjpro150_linux.zip -O mujoco.zip \
&& unzip mujoco.zip -d .mujoco \
&& rm mujoco.zip
RUN wget https://www.roboti.us/download/mujoco200_linux.zip -O mujoco.zip \
&& unzip mujoco.zip -d .mujoco \
&& rm mujoco.zip
# mujoco的key要先从mujoco官网上搞下来才能运行这份dockerfile
COPY ./mjkey.txt .mujoco/mjkey.txt
# 把mujoco动态文件的共享库加进环境变量
ENV LD_LIBRARY_PATH /home/user/.mujoco/mjpro150/bin:${LD_LIBRARY_PATH}
ENV LD_LIBRARY_PATH /home/user/.mujoco/mjpro200_linux/bin:${LD_LIBRARY_PATH}
# 用conda安装python3.6,然后用pip安装requirement.txt中的python包
RUN conda install -y python=3.6
RUN conda install mpi4py
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
RUN pip install glfw Cython imageio lockfile
RUN pip install mujoco-py==1.50.1.68
RUN pip install git+git://github.com/deepmind/dm_control.git@103834
RUN pip install git+https://github.com/ShangtongZhang/dm_control2gym.git@scalar_fix
RUN pip install git+git://github.com/openai/baselines.git@8e56dd#egg=baselines
WORKDIR /home/user/deep_rl
国内的麻烦需要换源,下篇来用docker实操一下,一劳永逸解决强化学习环境的问题。