Dockerfile最佳实践:

通用优化:

########################################### 体积与性能 #############################################

  • 1、使用alpine系统,保证最终镜像的体积不会太大
  • 2、使用多阶段构建,使最终的镜像避免包含不必要的文件
  • 3、尽量一个RUN 来跑完所有事情,当然使用多阶段构建可以解决多层影响性能的问题
  • 4、不会发生改变的层放在上边,来使用cache,比如ENV,CMD,EXPOSE
  • 5、使用各种中国镜像加速站来安装依赖

########################################### 安全启动 #############################################

  • 1、使用 tini作为PID为1的进程来启动程序,避免停止需要10秒的强制停止和僵尸进程
  • 2、较复杂的应用使用 entrypoint.sh 来初始化配置,同时用 exec来替换进程
  • 3、使用 非root用户启动,保证安全性,不用USER,使用gosu来切换启动用户
  • 4、针对gosu 和 exec的使用,alpine有一个 su-exec

########################################### 其它优化 #############################################

  • 1、使用LABEL 来表示作者等信息
  • 2、安装依赖程序的时候使用固定版本
  • 3、时间/时区等设置正确
  • 4、多使用ARG 可以在构建时传入参数:比如镜像版本,某个依赖的版本,时区; ARG 在FROM前后是分开的
  • 5、优雅停止, STOPSIGNAL
  • 6、健康检查,HEALTHCHECK,在k8s里有健康检查可以不写这个

一、体积与性能

1.1、使用alpine系统,保证最终镜像的体积不会太大

需要用当前的最新版本,要使用版本号,不要用latest,任何时候都避免用latest!

1.2、使用多阶段构建,使最终的镜像避免包含不必要的文件

FROM xxx AS builder
FROM builder AS tester
FROM builder

1.3、尽量一个RUN 来跑完所有事情,当然使用多阶段构建可以解决多层影响性能的问题

RUN xxxx &&\
    xxxx &&\
    xxxx

1.4、不会发生改变的层放在上边,来使用cache,比如ENV,CMD,EXPOSE

1.5、使用各种中国镜像加速站来安装依赖

  • ubuntu/debain/apt-get
RUN echo 'deb http://mirrors.aliyun.com/debian/ buster main non-free contrib \n\
deb-src http://mirrors.aliyun.com/debian/ buster main non-free contrib    \n\
deb http://mirrors.aliyun.com/debian-security buster/updates main         \n\
deb-src http://mirrors.aliyun.com/debian-security buster/updates main      \n\
deb http://mirrors.aliyun.com/debian/ buster-updates main non-free contrib \n\
deb-src http://mirrors.aliyun.com/debian/ buster-updates main non-free contrib \n\
deb http://mirrors.aliyun.com/debian/ buster-backports main non-free contrib    \n\
deb-src http://mirrors.aliyun.com/debian/ buster-backports main non-free contrib  \n'\
> /etc/apt/sources.list
  • alpine/apk
RUN sed -i "s/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g" /etc/apk/repositories &&\
    apk update &&\
    apk add --no-cache tini

golang

ENV GOPROXY https://goproxy.cn

python/pip

RUN pip install --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/

nodejs

RUN echo 'registry=https://registry.npm.taobao.org/  \n\
sass_binary_site="https://npm.taobao.org/mirrors/node-sass/"  \n\
phantomjs_cdnurl="http://cnpmjs.org/downloads"  \n\
electron_mirror="https://npm.taobao.org/mirrors/electron/"  \n\
sqlite3_binary_host_mirror="https://foxgis.oss-cn-shanghai.aliyuncs.com/"  \n\
profiler_binary_host_mirror="https://npm.taobao.org/mirrors/node-inspector/"  \n\
chromedriver_cdnurl="https://cdn.npm.taobao.org/dist/chromedriver"  \n\
sentrycli_cdnurl=https://npm.taobao.org/mirrors/sentry-cli  \n'\
> ./npmrc

二、安全启动

2.1、使用 tini作为PID为1的进程来启动程序

2.2、较复杂的应用使用 entrypoint.sh 来初始化配置,同时用 exec来替换进程

2.3、使用 非root用户启动,保证安全性,不用USER,使用gosu来切换启动用户

2.4、针对gosu 和 exec的使用,alpine有一个 su-exec

