当前位置:   article > 正文

制作容器镜像的最佳实践_yum no-install-recommends

yum no-install-recommends

构建体积更小的镜像

构建体积更小的镜像,有利于镜像本身的传输,尤其在 Kubernetes 的环境中,镜像的传输会更加频繁,更小的体积意味着更快的调度时间和更小的空间占用。那如何构建更加小的镜像呢?

使用更小的基础镜像

不同的基础镜像构建相同的应用会有不同的大小。使用更小基础镜像会有利于减小最终镜像的大小。建议非特殊情况下,使用 alpine 镜像。他的基础镜像只有不到 5MB, 而且还自带包管理工具。一般需求都可以满足。

  1. FROM alpine
  2. RUN apk add nginx

如果你的应用可以独立运行,如 Go 编译出来的 Binary 文件,还可以直接使用 scratch 这个空白镜像。

  1. FROM scratch
  2. COPY mybinary /mybinary
  3. CMD [ "/mybinary" ]

合并 RUN 命令

不同的 RUN 命令如果先后做了文件的增加和删除,因为镜像分层文件系统的原因,还是会增加镜像体积的。如果两个 RUN 命令合并后,才会达到所想的效果。

  1. ...
  2. RUN curl http://xyz.abc -o abc.tar.gz \
  3. ...
  4. rm -rf abc.tar.gz
  5. ...

去除掉不用的软件包

在安装包的过程中,尽量只安装需要用到的包。尤其是在编译过程中,难免会安装一些运行过程中不需要的软件包,如 glibc-devel 等。那在最后,最好把这些包去掉,来节省镜像空间。又因为镜像分层系统的原因,这些安装和卸载的语法还应该处理同一个 RUN 命令下

  1. ...
  2. RUN BUILD_DEP_PKGS="
  3. gcc \
  4. libc-devel"
  5. && yum -y install $BUILD_DEP_PKGS \
  6. ...
  7. && yum -y remove $BUILD_DEP_PKGS
  8. ...

只安装必要依赖包的

apt 和 yum/dnf 在安装包的过程中,默认会自动安装一些弱依赖包。一般情况下是使用不到的。可以显示的关闭弱依赖。

  1. # apt-get for Debian family
  2. apt-get -y install --no-install-recommends {{ package }}
  3. # yum from RHEL family
  4. yum -y --setopt=install_weak_deps=false \
  5. --setopt=tsflags=nodocs \
  6. --setopt=override_install_langs=en_US.utf8 \
  7. install vim
  • --no-install-recommends : 不安装推荐依赖
  • --setopt=install_weak_deps=false: 不安装弱依赖包
  • --setopt=tsflags=nodocs: 不安装文档包
  • --setopt=override_install_langs=en_US.utf8: 设置默认语言为 en_US.utf8

关闭用户初始化 lastlog 和 faillog 数据库

如果你要在镜像中创建新的用户,可以不初始化用户相关的 lastlog 和 faillog 数据库1。可以使用 useradd -l 或把 /var/log/faillog 和 /var/log/lastlog 文件删除掉,彻底关掉该功能。每个用户大概用占用掉 4k 左右的空间。

使用 multi stage 构建

对于很多需要编译运行的程序,如 Java, C, Golang 等,其编译环境和运行环境是不一样的,为了减小最终镜像的大小,我们可以使用上面提到的合并 RUN 命令的方式,但是看着会比较乱,比较难以维护。docker daemon >= 17.05 3版本的时候支持 multi stage 构建,解决了此类问题,他使用两个镜像来构建,通过 COPY --from 语法,在两个镜像之间传递构建好的文件。使用方法如下:

  1. FROM golang:1.7.3 as builder
  2. WORKDIR /go/src/github.com/alexellis/href-counter/
  3. RUN go get -d -v golang.org/x/net/html
  4. COPY app.go .
  5. RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
  6. FROM alpine:latest
  7. RUN apk --no-cache add ca-certificates
  8. WORKDIR /root/
  9. COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
  10. CMD ["./app"]

