Dockerfile最佳实践
一个容器对应一个进程
一个Docker容器应该只对应一个进程,也就是一个Docker 镜像一般只包含一个应用的制品包(比如.jar)。
在需要组合多个进程的场景,使用容器组(比如Docker Compose,或Kubernetes Pod)。
选用合适的基础镜像
- 选用基础镜像的原则:
- 使用官方提供的基础镜像(official)
- 基础镜像应该提供足够的支持,使得Dockerfile尽量简单(easy enough)
- 基础镜像要足够精简,尽量不要包含不需要的内容(simple enough)
- 使用指定标签(版本)的基础镜像,不使用latest标签的基础镜像 (explicit)
把最少改动的步骤放在最前面
把最少改动的步骤放在最前面,也就是准备应用的COPY步骤要放在安装工具和准备环境的RUN命令之后,能够重用前面构建的层的cache,防止每次都要重复构建。Dockefile中步骤一般为:
选择基础镜像,比如:
FROM openjdk:8-jdk-stretch
安装Dockerfile后面步骤需要用到的工具和docker execdebug时需要用到的工具,比如:
RUN apt-get update && apt-get upgrade -y && apt-get install -y git curl && rm -rf /var/lib/apt/lists/*
其他的一些准备环境步骤,比如:
RUN curl -fsSL ${JENKINS_URL} -o /usr/share/jenkins/jenkins.war
&& echo "${JENKINS_SHA} /usr/share/jenkins/jenkins.war" | sha256sum -c -
准备应用,比如:
COPY jenkins.sh /usr/local/bin/jenkins.sh
声明程序的入口点,比如:
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/jenkins.sh"]
暴露端口,比如:
EXPOSE 8080
Dockerfile的步骤中只有RUN、COPY和ADD才会创建层,其它指令的先后顺序主要是根据Dockerfile的语法、逻辑结构和习惯。
参考文档:
https://github.com/jenkinsci/docker/blob/master/Dockerfile
Docker构建上下文中不要包含不需要的文件
运行docker build [options] PATH构建Docker镜像时,Docker会将PATH路径下的全部内容作为构建上下文传给Docker Daemon。参见docker build日志中的第一句Sending build context to Docker daemon。
如果PATH路径下的内容太多,会导致镜像构建很慢,如果Dockerfile书写不当,还会引入不必要的文件,从而导致镜像变大,影响镜像构建和拉取速度。可以通过.dockerignore文件在Docker构建时不要将指定内容包含在构建上下文中。
.dockerignore例子:
# ignore .git and .cache folders
.git
.cache
# ignore all *.class files in all folders, including build root
**/*.class
# ignore all markdown files (md)
*.md
还可以将Dockerfile和制品包(比如.jar)放到一个干净的新目录下,再来构建镜像,参见:
https://github.com/cookcodeblog/gs-rest-service-maven/blob/master/docker-build.sh
https://github.com/cookcodeblog/gs-rest-service-maven/blob/master/Dockerfile
参考文档:
https://docs.docker.com/engine/reference/builder/#dockerignore-file
https://codefresh.io/docker-tutorial/not-ignore-dockerignore/
多阶段构建
Docker提供了多阶段构建(multistag-builds)的功能,但是一般在做多阶段构建时不需要这么复杂,只需要分成以下2步:
先构建出应用制品包,比如用Maven构建出应用的.jar包
在docker build构建Docker镜像时,将上一步生成的.jar包复制到镜像中
减少Docker镜像层的数量
Dokcerfile中的RUN、COPY和ADD命令才会创建镜像层,因此减少Docker镜像层的数量就是要减少这几个命令的次数,特别是RUN命令的次数。
在安装工具时可以在一句命令中安装多个工具:
正例:
apt-get install -y git curl
反例:
apt-get install -y git
apt-get install -y curl
用&&拼接多个命令:
正例:
RUN apt-get update && apt-get upgrade -y && apt-get install -y git curl
反例:
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y git curl
用来在拼接多个命令时换行,增加可读性
RUN curl -fsSL https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-$(dpkg --print-architecture) -o /sbin/tini
&& curl -fsSL https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-$(dpkg --print-architecture).asc -o /sbin/tini.asc
&& gpg --no-tty --import ${JENKINS_HOME}/tini_pub.gpg
&& gpg --verify /sbin/tini.asc
&& rm -rf /sbin/tini.asc /root/.gnupg
&& chmod +x /sbin/tini
使用专门的user和group
如果不以root用户来运行应用,则可以使用专门的user和group。
以Jenkins为例,Jenkins镜像使用了jenkins(1000)/jenkins(1000)的user(uid)/group(gid):
ARG user=jenkins
ARG group=jenkins
ARG uid=1000
ARG gid=1000
ARG JENKINS_HOME=/var/jenkins_home
RUN mkdir -p $JENKINS_HOME
&& chown ${uid}:${gid} $JENKINS_HOME
&& groupadd -g ${gid} ${group}
&& useradd -d "$JENKINS_HOME" -u ${uid} -g ${gid} -m -s /bin/bash ${user}
USER ${user}
查看Jenkins的user/group信息:
# 进入容器
docker exec -it <cotainer_id> /bin/bash
# 查看user / group信息
cat /etc/passwd | grep jenkins
# 每行7个字段,以:隔开
# 1. Username: jenkins
# 2. Password: x表示加密的密码
# 3. User ID(UID): 1000
# 4. Group ID(GID): 1000
# 5. User ID Info: User描述信息
# 6. Home directory: Home目录
# 7. Command/Shell: Command或Shell的绝对路径,比如/bin/bash,为/bin/false表示不允许执行bash
jenkins:x:1000:1000:Linux User,,,:/var/jenkins_home:/bin/bash
参考文档:
https://www.cyberciti.biz/faq/understanding-etcpasswd-file-format/
一次构建,多环境运行
在Dockerfile中使用ENV指令定义环境变量并可设置缺省值,通过docker run -e指定运行时环境变量。
容易混淆的Dockerfile的指令
ADD和COPY
推荐使用更为简单的COPY指令。
ADD命令虽然可以提供额外的下载和解压等功能,但是下载可以通过curl命令,解压可以通过tar -zxvf指令来操作。
VOLUME
在Dockerfile中VOLUME指令在docker run 时会在宿主机下的/var/lib/docker/volumes目录下新建<volume_id>的持久卷目录。如果是下次docker start时则会重用之前的持久卷。
# 列出持久卷
docker volume ls
# 查看某个容器的持久卷
docker inspect <container_id> | less
# 查找Mounts关键字
使用docker run -v可以在运行容器时指定持久卷目录,也可以使用已存在的目录。
CMD和ENTRYPOINT
CMD语法:CMD ["executable", "param1", "param2"…]
ENTRYPOINT语法:ENTRYPOINT ["executable", "param1", "param2"…]
Dockerfile中应该只包含一个CMD或一个ENTRYPOINT。
包含多个CMD时,只有最后一个CMD才会生效,并会让Dockerfile难懂。
同时包含CMD和ENTRYPOINT时,CMD中的参数其实是ENTRYPOINT的参数,让Dockerfile难懂。
ARG和ENV
ARG是构建时参数,通过docker build --build-arg arg=value指定。
ENV是运行时参数,通过docker run -e var=value指定。
在Dockerfile中也可以用ARG来给ENV赋值,例如:
ARG JENKINS_HOME=/var/jenkins_home
ENV JENKINS_HOME $JENKINS_HOME
ARG JENKINS_VERSION
ENV JENKINS_VERSION ${JENKINS_VERSION:-2.121.1}
参考文档
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
https://takacsmark.com/dockerfile-tutorial-by-example-dockerfile-best-practices-2018/
https://spring.io/guides/gs/spring-boot-docker/
转载:https://blog.csdn.net/nklinsirui/article/details/96113636