Docker实操-2——优雅地写Dockerfile

Docker-Dockerfile

  • 概述
  • 一、Dockerfile的指令(instruction)
    • 1.0 命令的形式
    • 1.1 基本命令
    • 1.2 对比加深理解
  • 二、基本原理
    • 2.1 多阶段构建镜像
    • 2.2 小建议
  • 四、实例分析
    • 4.1 强化库的一个例子

资料来源:

Docker Documentation

概述

本文关注的问题:如何多快好省地写Dockerfile,使得由此构建的image,以及基于此image而运行的container具备体积小、速度快、安全高的特点?更进一步,rebuild速度快?高可用?

构建镜像的流程:
Command Line Interface–>Dockerfile + build context–> Docker Daemon–>Build Image–>Ruturn Image

解释:

  • 进入命令行交互界面CLI,利用Docker提供的API,以Docker Client方式运行
  • 有一份写好的Dockerfile描述如何构建镜像(Dockerfile)+ 当前路径上下文(Build Context)
  • 将Dockerfile 以及context下的内容均以C/S方式发送给Docker内的Daemon进程
  • Daemon进程结合.dockerignore文件开始镜像的构建(build),镜像构建好后返回

一、Dockerfile的指令(instruction)

1.0 命令的形式

首先命令词的写法只有两种形式shell form 和 exec form
比如用RUN命令作为一个例子:

shell form : RUN command—>自动打开一个默认的shell来执行command
exec form :RUN ["executable", "param1", "param2"]—>直接执行command,不开shell

区别:

  1. shell form的命令,都是通过一个shell进程运行或启动的,这时container的PID-1是shell进程
  2. exec form的命令,是直接启动executable的应用,这时container的PID-1是executable进程

1.1 基本命令

  • FROM:选择Base image,一般会选择官方的基础镜像如unbuntu:18.04
# 格式:FROM [--platform=] [:] [AS ]

#linux平台/镜像名:标签名 as 别名
FROM linux/amd64:latest as amd 

# 简单用法:
FROM ubuntu:18.04 

每一个FROM都会清空Dockerfile里在这个From之前的所有状态量,如ARG,例子如下

  • ARG:Dockerfile中对变量的定义,主要用于构建阶段如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用运行命令并提交的方式来构建镜像层,一般用包管理器下载常用工具:
    • exec form:RUN ["executable", "param1", "param2"] (并不会invoke一个command shell)
    • shell form: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:在镜像的容器层中跑应用程序软件或脚本用的,与RUN是安装依赖并提交改动构建镜像有很大的不同,CMD工作在容器层,并不构建镜像。常常搭配ENTRYPOINT指定Command。
# CMD ["executable", "param1", "param2"]
CMD ["/usr/bin/wc","--help"]
# CMD 
CMD /usr/bin/wc --help
  • ENTRYPOINT:镜像容器层的入口点,一般用来指定主命令即executable那部分,用CMD来指定参数–flags
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都是在容器层执行命令,其两者同时出现时,容器内执行的命令如下:
Docker实操-2——优雅地写Dockerfile_第1张图片
怎么用这个表?比如下面意味着在容器中执行 top -b -c的命令

FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]
  • ENV:跑应用程序前,除了运行环境的依赖,还要处理环境变量,该命令一般用来辅助RUN和CMD命令,会一直保存在镜像中即无状态变量如:
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
  • ADD:不仅仅复制文件进镜像,还有一些额外操作,比如能添加远程URL的资源,并自动压缩tar.gz等文件
  • COPY:单纯地复制本地host的文件进镜像,有一定技巧(为了避免因频繁改动而导致的镜像层重建)

技巧写法:

# 一般格式 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:指定镜像内命令默认的工作目录,不用cd来cd去
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

  • VOLUME:存储!作为镜像writable的容器层,在物理机上存运行中容器的状态数据,有named volume以及bind mount两种方式。一般使用named volume。
  • USER:USER [:]用户ID和组ID,一般指定运行命令的权限,管理员权限推荐用gosu,不用sudo
  • LABEL:管理镜像metadata的工具,作用是标记镜像的信息,方便整理镜像,如镜像版本信息等,存储方式是key-value
LABEL com.example.version="0.0.1-beta"
  • EXPOSE:暴露容器内应用程序监听的端口,这是从镜像构建者的角度去看的,而不是跑这个镜像容器的使用者角度。docker run -p 是从使用者角度进行端口的publish、并进行在物理机的映射。还可以指定协议如80/udp;80/tcp
EXPOSE 80/udp
  • ONBUILD:分阶段BUILD的利器,进阶内容。大体说在一个Dockerfile中,比如有image1部分 和 image 2部分,image1部分有个ONBUILD的指令,image2把image1当作基础镜像FROM,这时会执行ONBUILD的指令。
  • HEALTHYCHECK:用于检查容器的功能状态(working)。容器可能仍在running,但可能功能不work了。
# 每五分钟用相关指令(CMD),check一下,如果3s内没反应,则以状态1退出
HEALTHCHECK --interval=5m --timeout=3s \
  CMD curl -f http://localhost/ || exit 1

1.2 对比加深理解

  • ARG & ENV : ARG有利于docker build传递参数,不保留在构建的Image中,有一些参数构建时不希望留在镜像,如密码;ENV会保留在最终构建的Image中;两者同时存在时ENV变量总会改写ARG变量:

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 & RUN:RUN会创建镜像层,CMD不会,只是用来标记容器中可执行的程序命令。于是运行’docker run -it python3’时就会进入一个运行了CMD指定命令的shell中进行交互
# CMD并不增加容器大小
CMD ["perl", "-de0"]
CMD["python3"] 
  • CMD & ENTRYPOINT:一个image中可以有很多程序的相关命令,比如python,perl等,但这个image本身就是当作主要命令进行使用时,就可以用ENTRYPOINT,然后CMD当作参数进行传递,如当前image相关的均为一个自己编写的sh命令:
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["postgres"]
  • ADD & COPY:实践上,只要不涉及文件自动解压功能,就用COPY,否则用ADD,比如:
# 把文件abc.tar.xz复制到容器内的根目录并自动解压,创建容器层(image layer)
ADD rootfs.tar.xz /

二、基本原理

2.1 多阶段构建镜像

一个简易的Dockerfile:

FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py
  • FROM:是一个unbuntu:18.04的基础镜像,占188.1MB,在最底层
  • COPY:将当前工作目录下文件拷贝到镜像中./app路径,占194.5KB,-构建在基础镜像之上
  • RUN:利用make命令构建app,占1.895KB
  • CMD:最顶层的镜像层,指定容器内要执行的命令,因此不占内存

把这个镜像run起来,变成容器时,相当于在镜像层级结构上多加一层可写的container layer,所有数据变化都保存在容器层,而不影响镜像层。如下图所示:
Docker实操-2——优雅地写Dockerfile_第2张图片

镜像的内容是固定不变的,可读不可写,容器层的内容是可读写的,但如果没有数据卷即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的速度、减少镜像的大小

2.2 小建议

在最新版的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输出信息,帮助诊断。

四、实例分析

4.1 强化库的一个例子

此处选取强化一个开源库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实操一下,一劳永逸解决强化学习环境的问题。

你可能感兴趣的:(环境配置,docker,容器,运维)