它使用 golang:1.7.3 做为构建镜像,来构建出名为 app 的应用,然后把 app 再拷贝到以 alpine:latest 为基础的镜像里面,这样做出来的镜像肯定会很小。

缩小镜像体积

使用 dive 分析镜像的空间占用

dive 是一个命令行工具,可以分析镜像里面每层的空间占用大小,有助于发现问题,以减少镜像大小。

 

合并镜像的多个层

docker 自从 1.13 版本增加了一个 squash 的 experimental特性 2。他可以把把当前 Dockerfile 文件里面的多个层在构建的时候自动合并成一个。但是不支持合并其父镜像。使用前,需要打开 docker daemon 的 experiment 特性。使用方法如下

$ docker build --squash .

 除了这个方法,还有一个工具叫 docker squash , 他支持合并镜像的任意层。使用方法如下

  1. $ pip install docker-squash
  2. $ docker-squash -t kolla/centos-source-base:new \
  3. kolla/centos-source-base:master
  4. Old image has 40 layers
  5. Attempting to squash last 40 layers...
  6. ...
  7. Original image size: 388.51 MB
  8. Squashed image size: 281.43 MB
  9. Image size decreased by 27.56 %
  10. Image registered in Docker daemon as kolla/centos-source-base:new

可以看到,通过 squash 镜像大小从 388.51MB 缩小到了 281.43MB ,缩小了 27.56% 。

注意:在使用 squash 的时候,会消耗大量的硬盘 IO,如果硬盘性能不好,速度会比较慢

镜像安全

init 进程

重启容器的时候,经常会很慢,而且docker daemon 日志中经常会抛出以下错误

dockerd[559]: msg="Container 5054f failed to exit within 10 seconds of signal 15 - using the force"

默认的的 signal 15 根本就没有使其退出,最后还是 10 秒超时后强制退出(kill)的。而且有时还会出现大量僵尸进程。

容器化后,由于单容器单进程,已经没有传统意义上的 init 进程。应用进程直接占用了 pid 1 的进程号,应用默认是不会处理SIGTERM 信号的,所以会导致 signal 15 不能使进程正常退出,只能使用 SIGKILL 直接杀死进程。而这可能导致某些资源不能正常回收。另外 pid 1 进程还需要负责其子进程的回收工作,但是一般应用也不会对此进行处理。所以会导致僵尸进程的出现。更多解释,请参看我写的另外一篇文章:Docker init 进程

解决方案是使用 tini , 或 dumb-init 等轻量级 init 进程。所以在制作镜像的时候最好安装上其中一个。

  1. # install dumb-init
  2. RUN wget -O /usr/local/bin/dumb-init \
  3. https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64
  4. RUN chmod +x /usr/local/bin/dumb-init
  5. # Runs "/usr/bin/dumb-init -- /my/script --with --args"
  6. ENTRYPOINT ["/usr/bin/dumb-init", "--"]
  7. CMD ["/my/script", "--with", "--args"]

如果是使用 docker 的情况下,docker 自 1.13 之后已经自带一个 init 进程,其实就是 tini, 使用方法如下

  1. $ docker run --init centos:7 ps auxf
  2. USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
  3. root 1 0.0 0.0 1000 4 ? Ss 03:45 0:00 /dev/init -- ps auxf
  4. root 6 0.0 0.0 51748 3444 ? R 03:45 0:00 ps auxf

Drop root

虽然 container 在运行的时候 ,是与系统隔离开的,但是如果你使用 root 运行容器的话,他在系统层面上看,也是 root 账号,权限是比较大的,会有一定的风险。更安全的做法是使用非root 账号运行。这点在 openshift 上面体现的比较明显,他默认是使用一个很大的 uid 来运行,来限制容器内的进程权限。