因为容器中没有init/systemd,所以会导致很多问题,shell启动进程,shell不会将停止信号发给子进程,docker会等待10s,15秒后强制删除。
参考:tini优化docker停止缓慢10s,进程收不到停止信号
gosu

使用 tini来启动进程,作为PID为1的进程。
tini是专为容器设计的轻量init进程。


RUN groupadd -r someone && useradd -r -g someone someone
WORKSPACE /app
COPY . .
RUN chmod +x entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["tini", "--", "top"]

## entrypoint.sh
#!/bin/bash
set -ex

## 正常启动用 tini启动
if [ "$1" = 'tini' -a "$(id -u)" = '0' ]; then
    exec gosu someone "$0" "$@";
fi

exec "$@"

#!/bin/bash
set -ex

## 正常启动用 tini启动
if [ "$1" = 'tini' -a "$(id -u)" = '0' ]; then
    /usr/local/bin/python3.8 manage.py compilemessages;
    /usr/local/bin/python3.8 manage.py migrate;
    # 无法修改configmap挂载的配置文件
    # chown -R someone:someone /var/someone;
    exec gosu someone "$0" "$@";
fi

exec "$@"


ENTRYPOINT ["/var/arkid/docker-entrypoint.sh"]
CMD ["tini", "--", "/usr/local/bin/python3.8", "manage.py", "runserver", "0.0.0.0:80"]

someone        1     0  tini -- /usr/local/bin/python3.8 manage.py runserver 0.0.0.0:80
someone      199     1  /usr/local/bin/python3.8 manage.py runserver 0.0.0.0:80
someone      292   199  /usr/local/bin/python3.8 manage.py runserver 0.0.0.0:80



# exec形式,首选形式, exec 执行命令.
ENTRYPOINT ["executable", "param1", "param2"]
# shell形式,sh -c .
ENTRYPOINT command param1 param2

# exec形式,这是首选形式.
CMD ["executable","param1","param2"]
# 提供给ENTRYPOINT的默认参数.
CMD ["param1","param2"]
# shell形式, sh -c .
CMD command param1 param2


## gosu无法绑定0-1023
0-1023 是系统保留端口,只能root或者sudo启动才能绑定:
Error: You don't have permission to access that port.

可以配置一下,内核版本也有要求>=2.6.24
setcap 'cap_net_bind_service=+ep' /usr/local/bin/python3.8

三、其它优化

3.1、使用LABEL

LABEL maintainer="dev@someproject.org"
LABEL build_date="2017-09-05"

3.2、安装依赖程序的时候使用固定版本

3.3、时间/时区等设置正确

ARG tz="Asia/Shanghai"
ARG apk="mirrors.ustc.edu.cn"

RUN sed -i "s/dl-cdn.alpinelinux.org/${apk}/g" /etc/apk/repositories &&\
    apk update &&\
    apk add tini tzdata su-exec &&\
    cp /usr/share/zoneinfo/${tz} /etc/localtime &&\
    echo ${tz} > /etc/timezone

3.4、多使用ARG 可以在构建时传入参数:比如镜像版本,某个依赖的版本,时区; ARG 在FROM前后是分开的

3.5、优雅停止, STOPSIGNAL

3.6、健康检查,HEALTHCHECK,在k8s里有健康检查这个就不重要了

HEALTHCHECK --interval=2m --timeout=10s --start-period=5s --retries=3 CMD curl -f http://127.0.0.1:8080/ || exit 1

四、其它问题解决

openjdk(jenkins等)时区问题

# 在构建Dockerfile时解决
ENV TZ="Asia/Shanghai"
RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime &&\
       echo ${TZ} > /etc/timezone

# 在启动时挂载
-v /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime
# timezone文件里写 Asia/Shanghai
-v /xxx/timezone:/etc/timezone  

# pod里使用configmap挂载
apiVersion: v1
Kind: ConfigMap
data:
  timezone: Asia/Shanghai
metadata:
  name: timezone

  volumeMounts:
    - mountPath: /etc/timezone
      name: config-volume
      subPath: timezone
volumes:
  - name: config-volume
    configMap:
      name: timezone

COPY指令

不能使用 ./* , 加*无法复制文件夹
COPY 文件不能用相对路径,只能用绝对路径,源和目的都是。