
与软件开发的任何其他流程一样,每个开发人员都会列出他希望加快 Docker 构建速度的原因。我将列出对我来说最重要的原因。本文将在其中逐步介绍 Rails 默认 Dockerfile 的每一行,并解释最佳实践和优化。
Docker 镜像可以通过不同的方式进行优化,包括但不限于镜像大小缩减、构建性能优化、安全性和可维护性最佳实践以及特定于应用程序的优化。在第一篇文章中,我将只涉及镜像大小缩减优化,并解释它们为何重要。
更快的构建和部署
较小的镜像构建速度更快,因为需要处理的文件和层更少。这提高了开发人员的生产力,尤其是在迭代开发周期中。较小的镜像在部署期间推送到注册表和从注册表中拉取所需的时间更少。这在容器频繁构建和部署的 CI/CD 管道中尤其重要。
降低存储成本和网络带宽使用
较小的镜像在容器注册表、本地开发机器和生产服务器上消耗更少的存储空间。这降低了基础设施成本,尤其是对于大规模部署。较小的镜像在服务器之间传输时使用更少的带宽,这在你在本地或 CI/CD 管道中构建镜像并将其推送到注册表时尤为重要。
提升性能和安全性
较小的镜像需要更少的资源(例如,CPU、RAM)来加载和运行,从而提高容器化应用程序的整体性能。更快的启动时间意味着你的服务可以更快地准备就绪,这对于扩展和高可用性系统至关重要。像 alpine 或 debian-slim 这样的最小基础镜像包含更少的预装软件包,从而降低了未修补或不必要的软件被利用的风险。
除了以上所有提到的内容之外,删除不必要的文件和工具还可以最大限度地减少诊断问题时的干扰,并带来更好的可维护性和减少技术债务。
检查 Docker 镜像
要获取镜像的不同参数,包括大小,你可以查看 Docker 桌面或在终端中运行 docker images 命令。
➜ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
kamal-dashboard latest 673737b771cd 2 days ago 619MB
kamal-proxy latest 5f6cd8983746 6 weeks ago 115MB
docs-server latest a810244e3d88 6 weeks ago 1.18GB
busybox latest 63cd0d5fb10d 3 months ago 4.04MB
postgres latest 6c9aa6ecd71d 3 months ago 456MB
postgres 16.4 ced3ad69d60c 3 months ago 453MB
了解镜像的大小并不能让你全面了解情况。你不知道镜像中有什么,它有多少层,或者每一层有多大。Docker 镜像层是一个只读的、不可变的文件系统层,它是 Docker 镜像的一个组件。每一层都代表对镜像文件系统进行的一组更改,例如添加文件、修改配置或安装软件。
Docker 镜像是逐层增量构建的,每一层都对应于 Dockerfile 中的一条指令。要获取镜像的层,你可以运行 docker history 命令。
➜ docker history kamal-dashboard:latest
IMAGE CREATED CREATED BY SIZE COMMENT
673737b771cd 4 days ago CMD ["./bin/thrust" "./bin/rails" "server"] 0B buildkit.dockerfile.v0
<missing> 4 days ago EXPOSE map[80/tcp:{}] 0B buildkit.dockerfile.v0
<missing> 4 days ago ENTRYPOINT ["/rails/bin/docker-entrypoint"] 0B buildkit.dockerfile.v0
<missing> 4 days ago USER 1000:1000 0B buildkit.dockerfile.v0
<missing> 4 days ago RUN /bin/sh -c groupadd --system --gid 1000 … 54MB buildkit.dockerfile.v0
<missing> 4 days ago COPY /rails /rails # buildkit 56.2MB buildkit.dockerfile.v0
<missing> 4 days ago COPY /usr/local/bundle /usr/local/bundle # b… 153MB buildkit.dockerfile.v0
<missing> 4 days ago ENV RAILS_ENV=production BUNDLE_DEPLOYMENT=1… 0B buildkit.dockerfile.v0
<missing> 4 days ago RUN /bin/sh -c apt-get update -qq && apt… 137MB buildkit.dockerfile.v0
<missing> 4 days ago WORKDIR /rails 0B buildkit.dockerfile.v0
<missing> 3 weeks ago CMD ["irb"] 0B buildkit.dockerfile.v0
<missing> 3 weeks ago RUN /bin/sh -c set -eux; mkdir "$GEM_HOME";… 0B buildkit.dockerfile.v0
<missing> 3 weeks ago ENV PATH=/usr/local/bundle/bin:/usr/local/sb… 0B buildkit.dockerfile.v0
<missing> 3 weeks ago ENV BUNDLE_SILENCE_ROOT_WARNING=1 BUNDLE_APP… 0B buildkit.dockerfile.v0
<missing> 3 weeks ago ENV GEM_HOME=/usr/local/bundle 0B buildkit.dockerfile.v0
<missing> 3 weeks ago RUN /bin/sh -c set -eux; savedAptMark="$(a… 78.1MB buildkit.dockerfile.v0
<missing> 3 weeks ago ENV RUBY_DOWNLOAD_SHA256=018d59ffb52be3c0a6d… 0B buildkit.dockerfile.v0
<missing> 3 weeks ago ENV RUBY_DOWNLOAD_URL=https://cache.ruby-lan… 0B buildkit.dockerfile.v0
<missing> 3 weeks ago ENV RUBY_VERSION=3.4.1 0B buildkit.dockerfile.v0
<missing> 3 weeks ago ENV LANG=C.UTF-8 0B buildkit.dockerfile.v0
<missing> 3 weeks ago RUN /bin/sh -c set -eux; mkdir -p /usr/loca… 19B buildkit.dockerfile.v0
<missing> 3 weeks ago RUN /bin/sh -c set -eux; apt-get update; a… 43.9MB buildkit.dockerfile.v0
<missing> 3 weeks ago # debian.sh --arch 'arm64' out/ 'bookworm' '… 97.2MB debuerreotype 0.15
由于我已经提供了关于镜像和层的理论,现在是时候探索 Dockerfile 了。从 Rails 7.1 开始,Dockerfile 会随着新的 Rails 应用程序一起生成。以下是一个示例,展示它可能是什么样子。
# syntax=docker/dockerfile:1
# check=error=true
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.1
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
# Rails app lives here
WORKDIR /rails
# Install base packages
# Replace libpq-dev with sqlite3 if using SQLite, or libmysqlclient-dev if using MySQL
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Set production environment
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development"
# Throw-away build stage to reduce size of final image
FROM base AS build
# Install packages needed to build gems
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential curl git pkg-config libyaml-dev && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
bundle exec bootsnap precompile --gemfile
# Copy application code
COPY . .
# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
# Final stage for app image
FROM base
# Copy built artifacts: gems, application
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --from=build /rails /rails
# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
chown -R rails:rails db log storage tmp
USER 1000:1000
# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]
下面我将提供一系列应用于上面 Dockerfile 的方法和规则,以使最终镜像大小高效。
优化软件包安装
我确信你只在你的本地开发机器上保留需要的软件。这同样应该应用于 Docker 镜像。在下面的例子中,我将持续恶化从上面的 Rails Dockerfile 中提取的 [Dockerfile]。我将其称为原始 Dockerfile 版本。
规则 #1:使用最小基础镜像
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
基础镜像是 Dockerfile 的起点。它是用于创建容器的镜像。基础镜像是 Dockerfile 中的第一层,它是唯一一个不是由 Dockerfile 本身创建的层。
基础镜像是用 FROM 命令指定的,后面跟着镜像名称和标签。标签是可选的,如果没有指定,则使用 latest 标签。基础镜像可以是 Docker Hub 或任何其他注册表上可用的任何镜像。
在上面的 Dockerfile 中,我们使用带有 3.4.1-slim 标签的 ruby 镜像。ruby 镜像是 Docker Hub 上可用的 [官方 Ruby 镜像]。3.4.1-slim 标签是基于 debian-slim 镜像的 Ruby 镜像的精简版本。而 debian-slim 镜像是 Debian Linux 镜像的最小版本,已针对大小进行了优化。请查看下表,以了解 slim 镜像的尺寸有多小。
➜ docker images --filter "reference=ruby"
REPOSITORY TAG IMAGE ID CREATED SIZE
ruby 3.4.1-slim 0bf957e453fd 5 days ago 219MB
ruby 3.4.1-alpine cf9b1b8d4a0c 5 days ago 99.1MB
ruby 3.4.1-bookworm 1e77081540c0 5 days ago 1.01GB
_截至 2024 年 1 月,当前的 Debian 版本被称为 bookworm,前一个版本是 bullseye。
219 MB 而不是 1GB - 这是一个巨大的差异。但是如果 alpine 镜像更小呢?alpine 镜像是基于 Alpine Linux 发行版的,它是一个超轻量级的 Linux 发行版,针对大小和安全性进行了优化。Alpine 使用 musl 库(而不是 glibc)和 busybox(一组紧凑的 Unix 实用程序)而不是 GNU 对应的程序。虽然技术上可以使用 alpine 镜像来运行 Rails,但我不会在本文中介绍它。
规则 #2:最小化层数
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
Dockerfile 中的每个 RUN、COPY 和 FROM 指令都会创建一个新层。层数越多,镜像尺寸就越大。这就是为什么最佳实践是将多个命令合并到一个 RUN 指令中。为了说明这一点,让我们看一下下面的例子。
# syntax=docker/dockerfile:1
# check=error=true
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.1
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
RUN apt-get update -qq
RUN apt-get install --no-install-recommends -y curl
RUN apt-get install --no-install-recommends -y libjemalloc2
RUN apt-get install --no-install-recommends -y libvips
RUN apt-get install --no-install-recommends -y libpq-dev
RUN rm -rf /var/lib/apt/lists /var/cache/apt/archives
CMD ["echo", "Whalecome!"]
我已将 RUN 指令拆分为多行,这显然使它们更具[可读性]。但这将如何影响镜像的大小呢?让我们构建镜像并查看一下。
➜ time docker build -t no-minimize-layers --no-cache -f no-minimize-layers.dockerfile .
0.31s user 0.28s system 2% cpu 28.577 total
构建镜像花费了 28 秒,而构建[最小化层数的原始版本] 只需要 19 秒(快了近 33%)。
➜ time docker build -t original --no-cache -f original.dockerfile .
0.25s user 0.28s system 2% cpu 19.909 total
让我们检查一下镜像的大小。
➜ docker images --filter "reference=*original*" --filter "reference=*no-minimize*"
REPOSITORY TAG IMAGE ID CREATED SIZE
original latest f1363df79c8a 8 seconds ago 356MB
no-minimize-layers latest ad3945c8a8ee 43 seconds ago 379MB
层数最小化的镜像比没有最小化层数的镜像小 23 MB。这是一个 6% 的尺寸缩减。虽然在这个例子中看起来差异很小,但如果你将所有 RUN 指令拆分成多行,差异会更大。
规则 #3:只安装需要的
默认情况下,apt-get install 会安装推荐的软件包以及你要求它安装的软件包。--no-install-recommends 选项告诉 apt-get 只安装明确指定的软件包,而不是推荐的软件包。
➜ time docker build -t without-no-install-recommends --no-cache -f without-no-install-recommends.dockerfile .
0.33s user 0.30s system 2% cpu 29.786 total
➜ docker images --filter "reference=*original*" --filter "reference=*recommends*"
REPOSITORY TAG IMAGE ID CREATED SIZE
without-no-install-recommends latest 41e6e37f1e2b 3 minutes ago 426MB
minimize-layers latest dff22c85d84c 17 minutes ago 356MB
如你所见,没有 --no-install-recommends 的镜像比 [原始镜像] 大 70 MB。这是一个 16% 的尺寸增加。
使用 dive 实用程序查看哪些文件被添加到镜像中 - 在文章末尾阅读更多相关信息。
规则 #4:安装后清理
原始 Dockerfile 在 apt-get install 命令之后包含 rm -rf /var/lib/apt/lists/* /var/cache/apt/archives 命令。此命令删除安装后不再需要的软件包列表和存档。让我们看看它如何影响镜像大小,为了实现这一点,我将创建一个新的 [没有清理命令的 Dockerfile]。
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev
构建镜像所需的时间与原始镜像几乎相同,这是合理的。
➜ time docker build -t without-cleaning --no-cache -f without-cleaning.dockerfile .
0.28s user 0.30s system 2% cpu 21.658 total
让我们检查一下镜像的大小。
➜ docker images --filter "reference=*original*" --filter "reference=*cleaning*"
REPOSITORY TAG IMAGE ID CREATED SIZE
without-cleaning latest 52884fe50773 2 minutes ago 375MB
original latest f1363df79c8a 16 minutes ago 356MB
没有清理的镜像比有清理的镜像大 19 MB,这是一个 5% 的尺寸增加。
最坏的情况
如果上面提到的所有四种优化都没有应用会怎么样?让我们创建一个 [没有任何优化的 Dockerfile]并构建镜像。
# syntax=docker/dockerfile:1
# check=error=true
ARG RUBY_VERSION=3.4.1
FROM docker.io/library/ruby:$RUBY_VERSION AS base
RUN apt-get update -qq
RUN apt-get install -y curl
RUN apt-get install -y libjemalloc2
RUN apt-get install -y libvips
RUN apt-get install -y libpq-dev
CMD ["echo", "Whalecome!"]
➜ time docker build -t without-optimizations --no-cache -f without-optimizations.dockerfile .
0.46s user 0.45s system 1% cpu 1:02.21 total
哇,构建镜像花费了一分多钟。
➜ docker images --filter "reference=*original*" --filter "reference=*without-optimizations*"
REPOSITORY TAG IMAGE ID CREATED SIZE
without-optimizations latest 45671929c8e4 2 minutes ago 1.07GB
original latest f1363df79c8a 27 hours ago 356MB
没有优化的镜像比原始镜像大 714 MB,这是一个 200% 的尺寸增加。这清楚地表明了优化 Dockerfile 的重要性,更大的镜像需要更多的时间来构建并消耗更多的磁盘空间。
始终使用 .dockerignore
.dockerignore 文件类似于 Git 使用的 .gitignore 文件。它用于从构建上下文中排除文件和目录。上下文是在构建镜像时发送到 Docker 守护程序的文件和目录集。上下文作为 tarball 发送到 Docker 守护程序,因此保持其尽可能小非常重要。
如果由于某种原因,你的项目中没有 .dockerignore 文件,你可以手动创建它。我建议你使用官方 Rails .dockerignore [文件模板] 作为起点。以下是一个示例,展示它可能是什么样子。
# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files.
# Ignore git directory.
/.git/
/.gitignore
# Ignore bundler config.
/.bundle
# Ignore all environment files.
/.env*
# Ignore all default key files.
/config/master.key
/config/credentials/*.key
# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
# Ignore pidfiles, but keep the directory.
/tmp/pids/*
!/tmp/pids/.keep
# Ignore storage (uploaded files in development and any SQLite databases).
/storage/*
!/storage/.keep
/tmp/storage/*
!/tmp/storage/.keep
# Ignore assets.
/node_modules/
/app/assets/builds/*
!/app/assets/builds/.keep
/public/assets
# Ignore CI service files.
/.github
# Ignore development files
/.devcontainer
# Ignore Docker-related files
/.dockerignore
/Dockerfile*
在项目中拥有 .dockerfile 文件不仅可以从上下文中排除不必要的文件和目录(例如,来自 .github 文件夹的 GitHub 工作流或来自 node_modules 的 JavaScript 依赖项)。它还有助于避免意外地将敏感信息添加到镜像中。例如,包含环境变量的 .env 文件或用于解密凭据的 master.key _文件。
使用 Dive
当解释清楚时,上面提到的所有优化似乎都很明显。如果你已经有一个巨大的镜像,并且你不知道从哪里开始,该怎么办?
我最喜欢和最有用的工具是 [Dive]。Dive 是一个用于探索 Docker 镜像、层内容以及发现缩小镜像大小方法的 TUI 工具。Dive 可以使用你的系统软件包管理器安装,或者你可以使用其官方 Docker 镜像来运行它。让我们使用我们最坏情况下的镜像。
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest without-optimizations

Dive Docker 层检查工具
在上面的屏幕截图中,你可以看到我们对最不理想镜像的检查。Dive 显示了每一层的大小、镜像的总大小以及每一层中更改的文件(添加、修改或删除)。对我来说,这是 Dive 最有用的功能。通过在右侧面板中列出文件,你可以轻松地识别不需要的文件并删除将它们添加到镜像中的命令。
我真正喜欢 Dive 的一点是,除了拥有终端 UI 之外,它还可以提供 CI 友好的输出,这在本地开发中也很有效。要使用它,请在将 CI 环境变量设置为 true 的情况下运行 Dive,该命令的输出位于下面的屏幕截图中。
docker run -e CI=true --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest without-optimizations

Dive CI 友好的输出
我个人倾向于定期使用 Dive,例如每周一次,以确保你的镜像仍然处于良好状态。在接下来的文章中,我将介绍我用来检查 Dockerfile 的自动化工作流,包括 Dive 和 [Hadolint]。
不要压缩层
我见过的一种最小化镜像大小的方法是尝试压缩层。这个想法是将几个层合并成一个层以减小镜像大小。Docker 有一个实验性选项 --squash,除此之外,还有像 [docker-squash] 这样的第三方工具。
虽然这种方法在过去有效,但目前它已被弃用,不建议使用。压缩层破坏了 Docker 的基本功能,即层缓存。除此之外,在使用 --squash 时,你可能会无意中将早期层中的敏感文件或临时文件包含在最终镜像中。这是一种全有或全无的方法,缺乏细粒度的控制。
建议使用多阶段构建而不是压缩层。Rails Dockerfile 已经使用了多阶段构建,我将在下一篇文章中解释它是如何工作的。
结论
优化 Docker 镜像,就像任何其他优化一样,不能一劳永逸。这是一个需要定期检查和改进的持续过程。我试图涵盖基础知识,但了解和理解它们至关重要。

原创文章,作者:诺多,如若转载,请注明出处:https://www.huluohu.com/posts/1398/
微信扫一扫
支付宝扫一扫