在使用非 root 账号运行,尤其是在 kubernetes/openshift 这种环境中,uid 是不能事先确认的,需要对读写目录标记上0777 的权限。包括某些可能通过 volume 挂载的目录。

注意,对于 volume 挂载的目录,在 kubernetes/openshift 环境中,并不是必要的,volume 目录的 uid / gid 会由 kubelet 自动配置好。

例子如下

  1. RUN ... \
  2. && rm -rf /etc/nginx/*.default \
  3. && chmod -R a+rwx ${NGINX_HOME_TMP} \
  4. && chmod -R a+rwx /etc/nginx \
  5. && chmod -R a+rwx /var/log/nginx \
  6. && chmod -R a+rwx /var/run/nginx

 把需要 nginx 读写的地方配置上 rwx 权限,这样不管 nginx 进程的 uid 是多少,都可以正常运行。

镜像 tag 管理

理论上讲,docker 的镜像内容是不可变的,其 tag 应该是和 git 的 tag 是一样的,应该是一个只读值不应该变化。但是平时大家在使用的时候,习惯的使用 :latest , 这样在生产环境中是比较危险的,很容易搞错镜像。我建议使用如下风格的 tag 命令方式来管理 image.

  1. X.Y.Z-N
  2. X.Y.Z 跟随镜像里面关联代码的版本号
  3. N 使用构建次数,或使用时间戳。来标记 Dockerfile 的变化及构建时间

 

其它镜像构建方式

现在,除了 Dockerfile 这种构建方式,还有很多其它的镜像构建工具,有兴趣可以深入研究下

  • source to image: openshift 里面带的一个方便由源代码直接生成镜像的方案,可以单独使用。
  • makisu: 方便在 kubernetes 环境中使用的镜像构建方案,可以 unprivileged 的容器中使用
  • buildash: 构建 OCI 兼容格式镜像工具。

Example

  1. FROM centos:7
  2. EXPOSE 8080
  3. EXPOSE 8443
  4. ARG http_proxy
  5. ARG https_proxy
  6. ARG NGINX_MODULE_VTS_VERSION=v0.1.18
  7. ARG NGINX_VERSION=1.13.3
  8. ARG TINI_VERSION=v0.18.0
  9. ENV NGINX_GROUP nginx
  10. ENV NGINX_HOME_TMP /var/spool/nginx/tmp
  11. ENV NGINX_HOME /var/lib/nginx
  12. ENV NGINX_USER nginx
  13. ENV NGINX_DOWNLOAD_URL http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz
  14. RUN BUILD_DEP_PKGS="\
  15. cpp \
  16. expat-devel \
  17. fontconfig-devel \
  18. freetype-devel \
  19. gcc \
  20. gd-devel \
  21. git \
  22. glibc-devel \
  23. glibc-headers \
  24. kernel-headers \
  25. keyutils-libs-devel \
  26. krb5-devel \
  27. libcom_err-devel \
  28. libjpeg-turbo-devel \
  29. libpng-devel \
  30. libselinux-devel \
  31. libsepol-devel \
  32. libverto-devel \
  33. libX11-devel \
  34. libXau-devel \
  35. libxcb-devel \
  36. libxcb-devel \
  37. libXpm-devel \
  38. make \
  39. openssl-devel \
  40. pcre-devel \
  41. xorg-x11-proto-devel \
  42. zlib-devel \
  43. " \
  44. && rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 \
  45. && yum -y --setopt=install_weak_deps=false --setopt=tsflags=nodocs --setopt=override_install_langs=en_US.utf8 \
  46. install \
  47. ${BUILD_DEP_PKGS} \
  48. rsync \
  49. wget \
  50. && curl -L https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini -o /tini \
  51. && chmod +x /tini \
  52. && wget ${NGINX_DOWNLOAD_URL} -O /tmp/nginx.tar.gz \
  53. && tar xf /tmp/nginx.tar.gz -C /tmp \
  54. && git clone --branch ${NGINX_MODULE_VTS_VERSION} https://github.com/vozlt/nginx-module-vts.git /tmp/nginx-module-vts \
  55. && ( \
  56. cd /tmp/nginx-${NGINX_VERSION} \
  57. && ./configure \
  58. --add-module=/tmp/nginx-module-vts \
  59. --conf-path=/etc/nginx/nginx.conf \
  60. --error-log-path=/var/log/nginx/error.log \
  61. --group=${NGINX_GROUP} \
  62. --http-client-body-temp-path=${NGINX_HOME_TMP}/client_body \
  63. --http-fastcgi-temp-path=${NGINX_HOME_TMP}/fastcgi \
  64. --http-log-path=/var/log/nginx/access.log \
  65. --http-proxy-temp-path=${NGINX_HOME_TMP}/proxy \
  66. --http-uwsgi-temp-path=${NGINX_HOME_TMP}/uwsgi \
  67. --lock-path=/var/local/subsys/nginx \
  68. --modules-path=/usr/lib64/nginx/modules \
  69. --pid-path=/var/run/nginx/nginx.pid \
  70. --prefix=/etc/nginx \
  71. --sbin-path=/usr/bin/nginx \
  72. --user=${NGINX_USER} \
  73. --with-http_gzip_static_module \
  74. --with-http_image_filter_module \
  75. --with-http_realip_module \
  76. --with-http_secure_link_module \
  77. --with-http_ssl_module \
  78. --with-http_stub_status_module \
  79. --with-http_sub_module \
  80. --with-http_v2_module \
  81. --without-http_scgi_module \
  82. --without-mail_imap_module \
  83. --without-mail_pop3_module \
  84. --without-mail_smtp_module \
  85. --without-poll_module \
  86. --without-select_module \
  87. --with-threads \
  88. && make -j4 \
  89. && make install \
  90. && cp -r conf/* /etc/nginx/ \
  91. ) \
  92. && rm -rf /tmp/nginx.tar.gz \
  93. /tmp/nginx-module-vts \
  94. /tmp/nginx-${NGINX_VERSION} \
  95. && yum -y remove ${BUILD_DEP_PKGS} \
  96. && yum clean all \
  97. && mkdir -p ${NGINX_HOME_TMP}/{client_body,proxy,fastcgi,uwsgi} \
  98. && mkdir -p /usr/share/nginx \
  99. && mkdir -p /var/run/nginx \
  100. && mkdir -p /var/www \
  101. && mv /etc/nginx/html /usr/share/nginx/ \
  102. && rm -rf /etc/nginx/*.default \
  103. && chmod -R a+rwx ${NGINX_HOME_TMP} \
  104. && chmod -R a+rwx /etc/nginx \
  105. && chmod -R a+rwx /var/log/nginx \
  106. && chmod -R a+rwx /var/run/nginx \
  107. && sed -i '/listen/s,listen\s*80;,listen 8080;,g' /etc/nginx/nginx.conf \
  108. && sed -i '/^#user/s,#\(user.*\),\1,g' /etc/nginx/nginx.conf \
  109. && ln -sf /var/log/nginx /etc/nginx/logs \
  110. && ln -sf /var/www /etc/nginx/www

 

REF


  1. https://github.com/sagemathinc/cocalc/issues/2287#issue-249824529 

  2. https://docs.docker.com/engine/reference/commandline/build/#squash-an-images-layers---squash-experimental 

  3. Multi-stage builds | Docker Documentation 

  4. 10 tips for building and managing containers 

  5. Dockerfile Security Best Practices 

原始链接:制作容器镜像的最佳实践 - 代码杂货铺

许可协议:"署名-非商用-相同方式共享 3.0" 转载请保留原文链接及作者。

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/684431
推荐阅读
相关标签
  

闽ICP备14008679号