Docker容器化技术做为DevOps中的一个重要组成部分,具体表如今开发、测试、生产环境的统一这一大特色;php
实际上应用上线(应用构建和部署)用Docker实现时,就是基于某个运行环境或者操做系统的 Image 作一些配置调整,生成新的 Image,而后基于此 Image 运行一个 Container;html
你能够 pull 一个基础镜像,而后基于此镜像运行一个容器,进入此容器安装一些软件或者进行一些相应的环境配置,而后经过 docker commit
命令来基于这个容器建立一个新的镜像;可是这样操做有一些弊端,好比,其余人员不知道你这个镜像具体怎么建立的,安全性、镜像建立过程、镜像内容不够透明,因此,咱们通常基于 Dockerfile 来建立新镜像文件。mysql
Dockerfile 中存放一条条指令,用于指定其基础镜像( image 是只读的,负责应用的存储、分发,而 Container 则是经过 image 建立的可读写的层,建立新的镜像的时候,须要运行一个 Container,事后会删掉这个临时运行的 Container)、建立者信息、须要执行的命令、启动时的指令等信息;当你的 Dockerfile 建立好了之后,就能够经过 docker build
命令来构建一个新的 image。linux
Dockerfile经常使用指令nginx
类型 | 命令 |
---|---|
基础镜像信息 | FROM |
维护者信息 | MAINTAINER |
镜像操做指令 | RUN、COPY、ADD、EXPOSE、WORKDIR、ONBUILD、USER、VOLUME等 |
容器启动时执行指令 | CMD、ENTRYPOINT |
格式为 FROM <image>
或FROM <image>:<tag>
。第一条指令必须为 FROM
指令git
建立镜像的用户信息,如:MAINTAINER docker_user docker_user@email.com
github
LABEL
指令是此指令的更为灵活的版本,你应该使用它,由于它能够设置所需的任何元数据,而且可使用 docker inspect
查看相关内容。要设置与MAINTAINER字段相对应的标签,可使用:LABEL maintainer="docker_user docker_user@email.com"
,一个 Dockerfile 能够有多个 LABLE
标签golang
RUN <命令行命令>
,等同于,在终端操做的 shell 命令。RUN ["可执行文件", "参数1", "参数2"]
,例如 RUN ["./test.php", "dev", "offline"]
,等价于 RUN ./test.php dev offline
提示:当命令较长时可使用\
来换行;Dockerfile 的指令每执行一次都会在 docker 上新建一层,过多无心义的层,会形成镜像膨胀过大,因此在写 RUN 指令时,尽可能将多条命令用&&
来连接。(此提示仅针对较旧的Docker版本,新版本请使用多阶段构建,下面有解释多阶段构建的意义)
指定启动容器时执行的命令,每一个 Dockerfile 只能有一条 CMD
命令,若是指定了多条命令,只有最后一条会被执行。sql
若是用户启动容器时候指定了运行的命令,则会覆盖掉 CMD
指定的命令。docker
支持三种格式
CMD ["executable","param1","param2"]
使用 exec
执行,推荐方式;CMD command param1 param2
在 /bin/sh
中执行,提供给须要交互的应用;CMD ["param1","param2"]
提供给 ENTRYPOINT
的默认参数;EXPOSE指令通知Docker运行时容器在指定的网络端口上进行侦听,你能够指定端口是侦听TCP仍是UDP,若是未指定协议,则默认值为TCP。
格式:EXPOSE <port> [<port>/<protocol>...]
例子:EXPOSE 80/tcp
、EXPOSE 80/udp
、EXPOSE 80
EXPOSE指令实际上不会开放端口,它充当构建映像和运行容器之间的一种文档类型,实际开放哪些端口,要在运行容器时,在docker run
后面跟上-p
参数开放并映射一个或多个端口,或使用-P
开放并映射到随机端口。
ENV指令将环境变量<key>设置为值<value>,此值将在构建阶段中全部后续指令的环境中使用,而且在许多状况下也能够内联替换。
ENV指令有两种形式:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
例子:
ENV myName John Doe
ENV myName="John Doe" myDog=Rex\ The\ Dog \ myCat=fluffy
复制指令,从源目录中复制文件或者目录到容器里指定路径。
格式:
COPY [--chown=<user>:<group>] <源路径1>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
[--chown=<user>:<group>]:可选参数,用户改变复制到容器内文件的拥有者和属组。
例子:
COPY hom* /mydir/
COPY hom?.txt /mydir/
ADD 指令和 COPY 的使用格式一致(一样需求下,官方推荐使用 COPY),功能也相似,不一样之处以下:
ADD 的优势:在执行 <源文件> 为 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的状况下,会自动复制并解压到 <目标路径>。
ADD 的缺点:在不解压的前提下,没法复制 tar 压缩文件。会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。具体是否使用,能够根据是否须要自动解压来决定。
相似于 CMD 指令,但其不会被 docker run 的命令行参数指定的指令所覆盖,并且这些命令行参数会被看成参数送给 ENTRYPOINT 指令指定的程序。
可是, 若是运行 docker run 时使用了 --entrypoint 选项,此选项的参数可看成要运行的程序覆盖 ENTRYPOINT 指令指定的程序。
优势:在执行 docker run 的时候能够指定 ENTRYPOINT 运行所需的参数。
注意:若是 Dockerfile 中若是存在多个 ENTRYPOINT 指令,仅最后一个生效。
格式:ENTRYPOINT ["<executeable>","<param1>","<param2>",...]
能够搭配 CMD 命令使用:通常是变参才会使用 CMD ,这里的 CMD 等因而在给 ENTRYPOINT 传参,示例:
假设已经过 Dockerfile 构建了 nginx:test 镜像:
FROM nginx ENTRYPOINT ["nginx", "-c"] # 定参 CMD ["/etc/nginx/nginx.conf"] # 变参
一、不传参运行:$ docker run nginx:test
容器内会默认运行如下命令,启动主进程。
nginx -c /etc/nginx/nginx.conf
二、传参运行:$ docker run nginx:test -c /etc/nginx/new.conf
容器内会默认运行如下命令,启动主进程(/etc/nginx/new.conf:假设容器内已有此文件)
nginx -c /etc/nginx/new.conf
定义数据卷,在启动容器时忘记挂载数据卷,会自动挂载到此处定义的数据卷。
做用:
格式:
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
在启动容器 docker run 的时候,咱们能够经过 -v 参数修改挂载点。
用于指定执行后续命令的用户和用户组,这边只是切换后续命令执行的用户(用户和用户组必须提早已经存在)。
格式:USER <用户名>[:<用户组>]
例子:USER jack
指定工做目录,格式为 WORKDIR /path/to/workdir
。
为后续的 RUN
、CMD
、ENTRYPOINT
指令配置工做目录。
可使用多个 WORKDIR
指令,后续命令若是参数是相对路径,则会基于以前命令指定的路径。例如
# 最终路径为 /a/b/c WORKDIR /a WORKDIR b WORKDIR c RUN pwd
本节内容彻底抄自Dockerfile多阶段构建原理和使用场景
Docker 17.05版本之后,新增了Dockerfile多阶段构建。所谓多阶段构建,其实是容许一个Dockerfile 中出现多个FROM
指令。这样作有什么意义呢?
老版本Docker中为何不支持多个 FROM 指令?
在17.05版本以前的Docker,只容许Dockerfile中出现一个FROM
指令,这得从镜像的本质提及。
Docker 有个 “层” 的概念,最主要的文件是 层。
Dockerfile 中,大多数指令会生成一个层,好比下方的两个例子:
# 示例一,foo 镜像的Dockerfile # 基础镜像中已经存在若干个层了 FROM ubuntu:16.04 # RUN指令会增长一层,在这一层中,安装了 git 软件 RUN apt-get update \ && apt-get install -y --no-install-recommends git \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # 示例二,bar 镜像的Dockerfile FROM foo # RUN指令会增长一层,在这一层中,安装了 nginx RUN apt-get update \ && apt-get install -y --no-install-recommends nginx \ && apt-get clean \ && rm -rf /var/lib/apt/lists/*
假设基础镜像ubuntu:16.04
已经存在5层,使用第一个Dockerfile打包成镜像 foo,则foo有6层,又使用第二个Dockerfile打包成镜像bar,则bar中有7层。
若是ubuntu:16.04
等其余镜像不算,若是系统中只存在 foo 和 bar 两个镜像,那么系统中一共保存了多少层呢?
是7层,并不是13层,这是由于,foo和bar共享了6层。层的共享机制能够节约大量的磁盘空间和传输带宽,好比你本地已经有了foo镜像,又从镜像仓库中拉取bar镜像时,只拉取本地所没有的最后一层就能够了,不须要把整个bar镜像连根拉一遍。可是层共享是怎样实现的呢?
原来,Docker镜像的每一层只记录文件变动,在容器启动时,Docker会将镜像的各个层进行计算,最后生成一个文件系统,这个被称为 联合挂载。对此感兴趣的话能够进入了解一下 AUFS。
Docker的各个层是有相关性的,在联合挂载的过程当中,系统须要知道在什么样的基础上再增长新的文件。那么这就要求一个Docker镜像只能有一个起始层,只能有一个根。因此,Dockerfile中,就只容许一个FROM
指令。由于多个FROM
指令会形成多根,则是没法实现的。但为何 Docker 17.05 版本之后容许 Dockerfile支持多个FROM
指令了呢,莫非已经支持了多根?
多个 FROM 指令的意义
多个 FROM 指令并非为了生成多根的层关系,最后生成的镜像,仍以最后一条 FROM 为准,以前的 FROM 会被抛弃,那么以前的FROM 又有什么意义呢?
每一条 FROM 指令都是一个构建阶段,多条 FROM 就是多阶段构建,虽然最后生成的镜像只能是最后一个阶段的结果,可是,可以将前置阶段中的文件拷贝到后边的阶段中,这就是多阶段构建的最大意义。
最大的使用场景是将编译环境和运行环境分离,好比,以前咱们须要构建一个Go语言程序,那么就须要用到go命令等编译环境,咱们的Dockerfile多是这样的:
# Go语言环境基础镜像 FROM golang:1.10.3 # 将源码拷贝到镜像中 COPY server.go /build/ # 指定工做目录 WORKDIR /build # 编译镜像时,运行 go build 编译生成 server 程序 RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags '-w -s' -o server # 指定容器运行时入口程序 server ENTRYPOINT ["/build/server"]
基础镜像golang:1.10.3
是很是庞大的,由于其中包含了全部的Go语言编译工具和库,而运行时候咱们仅仅须要编译后的server
程序就好了,不须要编译时的编译工具,最后生成的大致积镜像就是一种浪费。
使用脉冲云的解决办法是将程序编译和镜像打包分开,使用脉冲云的编译构建服务,选择增长构Go语言构建工具,而后在构建步骤中编译。
最后将编译接口拷贝到镜像中就好了,那么Dockerfile的基础镜像并不须要包含Go编译环境:
# 不须要Go语言编译环境 FROM scratch # 将编译结果拷贝到容器中 COPY server /server # 指定容器运行时入口程序 server ENTRYPOINT ["/server"]
提示:scratch
是内置关键词,并非一个真实存在的镜像。FROM scratch
会使用一个彻底干净的文件系统,不包含任何文件。 由于Go语言编译后不须要运行时,也就不须要安装任何的运行库。FROM scratch
可使得最后生成的镜像最小化,其中只包含了server
程序。
在 Docker 17.05版本之后,就有了新的解决方案,直接一个Dockerfile就能够解决:
# 编译阶段 FROM golang:1.10.3 COPY server.go /build/ WORKDIR /build RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags '-w -s' -o server # 运行阶段 FROM scratch # 从编译阶段的中拷贝编译结果到当前镜像中 COPY --from=0 /build/server / ENTRYPOINT ["/server"]
这个 Dockerfile 的玄妙之处就在于 COPY 指令的--from=0
参数,从前边的阶段中拷贝文件到当前阶段中,多个FROM语句时,0表明第一个阶段。除了使用数字,咱们还能够给阶段命名,好比:
# 编译阶段 命名为 builder FROM golang:1.10.3 as builder # ... 省略 # 运行阶段 FROM scratch # 从编译阶段的中拷贝编译结果到当前镜像中 COPY --from=builder /build/server /
更为强大的是,COPY--from
不但能够从前置阶段中拷贝,还能够直接从一个已经存在的镜像中拷贝。好比,
FROM ubuntu:16.04 COPY --from=quay.io/coreos/etcd:v3.3.9 /usr/local/bin/etcd /usr/local/bin/
咱们直接将etcd镜像中的程序拷贝到了咱们的镜像中,这样,在生成咱们的程序镜像时,就不须要源码编译etcd了,直接将官方编译好的程序文件拿过来就好了。
有些程序要么没有apt源,要么apt源中的版本太老,要么干脆只提供源码须要本身编译,使用这些程序时,咱们能够方便地使用已经存在的Docker镜像做为咱们的基础镜像。可是咱们的软件有时候可能须要依赖多个这种文件,咱们并不能同时将 nginx 和 etcd 的镜像同时做为咱们的基础镜像(不支持多根),这种状况下,使用 COPY--from
就很是方便实用了。
“ \”
来进行换行“ \”
分割成多行,易读Dockerfile 文件内容以下
$ sudo cat Dockerfile FROM centos LABEL MAINTAINER="liu" RUN yum update -y && yum -y install nginx EXPOSE 80 ENTRYPOINT ["nginx", "-g", "daemon off;"]
构建Docker镜像
$ sudo docker build -t my_nginx:1.0 . Sending build context to Docker daemon 2.048kB Step 1/5 : FROM centos ---> 0f3e07c0138f Step 2/5 : LABEL MAINTAINER="liu" ---> Using cache ---> b331b67a50df Step 3/5 : RUN yum update -y && yum -y install nginx ---> Using cache ---> 2cb2a7a24e64 Step 4/5 : EXPOSE 80 ---> Running in a737a3fb5651 Removing intermediate container a737a3fb5651 ---> d21ac1194380 Step 5/5 : ENTRYPOINT ["nginx", "-g", "daemon off;"] ---> Running in 6d9b3a8689cd Removing intermediate container 6d9b3a8689cd ---> 2378b531443c Successfully built 2378b531443c Successfully tagged my_nginx:1.0
运行一个容器,检查咱们构建的镜像是否能够正常使用
$ sudo docker run --name t1 -d -p 81:80 my_nginx:1.0 0ed3f35d968ecfa400c976c467487682e22ffde07283d7a698682a829caeaf19 $ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0ed3f35d968e my_nginx:1.0 "nginx -g 'daemon of…" 3 seconds ago Up 2 seconds 0.0.0.0:81->80/tcp t1 $ sudo curl 127.0.0.1:81 -I HTTP/1.1 200 OK Server: nginx/1.14.1 Date: Thu, 26 Dec 2019 06:02:40 GMT Content-Type: text/html Content-Length: 4057 Last-Modified: Mon, 07 Oct 2019 21:16:24 GMT Connection: keep-alive ETag: "5d9bab28-fd9" Accept-Ranges: bytes
FROM debian:stretch-slim # add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added RUN groupadd -r mysql && useradd -r -g mysql mysql RUN apt-get update && apt-get install -y --no-install-recommends gnupg dirmngr && rm -rf /var/lib/apt/lists/* # add gosu for easy step-down from root ENV GOSU_VERSION 1.7 RUN set -x \ && apt-get update && apt-get install -y --no-install-recommends ca-certificates wget && rm -rf /var/lib/apt/lists/* \ && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture)" \ && wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture).asc" \ && export GNUPGHOME="$(mktemp -d)" \ && gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 \ && gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu \ && gpgconf --kill all \ && rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc \ && chmod +x /usr/local/bin/gosu \ && gosu nobody true \ && apt-get purge -y --auto-remove ca-certificates wget RUN mkdir /docker-entrypoint-initdb.d RUN apt-get update && apt-get install -y --no-install-recommends \ # for MYSQL_RANDOM_ROOT_PASSWORD pwgen \ # for mysql_ssl_rsa_setup openssl \ # FATAL ERROR: please install the following Perl modules before executing /usr/local/mysql/scripts/mysql_install_db: # File::Basename # File::Copy # Sys::Hostname # Data::Dumper perl \ && rm -rf /var/lib/apt/lists/* RUN set -ex; \ # gpg: key 5072E1F5: public key "MySQL Release Engineering <mysql-build@oss.oracle.com>" imported key='A4A9406876FCBD3C456770C88C718D3B5072E1F5'; \ export GNUPGHOME="$(mktemp -d)"; \ gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys "$key"; \ gpg --batch --export "$key" > /etc/apt/trusted.gpg.d/mysql.gpg; \ gpgconf --kill all; \ rm -rf "$GNUPGHOME"; \ apt-key list > /dev/null ENV MYSQL_MAJOR 8.0 ENV MYSQL_VERSION 8.0.18-1debian9 RUN echo "deb http://repo.mysql.com/apt/debian/ stretch mysql-${MYSQL_MAJOR}" > /etc/apt/sources.list.d/mysql.list # the "/var/lib/mysql" stuff here is because the mysql-server postinst doesn't have an explicit way to disable the mysql_install_db codepath besides having a database already "configured" (ie, stuff in /var/lib/mysql/mysql) # also, we set debconf keys to make APT a little quieter RUN { \ echo mysql-community-server mysql-community-server/data-dir select ''; \ echo mysql-community-server mysql-community-server/root-pass password ''; \ echo mysql-community-server mysql-community-server/re-root-pass password ''; \ echo mysql-community-server mysql-community-server/remove-test-db select false; \ } | debconf-set-selections \ && apt-get update && apt-get install -y mysql-community-client="${MYSQL_VERSION}" mysql-community-server-core="${MYSQL_VERSION}" && rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/mysql && mkdir -p /var/lib/mysql /var/run/mysqld \ && chown -R mysql:mysql /var/lib/mysql /var/run/mysqld \ # ensure that /var/run/mysqld (used for socket and lock files) is writable regardless of the UID our mysqld instance ends up having at runtime && chmod 777 /var/run/mysqld VOLUME /var/lib/mysql # Config files COPY config/ /etc/mysql/ COPY docker-entrypoint.sh /usr/local/bin/ RUN ln -s usr/local/bin/docker-entrypoint.sh /entrypoint.sh # backwards compat ENTRYPOINT ["docker-entrypoint.sh"] EXPOSE 3306 33060 CMD ["mysqld"]
FROM php:7.2-apache # persistent dependencies RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ # Ghostscript is required for rendering PDF previews ghostscript \ ; \ rm -rf /var/lib/apt/lists/* # install the PHP extensions we need (https://make.wordpress.org/hosting/handbook/handbook/server-environment/#php-extensions) RUN set -ex; \ \ savedAptMark="$(apt-mark showmanual)"; \ \ apt-get update; \ apt-get install -y --no-install-recommends \ libfreetype6-dev \ libjpeg-dev \ libmagickwand-dev \ libpng-dev \ ; \ \ docker-php-ext-configure gd --with-freetype-dir=/usr --with-jpeg-dir=/usr --with-png-dir=/usr; \ docker-php-ext-install -j "$(nproc)" \ bcmath \ exif \ gd \ mysqli \ opcache \ zip \ ; \ pecl install imagick-3.4.4; \ docker-php-ext-enable imagick; \ \ # reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies apt-mark auto '.*' > /dev/null; \ apt-mark manual $savedAptMark; \ ldd "$(php -r 'echo ini_get("extension_dir");')"/*.so \ | awk '/=>/ { print $3 }' \ | sort -u \ | xargs -r dpkg-query -S \ | cut -d: -f1 \ | sort -u \ | xargs -rt apt-mark manual; \ \ apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ rm -rf /var/lib/apt/lists/* # set recommended PHP.ini settings # see https://secure.php.net/manual/en/opcache.installation.php RUN { \ echo 'opcache.memory_consumption=128'; \ echo 'opcache.interned_strings_buffer=8'; \ echo 'opcache.max_accelerated_files=4000'; \ echo 'opcache.revalidate_freq=2'; \ echo 'opcache.fast_shutdown=1'; \ } > /usr/local/etc/php/conf.d/opcache-recommended.ini # https://wordpress.org/support/article/editing-wp-config-php/#configure-error-logging RUN { \ # https://www.php.net/manual/en/errorfunc.constants.php # https://github.com/docker-library/wordpress/issues/420#issuecomment-517839670 echo 'error_reporting = E_ERROR | E_WARNING | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING | E_RECOVERABLE_ERROR'; \ echo 'display_errors = Off'; \ echo 'display_startup_errors = Off'; \ echo 'log_errors = On'; \ echo 'error_log = /dev/stderr'; \ echo 'log_errors_max_len = 1024'; \ echo 'ignore_repeated_errors = On'; \ echo 'ignore_repeated_source = Off'; \ echo 'html_errors = Off'; \ } > /usr/local/etc/php/conf.d/error-logging.ini RUN set -eux; \ a2enmod rewrite expires; \ \ # https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html a2enmod remoteip; \ { \ echo 'RemoteIPHeader X-Forwarded-For'; \ # these IP ranges are reserved for "private" use and should thus *usually* be safe inside Docker echo 'RemoteIPTrustedProxy 10.0.0.0/8'; \ echo 'RemoteIPTrustedProxy 172.16.0.0/12'; \ echo 'RemoteIPTrustedProxy 192.168.0.0/16'; \ echo 'RemoteIPTrustedProxy 169.254.0.0/16'; \ echo 'RemoteIPTrustedProxy 127.0.0.0/8'; \ } > /etc/apache2/conf-available/remoteip.conf; \ a2enconf remoteip; \ # https://github.com/docker-library/wordpress/issues/383#issuecomment-507886512 # (replace all instances of "%h" with "%a" in LogFormat) find /etc/apache2 -type f -name '*.conf' -exec sed -ri 's/([[:space:]]*LogFormat[[:space:]]+"[^"]*)%h([^"]*")/\1%a\2/g' '{}' + VOLUME /var/www/html ENV WORDPRESS_VERSION 5.3.2 ENV WORDPRESS_SHA1 fded476f112dbab14e3b5acddd2bcfa550e7b01b RUN set -ex; \ curl -o wordpress.tar.gz -fSL "https://wordpress.org/wordpress-${WORDPRESS_VERSION}.tar.gz"; \ echo "$WORDPRESS_SHA1 *wordpress.tar.gz" | sha1sum -c -; \ # upstream tarballs include ./wordpress/ so this gives us /usr/src/wordpress tar -xzf wordpress.tar.gz -C /usr/src/; \ rm wordpress.tar.gz; \ chown -R www-data:www-data /usr/src/wordpress COPY docker-entrypoint.sh /usr/local/bin/ ENTRYPOINT ["docker-entrypoint.sh"] CMD ["apache2-foreground"]
参考文章:
https://docs.docker.com/engine/reference/builder/
http://www.dockerinfo.net/dockerfile介绍
https://www.runoob.com/docker/docker-dockerfile.html
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/