赞
踩
构建体积更小的镜像,有利于镜像本身的传输,尤其在 Kubernetes 的环境中,镜像的传输会更加频繁,更小的体积意味着更快的调度时间和更小的空间占用。那如何构建更加小的镜像呢?
不同的基础镜像构建相同的应用会有不同的大小。使用更小基础镜像会有利于减小最终镜像的大小。建议非特殊情况下,使用 alpine 镜像。他的基础镜像只有不到 5MB, 而且还自带包管理工具。一般需求都可以满足。
- FROM alpine
- RUN apk add nginx
如果你的应用可以独立运行,如 Go 编译出来的 Binary 文件,还可以直接使用 scratch 这个空白镜像。
- FROM scratch
- COPY mybinary /mybinary
- CMD [ "/mybinary" ]
不同的 RUN 命令如果先后做了文件的增加和删除,因为镜像分层文件系统的原因,还是会增加镜像体积的。如果两个 RUN 命令合并后,才会达到所想的效果。
- ...
- RUN curl http://xyz.abc -o abc.tar.gz \
- ...
- rm -rf abc.tar.gz
- ...
在安装包的过程中,尽量只安装需要用到的包。尤其是在编译过程中,难免会安装一些运行过程中不需要的软件包,如 glibc-devel 等。那在最后,最好把这些包去掉,来节省镜像空间。又因为镜像分层系统的原因,这些安装和卸载的语法还应该处理同一个 RUN 命令下
- ...
- RUN BUILD_DEP_PKGS="
- gcc \
- libc-devel"
- && yum -y install $BUILD_DEP_PKGS \
- ...
- && yum -y remove $BUILD_DEP_PKGS
- ...
apt
和 yum/dnf
在安装包的过程中,默认会自动安装一些弱依赖包。一般情况下是使用不到的。可以显示的关闭弱依赖。
- # apt-get for Debian family
- apt-get -y install --no-install-recommends {{ package }}
-
- # yum from RHEL family
- yum -y --setopt=install_weak_deps=false \
- --setopt=tsflags=nodocs \
- --setopt=override_install_langs=en_US.utf8 \
- 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 数据库1。可以使用 useradd -l
或把 /var/log/faillog
和 /var/log/lastlog
文件删除掉,彻底关掉该功能。每个用户大概用占用掉 4k 左右的空间。
对于很多需要编译运行的程序,如 Java, C, Golang 等,其编译环境和运行环境是不一样的,为了减小最终镜像的大小,我们可以使用上面提到的合并 RUN
命令的方式,但是看着会比较乱,比较难以维护。docker daemon >= 17.05 3版本的时候支持 multi stage 构建,解决了此类问题,他使用两个镜像来构建,通过 COPY --from
语法,在两个镜像之间传递构建好的文件。使用方法如下:
- FROM golang:1.7.3 as builder
- WORKDIR /go/src/github.com/alexellis/href-counter/
- RUN go get -d -v golang.org/x/net/html
- COPY app.go .
- RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
-
- FROM alpine:latest
- RUN apk --no-cache add ca-certificates
- WORKDIR /root/
- COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
- CMD ["./app"]
它使用 golang:1.7.3
做为构建镜像,来构建出名为 app
的应用,然后把 app
再拷贝到以 alpine:latest
为基础的镜像里面,这样做出来的镜像肯定会很小。
dive 是一个命令行工具,可以分析镜像里面每层的空间占用大小,有助于发现问题,以减少镜像大小。
docker 自从 1.13 版本增加了一个 squash 的 experimental
特性 2。他可以把把当前 Dockerfile 文件里面的多个层在构建的时候自动合并成一个。但是不支持合并其父镜像。使用前,需要打开 docker daemon 的 experiment 特性。使用方法如下
$ docker build --squash .
除了这个方法,还有一个工具叫 docker squash , 他支持合并镜像的任意层。使用方法如下
- $ pip install docker-squash
- $ docker-squash -t kolla/centos-source-base:new \
- kolla/centos-source-base:master
- Old image has 40 layers
- Attempting to squash last 40 layers...
- ...
- Original image size: 388.51 MB
- Squashed image size: 281.43 MB
- Image size decreased by 27.56 %
- Image registered in Docker daemon as kolla/centos-source-base:new
可以看到,通过 squash
镜像大小从 388.51MB
缩小到了 281.43MB
,缩小了 27.56%
。
注意:在使用 squash 的时候,会消耗大量的硬盘 IO,如果硬盘性能不好,速度会比较慢
重启容器的时候,经常会很慢,而且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 进程。所以在制作镜像的时候最好安装上其中一个。
- # install dumb-init
- RUN wget -O /usr/local/bin/dumb-init \
- https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64
- RUN chmod +x /usr/local/bin/dumb-init
-
- # Runs "/usr/bin/dumb-init -- /my/script --with --args"
- ENTRYPOINT ["/usr/bin/dumb-init", "--"]
- CMD ["/my/script", "--with", "--args"]
如果是使用 docker 的情况下,docker 自 1.13 之后已经自带一个 init 进程,其实就是 tini, 使用方法如下
- $ docker run --init centos:7 ps auxf
- USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
- root 1 0.0 0.0 1000 4 ? Ss 03:45 0:00 /dev/init -- ps auxf
- root 6 0.0 0.0 51748 3444 ? R 03:45 0:00 ps auxf
虽然 container 在运行的时候 ,是与系统隔离开的,但是如果你使用 root 运行容器的话,他在系统层面上看,也是 root 账号,权限是比较大的,会有一定的风险。更安全的做法是使用非root 账号运行。这点在 openshift 上面体现的比较明显,他默认是使用一个很大的 uid 来运行,来限制容器内的进程权限。
在使用非 root 账号运行,尤其是在 kubernetes/openshift 这种环境中,uid 是不能事先确认的,需要对读写目录标记上0777
的权限。包括某些可能通过 volume 挂载的目录。
注意,对于 volume 挂载的目录,在 kubernetes/openshift 环境中,并不是必要的,volume 目录的 uid / gid 会由 kubelet 自动配置好。
例子如下
- RUN ... \
- && rm -rf /etc/nginx/*.default \
- && chmod -R a+rwx ${NGINX_HOME_TMP} \
- && chmod -R a+rwx /etc/nginx \
- && chmod -R a+rwx /var/log/nginx \
- && chmod -R a+rwx /var/run/nginx
把需要 nginx 读写的地方配置上 rwx
权限,这样不管 nginx 进程的 uid
是多少,都可以正常运行。
理论上讲,docker 的镜像内容是不可变的,其 tag 应该是和 git 的 tag 是一样的,应该是一个只读值不应该变化。但是平时大家在使用的时候,习惯的使用 :latest
, 这样在生产环境中是比较危险的,很容易搞错镜像。我建议使用如下风格的 tag 命令方式来管理 image.
- X.Y.Z-N
-
- X.Y.Z 跟随镜像里面关联代码的版本号
- N 使用构建次数,或使用时间戳。来标记 Dockerfile 的变化及构建时间
现在,除了 Dockerfile
这种构建方式,还有很多其它的镜像构建工具,有兴趣可以深入研究下
- FROM centos:7
-
- EXPOSE 8080
- EXPOSE 8443
-
- ARG http_proxy
- ARG https_proxy
-
- ARG NGINX_MODULE_VTS_VERSION=v0.1.18
- ARG NGINX_VERSION=1.13.3
- ARG TINI_VERSION=v0.18.0
-
- ENV NGINX_GROUP nginx
- ENV NGINX_HOME_TMP /var/spool/nginx/tmp
- ENV NGINX_HOME /var/lib/nginx
- ENV NGINX_USER nginx
-
- ENV NGINX_DOWNLOAD_URL http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz
-
- RUN BUILD_DEP_PKGS="\
- cpp \
- expat-devel \
- fontconfig-devel \
- freetype-devel \
- gcc \
- gd-devel \
- git \
- glibc-devel \
- glibc-headers \
- kernel-headers \
- keyutils-libs-devel \
- krb5-devel \
- libcom_err-devel \
- libjpeg-turbo-devel \
- libpng-devel \
- libselinux-devel \
- libsepol-devel \
- libverto-devel \
- libX11-devel \
- libXau-devel \
- libxcb-devel \
- libxcb-devel \
- libXpm-devel \
- make \
- openssl-devel \
- pcre-devel \
- xorg-x11-proto-devel \
- zlib-devel \
- " \
- && rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 \
- && yum -y --setopt=install_weak_deps=false --setopt=tsflags=nodocs --setopt=override_install_langs=en_US.utf8 \
- install \
- ${BUILD_DEP_PKGS} \
- rsync \
- wget \
- && curl -L https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini -o /tini \
- && chmod +x /tini \
- && wget ${NGINX_DOWNLOAD_URL} -O /tmp/nginx.tar.gz \
- && tar xf /tmp/nginx.tar.gz -C /tmp \
- && git clone --branch ${NGINX_MODULE_VTS_VERSION} https://github.com/vozlt/nginx-module-vts.git /tmp/nginx-module-vts \
- && ( \
- cd /tmp/nginx-${NGINX_VERSION} \
- && ./configure \
- --add-module=/tmp/nginx-module-vts \
- --conf-path=/etc/nginx/nginx.conf \
- --error-log-path=/var/log/nginx/error.log \
- --group=${NGINX_GROUP} \
- --http-client-body-temp-path=${NGINX_HOME_TMP}/client_body \
- --http-fastcgi-temp-path=${NGINX_HOME_TMP}/fastcgi \
- --http-log-path=/var/log/nginx/access.log \
- --http-proxy-temp-path=${NGINX_HOME_TMP}/proxy \
- --http-uwsgi-temp-path=${NGINX_HOME_TMP}/uwsgi \
- --lock-path=/var/local/subsys/nginx \
- --modules-path=/usr/lib64/nginx/modules \
- --pid-path=/var/run/nginx/nginx.pid \
- --prefix=/etc/nginx \
- --sbin-path=/usr/bin/nginx \
- --user=${NGINX_USER} \
- --with-http_gzip_static_module \
- --with-http_image_filter_module \
- --with-http_realip_module \
- --with-http_secure_link_module \
- --with-http_ssl_module \
- --with-http_stub_status_module \
- --with-http_sub_module \
- --with-http_v2_module \
- --without-http_scgi_module \
- --without-mail_imap_module \
- --without-mail_pop3_module \
- --without-mail_smtp_module \
- --without-poll_module \
- --without-select_module \
- --with-threads \
- && make -j4 \
- && make install \
- && cp -r conf/* /etc/nginx/ \
- ) \
- && rm -rf /tmp/nginx.tar.gz \
- /tmp/nginx-module-vts \
- /tmp/nginx-${NGINX_VERSION} \
- && yum -y remove ${BUILD_DEP_PKGS} \
- && yum clean all \
- && mkdir -p ${NGINX_HOME_TMP}/{client_body,proxy,fastcgi,uwsgi} \
- && mkdir -p /usr/share/nginx \
- && mkdir -p /var/run/nginx \
- && mkdir -p /var/www \
- && mv /etc/nginx/html /usr/share/nginx/ \
- && rm -rf /etc/nginx/*.default \
- && chmod -R a+rwx ${NGINX_HOME_TMP} \
- && chmod -R a+rwx /etc/nginx \
- && chmod -R a+rwx /var/log/nginx \
- && chmod -R a+rwx /var/run/nginx \
- && sed -i '/listen/s,listen\s*80;,listen 8080;,g' /etc/nginx/nginx.conf \
- && sed -i '/^#user/s,#\(user.*\),\1,g' /etc/nginx/nginx.conf \
- && ln -sf /var/log/nginx /etc/nginx/logs \
- && ln -sf /var/www /etc/nginx/www
https://github.com/sagemathinc/cocalc/issues/2287#issue-249824529 ↩
https://docs.docker.com/engine/reference/commandline/build/#squash-an-images-layers---squash-experimental ↩
原始链接:制作容器镜像的最佳实践 - 代码杂货铺
许可协议:"署名-非商用-相同方式共享 3.0" 转载请保留原文链接及作者。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。