mkosi 是一个轻量级工具，用于从发行版软件包构建镜像。本文介绍如何使用 mkosi 从 RHEL 和 RHEL 通用基础镜像 Universal Base Image （UBI）的软件包构建镜像。RHEL UBI 是 RHEL 的一个子集，可以在没有订阅的情况下免费使用。mkosi 特性mkosi 支持一些输出格式，但最重要的是 可发现磁盘镜像 Discoverable Disk Images （DDI）。同一个 DDI 可用于引导容器、或运行在虚拟机、抑或是复制到 U 盘以引导真实物理机，然后从 U 盘复制到磁盘以引导系统。该镜像具有标准化的布局和描述其用途的元数据。mkosi 依赖其他工具来完成大部分工作：使用 systemd-repart 在磁盘镜像上创建分区，使用 mkfs.btrfs / mkfs.ext4 / mkfs.xfs 等创建文件系统，并使用 dnf / apt / pacman / zypper 下载和解压包。mkosi 支持一系列发行版：Debian 和 Ubuntu、Arch Linux、openSUSE，当然还包括 Fedora、CentOS Stream 及其衍生版本，以及最近的 RHEL UBI 和 RHEL 。由于实际的“重活”是由其他工具完成的，mkosi 可以进行交叉构建。这意味着可以使用一个发行版构建各种其他发行版的镜像。唯一的要求是主机上安装了相应的工具。Fedora 有原生的 apt、pacman 和 zypper，因此它为使用 mkosi 构建任何其他发行版提供了良好的基础。它还有一些有趣的功能：镜像可以由非特权用户创建，或者在没有设备文件的容器中创建，特别是没有对回环设备的访问权限。它还可以在没有特权的情况下将这些镜像启动为虚拟机（使用 qemu）。配置是声明性的，非常容易创建。使用 systemd-repart 创建磁盘分区，并使用 repart.d 配置文件定义应该如何完成此操作。有关更多详细信息，请参见 Daan DeMeyer 在 All Systems Go 大会上的两个演讲：《systemd-repart: Building Discoverable Disk Images》 和 《mkosi: Building Bespoke Operating System Images》。项目目标mkosi 的一个目标是允许对软件项目进行针对不同发行版的测试。它将为一个发行版创建一个镜像（使用该发行版的软件包），然后将软件项目编译并安装到该镜像中，插入不属于软件包的额外文件。但是，首个阶段，即从软件包创建镜像的过程，本身就是有用的。这是我们将首先展示的内容。我们 [1] 最近添加了对 RHEL 和 RHEL UBI 的支持。让我们从 RHEL UBI 开始，利用发行版软件包创建镜像。请注意，下面的示例要求 mkosi 19，而且不适用于更早的版本。带有 Shell 的基本 RHEL UBI 镜像$ mkdir -p mkosi.cache $ mkosi \ -d rhel-ubi \ -t directory \ -p bash,coreutils,util-linux,systemd,rpm \ --autologin 上面的命令指定了发行版 rhel-ubi，输出格式 directory，并请求安装软件包 bash、coreutils、…、rpm。rpm 通常不需要放到镜像内部，但在这里用于内省会很有用。我们还启用了以 root 用户自动登录。在启动构建之前，我们创建了缓存目录 mkosi.cache。当存在缓存目录时，mkosi 会自动使用它来持久化下载的 RPM 包。这将使相同软件包集合的后续调用速度更快。然后，我们可以使用 systemd-nspawn 将此镜像作为容器启动：$ sudo mkosi \ -d rhel-ubi \ -t directory \ boot systemd 252-14.el9_2.3 running in system mode (+PAM +AUDIT +SELINUX -APPARMOR +IMA +SMACK +SECCOMP +GCRYPT +GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS -FIDO2 +IDN2 -IDN -IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY +P11KIT -QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -BPF_FRAMEWORK +XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified) Detected virtualization systemd-nspawn. Detected architecture x86-64. Detected first boot. Red Hat Enterprise Linux 9.2 (Plow) ... [ OK ] Created slice Slice /system/getty. [ OK ] Created slice Slice /system/modprobe. [ OK ] Created slice User and Session Slice. ... [ OK ] Started User Login Management. [ OK ] Reached target Multi-User System. Red Hat Enterprise Linux 9.2 (Plow) Kernel 6.5.6-300.fc39.x86_64 on an x86_64 image login: root (automatic login) [root@image ~]# rpm -q rpm systemd rpm-4.16.1.3-22.el9.x86_64 systemd-252-14.el9_2.3.x86_64 正如前面提到的，此镜像可以用于启动虚拟机。但在此设置下，这是不可能的 —— 我们的镜像没有内核。事实上，RHEL UBI 根本不提供内核，因此我们无法使用它进行引导（无论是在虚拟机上还是在裸机上）。创建镜像我一开始说是要创建镜像，但到目前为止我们只有一个目录。让我们开始实际创建一个镜像：$ mkosi \ -d rhel-ubi \ -t disk \ -p bash,coreutils,util-linux,systemd,rpm \ --autologin 这将生成 image.raw，一个带有 GPT 分区表和单个根分区（用于本机架构）的磁盘镜像。$ sudo systemd-dissect image.raw Name: image.raw Size: 301.0M Sec. Size: 512 Arch.: x86-64 Image UUID: dcbd6499-409e-4b62-b251-e0dd15e446d5 OS Release: NAME=Red Hat Enterprise Linux VERSION=9.2 (Plow) ID=rhel ID_LIKE=fedora VERSION_ID=9.2 PLATFORM_ID=platform:el9 PRETTY_NAME=Red Hat Enterprise Linux 9.2 (Plow) ANSI_COLOR=0;31 LOGO=fedora-logo-icon CPE_NAME=cpe:/o:redhat:enterprise_linux:9::baseos HOME_URL=https://www.redhat.com/ DOCUMENTATION_URL=https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9 BUG_REPORT_URL=https://bugzilla.redhat.com/ REDHAT_BUGZILLA_PRODUCT=Red Hat Enterprise Linux 9 REDHAT_BUGZILLA_PRODUCT_VERSION=9.2 REDHAT_SUPPORT_PRODUCT=Red Hat Enterprise Linux REDHAT_SUPPORT_PRODUCT_VERSION=9.2 Use As: ✗ bootable system for UEFI ✓ bootable system for container ✗ portable service ✗ initrd ✗ sysext extension for system ✗ sysext extension for initrd ✗ sysext extension for portable service RW DESIGNATOR PARTITION UUID PARTITION LABEL FSTYPE ARCHITECTURE VERITY GROWFS NODE PARTNO rw root 1236e211-4729-4561-a6fc-9ef8f18b828f root-x86-64 xfs x86-64 no yes /dev/loop0p1 1 好的，我们现在有一个镜像，镜像中包含了一些来自 RHEL UBI 软件包的内容。我们如何在其上加点我们自己的东西呢？使用自己的文件扩展镜像有几种方法可以扩展镜像，包括从头开始编译某些东西。但在那之前，让我们做一些更简单的事情，将一个现成的文件系统注入到镜像中：$ mkdir -p mkosi.extra/srv/www/content $ cat >mkosi.extra/srv/www/content/index.html <<'EOF' Hello, World! EOF 现在，该镜像将包含 /srv/www/content/index.html。这种方法用于注入额外的配置或简单的程序。从源代码构建现在让我们过一遍完整流程，从源代码构建一些东西。例如，一个简单的 Meson 项目，有一个单独的 C 文件：$ cat >hello.c <<'EOF' #include <stdio.h> int main(int argc, char **argv) { char buf[1024]; FILE *f = fopen("/srv/www/content/index.html", "re"); size_t n = fread(buf, 1, sizeof buf, f); fwrite(buf, 1, n, stdout); fclose(f); return 0; } EOF $ cat >meson.build <<'EOF' project('hello', 'c') executable('hello', 'hello.c', install: true) EOF $ cat >mkosi.build <<'EOF' set -ex mkosi-as-caller rm -rf "$BUILDDIR/build" mkosi-as-caller meson setup "$BUILDDIR/build" "$SRCDIR" mkosi-as-caller meson compile -C "$BUILDDIR/build" meson install -C "$BUILDDIR/build" --no-rebuild EOF $ chmod +x mkosi.build 总结一下：我们有一些源代码（hello.c），一个构建系统配置文件（meson.build），以及一个由 mkosi 调用的胶水脚本（mkosi.build）。对于实际的项目，也会有相同的元素，只是更加复杂。这个脚本需要一些解释。mkosi 在创建镜像时使用用户命名空间。这允许包管理器（例如 dnf）安装由不同用户拥有的文件，即使它是由一个普通非特权用户调用的。我们使用 mkosi-as-caller 切换回调用者以进行编译。这样，在 $BUILDDIR 下编译期间创建的文件将由调用者拥有。现在让我们使用我们的程序构建镜像。与之前的调用相比，我们需要额外的软件包：meson、gcc。由于我们现在有了构建脚本，mkosi 将执行两个构建阶段：首先创建一个构建镜像，并在其中调用构建脚本，将安装产物存储在一个临时目录中，然后构建最终镜像，并将安装产物注入其中。（mkosi 设置 $DESTDIR，meson install 自动使用 $DESTDIR，因此并不需要我们明确指定。）$ mkosi \ -d rhel-ubi \ -t disk \ -p bash,coreutils,util-linux,systemd,rpm \ --autologin \ --build-package=meson,gcc \ --build-dir=mkosi.builddir \ --build-script=mkosi.build \ -f 此时，我们有了带有自定义载荷的镜像 image.raw。我们可以启动我们新创建的可执行文件作为 shell 命令：$ sudo mkosi -d rhel-ubi -t directory shell hello Hello, World! 获取 RHEL 的开发者订阅RHEL UBI 主要用作容器构建的基础层。它提供了有限的软件包（约 1500 个）。现在让我们切换到完整的 RHEL 安装。获取 RHEL 的最简单方法是使用 开发者许可证。它提供了权限注册 16 个运行 RHEL 的物理或虚拟节点，并提供自助式支持。首先，创建一个账户。然后，转到 管理页面 并确保启用了“用于 Red Hat 订阅管理的简化内容访问”。接下来，创建一个新的激活密钥，选择 “Red Hat 个人开发者订阅”。记下显示的组织 ID。在下面，我们将使用密钥名称和组织 ID 分别表示为 $KEY_NAME 和 $ORGANIZATION_ID。现在，我们准备使用 RHEL 内容：$ sudo dnf install subscription-manager $ sudo subscription-manager register \ --org $ORGANIZATION_ID --activationkey $KEY_NAME 使用 RHEL 构建镜像在之前的示例中，我们通过参数开关指定了所有配置。这对于快速开发很友好，但可能在情况复杂时变得难以处理。RHEL 是一个严肃的发行版，所以让我们改为使用配置文件：$ cat >mkosi.conf <<'EOF' [Output] Format=directory Output=rhel-directory [Distribution] Distribution=rhel [Content] Packages= bash coreutils util-linux systemd systemd-boot systemd-udev kernel-core Bootable=yes Bootloader=uki Autologin=yes WithDocs=no EOF 首先，让我们检查一下一切是否正常：$ mkosi summary 现在让我们构建镜像（呃，或者说，目录）：$ mkosi build $ mkosi qemu Welcome to Red Hat Enterprise Linux 9.2 (Plow)! [ OK ] Created slice Slice /system/modprobe. [ OK ] Reached target Initrd Root Device. [ OK ] Reached target Initrd /usr File System. [ OK ] Reached target Local Integrity Protected Volumes. [ OK ] Reached target Local File Systems. [ OK ] Reached target Path Units. [ OK ] Reached target Remote Encrypted Volumes. [ OK ] Reached target Remote Verity Protected Volumes. [ OK ] Reached target Slice Units. [ OK ] Reached target Swaps. ... [ OK ] Listening on Journal Socket. [ OK ] Listening on udev Control Socket. [ OK ] Listening on udev Kernel Socket. ... Red Hat Enterprise Linux 9.2 (Plow) Kernel 5.14.0-284.30.1.el9_2.x86_64 on an x86_64 localhost login: root (automatic login) [root@localhost ~]# 很好，我们将“镜像”构建为一个带有文件系统树的目录，并在虚拟机中引导了它。在引导的虚拟机中，findmnt / 显示根文件系统是 virtiofs。这是一个虚拟文件系统，将主机的目录暴露给客户机。我们其实也可以构建一个更传统的镜像，其中包含文件系统和文件内的分区表，但“目录 + virtiofs” 对于开发来说更快且更友好。我们刚刚引导的镜像未注册。要允许从镜像“内部”下载更新，我们必须将 yum 、subscription-manager 和 NetworkManager 添加到软件包列表，并在下载任何更新之前以与上述相同的方式调用 subscription-manager。在这之后，我们在基本仓库中就有大约 4500 个软件包可用，并且还有一些包含更多专业软件包的额外仓库。最后今天就是这些了。如果您有问题，可以在 Matrix 上找到我们，地址为 #mkosi:matrix.org，或者在 systemd 邮件列表 上找到我们。（题图：MJ/a6e60316-ed03-4e23-b8b4-22332a0f5bfa）Daan DeMeyer、Lukáš Nykrýn、Michal Sekletár、Zbigiew Jędrzejewski-Szmek ↩︎via: https://fedoramagazine.org/create-images-directly-from-rhel-and-rhel-ubi-package-using-mkosi/作者：Zbigniew Jędrzejewski-Szmek 选题：lujun9972 译者：GlassFoxowo 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
使用 Watchtower 自动更新 Docker 容器基础镜像对开发运维人员来说，保持 Docker 容器为最新版本是重要工作之一。手动更新 Docker 容器是一项耗时的工作。这篇文章解释了 Watchtower 是什么，如何安装它，以及在 Linux 中如何 使用 Watchtower 自动更新正在运行的 Docker 容器 。Watchtower 是什么？Watchtower 是一款自由开源的应用，用来监控运行中的 Docker 容器，并且当它发现基础镜像被更改后，可以自动的更新容器。若 Watchtower 发现一个运行中的容器需要更新，它会以发送 SIGTERM 信号的方式，优雅的结束运行中容器的运行。它会下载新镜像，然后以最初部署时使用的方式，重启容器。所有文件会在后台自动下载，因此不需要用户的介入。在这份指南中，我们将会明白如何在类 Unix 系统中使用 Watchtower 自动更新正在运行的 Docker 容器。我已经在 CentOS 和 Ubuntu 中测试了这份指南，所有的 Linux 发行版中操作过程都一样。在 Linux 中安装 Watchtower可以通过 Docker 镜像的方式下载 Watchtower 。因此，部署它是小事一桩。在你的 Linux 中安装 Docker 镜像，然后运行 Watchtower 立即开始监控 Docker 容器。参考下方指导在基于 PRM 和 DEB 包管理系统中安装 Docker如何在 CentOS 中安装 Docker如何在 Ubuntu 中安装 Docker适用于 Linux 的 Docker 桌面初学者手册安装 Docker 后，你可以使用以下命令以 root 用户身份部署 Watchtower 容器：# docker run -d --name watchtower -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower 如果你已经安装了 Docker 桌面版，以普通用户运行 Watchtower 容器。$ docker run -d --name watchtower -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower 该命令会拉取最新版的 watchtower 镜像，并运行 watchtower 容器。输出样例：Unable to find image 'containrrr/watchtower:latest' locally latest: Pulling from containrrr/watchtower 1045b2f97fda: Pull complete 35a104a262d3: Pull complete 1a0671483169: Pull complete Digest: sha256:bbf9794a691b59ed2ed3089fec53844f14ada249ee5e372ff0e595b73f4e9ab3 Status: Downloaded newer image for containrrr/watchtower:latest 91c104ef0e9896e8cd5ff30d9f13e728dbfad66443830ec2ac85dde6d7d37564 使用 Watchtower 自动更新 Docker 容器在你的系统上，Watchtower 正在和其他容器一起运行。你可以使用一下命令查看运行中的 Docker 容器列表：$ docker ps 输出样例：CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 91c104ef0e98 containrrr/watchtower "/watchtower" 14 minutes ago Up 14 minutes 8080/tcp watchtower f90b462b0712 bitnami/wordpress-nginx:6 "/opt/bitnami/script…" 19 minutes ago Up 19 minutes 0.0.0.0:80->8080/tcp, 0.0.0.0:443->8443/tcp ostechnix-wordpress-1 正如上方输出所示，watchtower 容器正在和名为 ostechnix-wordpress-1 的容器一起运行。从现在开始，watchtower 会每隔几分钟会检查该容器。如果 Watchtower 发现该容器的基础镜像的任何变化，它会优雅的关闭 ostechnix-wordpress-1 容器，然后使用与最初启动它时使用的相同方式，启动新的镜像。类似的，它会自动地每隔几分钟检查所有的运行中容器，并自动更新它们。Watchtower 如何更新多连接的容器？在监视多连接容器时，Watchtower 十分智能。假设我们现在运行两个容器。$ docker ps 输出样例：CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 91c104ef0e98 containrrr/watchtower "/watchtower" 14 minutes ago Up 14 minutes 8080/tcp watchtower f90b462b0712 bitnami/wordpress-nginx:6 "/opt/bitnami/script…" 19 minutes ago Up 19 minutes 0.0.0.0:80->8080/tcp, 0.0.0.0:443->8443/tcp ostechnix-wordpress-1 a895f082438a bitnami/mariadb:10.6 "/opt/bitnami/script…" 20 minutes ago Up 19 minutes 3306/tcp ostechnix-mariadb-1 正如你看到的，我们正在运行 ostechnix-wordpress-1 和 ostechnix-mariadb-1 这两个容器。ostechnix-mariadb-1 容器链接到 ostechnix-wordpress-1 容器。如果 Watchtower 发现 ostechnix-wordpress-1 容器有个新版本，它会先关闭与之相连接的 ostechnix-mariadb-1 容器 ，然后才会关闭 ostechnix-wordpress-1 容器。更新 ostechnix-wordpress-1 容器后，Watchtower 会以正确的顺序，且与最初启动它们时使用的相同方式，重启这两个容器，以便应用程序正确恢复。在我们的例子中，首先启动的是 ostechnix-mariadb-1 容器，然后是 ostechnix-wordpress-1 容器，以确保连接能够继续运行。监控特定容器默认情况下，Watchtower 将监控在它所指向的 Docker 守护进程中运行的所有 Docker 容器。不过，你可以像下面这样，通过指定容器名称限制 Watchtower 监视特定的 Docker 容器。$ docker run -d --name watchtower -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower ostechnix-wordpress-1 在上方的例子中，watchtower 会忽略其他容器，只监视名为 ostechnix-wordpress-1 的容器更新情况。如果你不指定任何参数，Watchtower 会照常监视所有运行中的 Docker 容器。发送提示或许你想收到容器更新的通知。你可以通过电子邮件、Slack 、MSTeams 以及 Gotify 发送通知。下面这个例子展示了如何通过电子邮件发送通知。假设你已经设置了 SMTP 服务器。docker run -d \ --name watchtower \ -v /var/run/docker.sock:/var/run/docker.sock \ -e WATCHTOWER_NOTIFICATIONS=email \ -e WATCHTOWER_NOTIFICATION_EMAIL_FROM=fromaddress@gmail.com \ -e WATCHTOWER_NOTIFICATION_EMAIL_TO=toaddress@gmail.com \ -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.gmail.com \ -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587 \ -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=fromaddress@gmail.com \ -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=app_password \ -e WATCHTOWER_NOTIFICATION_EMAIL_DELAY=2 \ containrrr/watchtower 参考下方 Watchtower Github 仓库和 Watchtower 官方主页获取更多信息：资料Watchtower GitHubWatchtower 主页via: https://ostechnix.com/automatically-update-running-docker-containers/作者：sk 选题：lkxed 译者：Donkey 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
容器镜像包含一个打包的应用，以及它的依赖关系，还有它在启动时运行的进程信息。容器是当今 IT 运维的一个关键部分。 容器镜像 container image 包含了一个打包的应用，以及它的依赖关系，还有它在启动时运行的进程信息。你可以通过提供一组特殊格式的指令来创建容器镜像，可以是提交给 注册中心 Registry ，或者是作为 Dockerfile 保存。例如，这个 Dockerfile 为 PHP Web 应用创建了一个容器：FROM registry.access.redhat.com/ubi8/ubi:8.1 RUN yum --disableplugin=subscription-manager -y module enable php:7.3 \ && yum --disableplugin=subscription-manager -y install httpd php \ && yum --disableplugin=subscription-manager clean all ADD index.php /var/www/html RUN sed -i 's/Listen 80/Listen 8080/' /etc/httpd/conf/httpd.conf \ && sed -i 's/listen.acl_users = apache,nginx/listen.acl_users =/' /etc/php-fpm.d/www.conf \ && mkdir /run/php-fpm \ && chgrp -R 0 /var/log/httpd /var/run/httpd /run/php-fpm \ && chmod -R g=u /var/log/httpd /var/run/httpd /run/php-fpm EXPOSE 8080 USER 1001 CMD php-fpm & httpd -D FOREGROUND 这个文件中的每条指令都会在容器镜像中增加一个 层 layer 。每一层只增加与下面一层的区别，然后，所有这些堆叠在一起，形成一个只读的容器镜像。它是如何工作的？你需要知道一些关于容器镜像的事情，按照这个顺序理解这些概念很重要：联合文件系统写入时复制（COW）叠加文件系统快照器联合文件系统 联合文件系统 Union File System （UnionFS）内置于 Linux 内核中，它允许将一个文件系统的内容与另一个文件系统的内容合并，同时保持“物理”内容的分离。其结果是一个统一的文件系统，即使数据实际上是以分支形式组织。这里的想法是，如果你有多个镜像有一些相同的数据，不是让这些数据再次复制过来，而是通过使用一个叫做 层 layer 的东西来共享。每一层都是一个可以在多个容器中共享的文件系统，例如，httpd 基础层是 Apache 的官方镜像，可以在任何数量的容器中使用。想象一下，由于我们在所有的容器中使用相同的基础层，我们节省了多少磁盘空间。这些镜像层总是只读的，但是当我们用这个镜像创建一个新的容器时，我们会在它上面添加一个薄的可写层。这个可写层是你创建、修改、删除或进行每个容器所需的其他修改的地方。写时复制（COW）当你启动一个容器时，看起来好像这个容器有自己的整个文件系统。这意味着你在系统中运行的每个容器都需要自己的文件系统副本。这岂不是要占用大量的磁盘空间，而且还要花费大量的时间让容器启动？不是的，因为每个容器都不需要它自己的文件系统副本!容器和镜像使用 写时复制 copy-on-write （COW）机制来实现这一点。写时复制策略不是复制文件，而是将同一个数据实例分享给多个进程，并且只在一个进程需要修改或写入数据时进行复制。所有其他进程将继续使用原始数据。Docker 对镜像和容器都使用了写时复制的机制。为了做到这一点，在旧版本中，镜像和运行中的容器之间的变化是通过 图驱动 graph driver 来跟踪的，现在则是通过 快照器 snapshotter 来跟踪。在运行中的容器中执行任何写操作之前，要修改的文件的副本被放在容器的可写层上。这就是发生 写 的地方。现在你知道为什么它被称为“写时复制”了么。这种策略既优化了镜像磁盘空间的使用，也优化了容器启动时间的性能，并与 UnionFS 一起工作。叠加文件系统 叠加文件系统 Overlay File System 位于现有文件系统的顶部，结合了上层和下层的目录树，并将它们作为一个单一的目录来呈现。这些目录被称为 层 layer 。下层保持不被修改。每一层只增加与下一层的差异（计算机术语为 “diff”），这种统一的过程被称为 联合挂载 union mount 。最低的目录或镜像层被称为 下层目录 lowerdir ，上面的目录被称为 上层目录 upperdir 。最后的覆盖层或统一层被称为 合并层 merged 。常见的术语包括这些层的定义： 基础层 Base layer ：是你的文件系统的文件所在的地方。就容器镜像而言，这个层就是你的基础镜像。 叠加层 Overlay layer ：通常被称为 容器层 container layer ，因为对运行中的容器所做的所有改变，如添加、删除或修改文件，都会写到这个可写层。对这一层所做的所有修改都存储在下一层，是基础层和差异层的联合视图。 差异层 Diff layer 包含了在叠加层所作的所有修改。如果你写的东西已经在基础层了，那么叠加文件系统就会把文件复制到差异层，并做出你想写的修改。这被称为写时复制。快照器通过使用层和图驱动，容器可以将其更改作为其容器文件系统的一部分来构建、管理和分发。但是使用 图驱动 graph driver 的工作真的很复杂，而且容易出错。 快照器 SnapShotter 与图驱动不同，因为它们不用了解镜像或容器。快照器的工作方式与 Git 非常相似，比如有树的概念，并跟踪每次提交对树的改变。一个 快照 snapshot 代表一个文件系统状态。快照有父子关系，使用一组目录。可以在父级和其快照之间进行差异比较（diff），以创建一个层。快照器提供了一个用于分配、快照和挂载抽象的分层文件系统的 API。总结你现在对什么是容器镜像以及它们的分层方法如何使容器可移植有了很好的认识。接下来，我将介绍容器的运行机制和内部结构。本文基于 techbeatly 的文章，经许可后改编。via: https://opensource.com/article/21/8/container-image作者：Nived V 选题：lujun9972 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
这要从一次咨询的失误说起：政府组织 A 让政府组织 B 开发一个 Web 应用程序。政府机构 B 把部分工作外包给某个人。后来，项目的托管和维护被外包给一家私人公司 C。C 公司发现，之前外包的人（已经离开很久了）构建了一个自定义的 Docker 镜像，并将其成为系统构建的依赖项，但这个人没有提交原始的 Dockerfile。C 公司有合同义务管理这个 Docker 镜像，可是他们他们没有源代码。C 公司偶尔叫我进去做各种工作，所以处理一些关于这个神秘 Docker 镜像的事情就成了我的工作。幸运的是，Docker 镜像的格式比想象的透明多了。虽然还需要做一些侦查工作，但只要解剖一个镜像文件，就能发现很多东西。例如，这里有一个 Prettier 代码格式化 的镜像可供快速浏览。首先，让 Docker 守护进程 daemon 拉取镜像，然后将镜像提取到文件中：docker pull tmknom/prettier:2.0.5 docker save tmknom/prettier:2.0.5 > prettier.tar 是的，该文件只是一个典型 tarball 格式的归档文件：$ tar xvf prettier.tar 6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/ 6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/VERSION 6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/json 6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/layer.tar 88f38be28f05f38dba94ce0c1328ebe2b963b65848ab96594f8172a9c3b0f25b.json a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/ a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/VERSION a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/json a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/layer.tar d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/ d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/VERSION d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/json d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/layer.tar manifest.json repositories 如你所见，Docker 在命名时经常使用 哈希 hash 。我们看看 manifest.json。它是以难以阅读的压缩 JSON 写的，不过 JSON 瑞士军刀 jq 可以很好地打印 JSON：$ jq . manifest.json [ { "Config": "88f38be28f05f38dba94ce0c1328ebe2b963b65848ab96594f8172a9c3b0f25b.json", "RepoTags": [ "tmknom/prettier:2.0.5" ], "Layers": [ "a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/layer.tar", "d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/layer.tar", "6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/layer.tar" ] } ] 请注意，这三个 层 Layer 对应三个以哈希命名的目录。我们以后再看。现在，让我们看看 Config 键指向的 JSON 文件。它有点长，所以我只在这里转储第一部分：$ jq . 88f38be28f05f38dba94ce0c1328ebe2b963b65848ab96594f8172a9c3b0f25b.json | head -n 20 { "architecture": "amd64", "config": { "Hostname": "", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "Tty": false, "OpenStdin": false, "StdinOnce": false, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], "Cmd": [ "--help" ], "ArgsEscaped": true, "Image": "sha256:93e72874b338c1e0734025e1d8ebe259d4f16265dc2840f88c4c754e1c01ba0a", 最重要的是 history 列表，它列出了镜像中的每一层。Docker 镜像由这些层堆叠而成。Dockerfile 中几乎每条命令都会变成一个层，描述该命令对镜像所做的更改。如果你执行 RUN script.sh 命令创建了 really_big_file，然后用 RUN rm really_big_file 命令删除文件，Docker 镜像实际生成两层：一个包含 really_big_file，一个包含 .wh.really_big_file 记录来删除它。整个镜像文件大小不变。这就是为什么你会经常看到像 RUN script.sh && rm really_big_file 这样的 Dockerfile 命令链接在一起——它保障所有更改都合并到一层中。以下是该 Docker 镜像中记录的所有层。注意，大多数层不改变文件系统镜像，并且 empty_layer 标记为 true。以下只有三个层是非空的，与我们之前描述的相符。$ jq .history 88f38be28f05f38dba94ce0c1328ebe2b963b65848ab96594f8172a9c3b0f25b.json [ { "created": "2020-04-24T01:05:03.608058404Z", "created_by": "/bin/sh -c #(nop) ADD file:b91adb67b670d3a6ff9463e48b7def903ed516be66fc4282d22c53e41512be49 in / " }, { "created": "2020-04-24T01:05:03.92860976Z", "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", "empty_layer": true }, { "created": "2020-04-29T06:34:06.617130538Z", "created_by": "/bin/sh -c #(nop) ARG BUILD_DATE", "empty_layer": true }, { "created": "2020-04-29T06:34:07.020521808Z", "created_by": "/bin/sh -c #(nop) ARG VCS_REF", "empty_layer": true }, { "created": "2020-04-29T06:34:07.36915054Z", "created_by": "/bin/sh -c #(nop) ARG VERSION", "empty_layer": true }, { "created": "2020-04-29T06:34:07.708820086Z", "created_by": "/bin/sh -c #(nop) ARG REPO_NAME", "empty_layer": true }, { "created": "2020-04-29T06:34:08.06429638Z", "created_by": "/bin/sh -c #(nop) LABEL org.label-schema.vendor=tmknom org.label-schema.name=tmknom/prettier org.label-schema.description=Prettier is an opinionated code formatter. org.label-schema.build-date=2020-04-29T06:34:01Z org .label-schema.version=2.0.5 org.label-schema.vcs-ref=35d2587 org.label-schema.vcs-url=https://github.com/tmknom/prettier org.label-schema.usage=https://github.com/tmknom/prettier/blob/master/README.md#usage org.label-schema.docker.cmd=do cker run --rm -v $PWD:/work tmknom/prettier --parser=markdown --write '**/*.md' org.label-schema.schema-version=1.0", "empty_layer": true }, { "created": "2020-04-29T06:34:08.511269907Z", "created_by": "/bin/sh -c #(nop) ARG NODEJS_VERSION=12.15.0-r1", "empty_layer": true }, { "created": "2020-04-29T06:34:08.775876657Z", "created_by": "/bin/sh -c #(nop) ARG PRETTIER_VERSION", "empty_layer": true }, { "created": "2020-04-29T06:34:26.399622951Z", "created_by": "|6 BUILD_DATE=2020-04-29T06:34:01Z NODEJS_VERSION=12.15.0-r1 PRETTIER_VERSION=2.0.5 REPO_NAME=tmknom/prettier VCS_REF=35d2587 VERSION=2.0.5 /bin/sh -c set -x && apk add --no-cache nodejs=${NODEJS_VERSION} nodejs-np m=${NODEJS_VERSION} && npm install -g prettier@${PRETTIER_VERSION} && npm cache clean --force && apk del nodejs-npm" }, { "created": "2020-04-29T06:34:26.764034848Z", "created_by": "/bin/sh -c #(nop) WORKDIR /work" }, { "created": "2020-04-29T06:34:27.092671047Z", "created_by": "/bin/sh -c #(nop) ENTRYPOINT [\"/usr/bin/prettier\"]", "empty_layer": true }, { "created": "2020-04-29T06:34:27.406606712Z", "created_by": "/bin/sh -c #(nop) CMD [\"--help\"]", "empty_layer": true } ] 太棒了！所有的命令都在 created_by 字段中，我们几乎可以用这些命令重建 Dockerfile。但不是完全可以。最上面的 ADD 命令实际上没有给我们需要添加的文件。COPY 命令也没有全部信息。我们还失去了 FROM 语句，因为它们扩展成了从基础 Docker 镜像继承的所有层。我们可以通过查看 时间戳 timestamp ，按 Dockerfile 对层进行分组。大多数层的时间戳相差不到一分钟，代表每一层构建所需的时间。但是前两层是 2020-04-24，其余的是 2020-04-29。这是因为前两层来自一个基础 Docker 镜像。理想情况下，我们可以找出一个 FROM 命令来获得这个镜像，这样我们就有了一个可维护的 Dockerfile。manifest.json 展示第一个非空层是 a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/layer.tar。让我们看看它：$ cd a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/ $ tar tf layer.tf | head bin/ bin/arch bin/ash bin/base64 bin/bbconfig bin/busybox bin/cat bin/chgrp bin/chmod bin/chown 看起来它可能是一个 操作系统 operating system 基础镜像，这也是你期望从典型 Dockerfile 中看到的。Tarball 中有 488 个条目，如果你浏览一下，就会发现一些有趣的条目：... dev/ etc/ etc/alpine-release etc/apk/ etc/apk/arch etc/apk/keys/ etc/apk/keys/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub etc/apk/keys/alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub etc/apk/keys/alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub etc/apk/protected_paths.d/ etc/apk/repositories etc/apk/world etc/conf.d/ ... 果不其然，这是一个 Alpine 镜像，如果你注意到其他层使用 apk 命令安装软件包，你可能已经猜到了。让我们解压 tarball 看看：$ mkdir files $ cd files $ tar xf ../layer.tar $ ls bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var $ cat etc/alpine-release 3.11.6 如果你拉取、解压 alpine:3.11.6，你会发现里面有一个非空层，layer.tar 与 Prettier 镜像基础层中的 layer.tar 是一样的。出于兴趣，另外两个非空层是什么？第二层是包含 Prettier 安装包的主层。它有 528 个条目，包含 Prettier、一堆依赖项和证书更新：... usr/lib/libuv.so.1 usr/lib/libuv.so.1.0.0 usr/lib/node_modules/ usr/lib/node_modules/prettier/ usr/lib/node_modules/prettier/LICENSE usr/lib/node_modules/prettier/README.md usr/lib/node_modules/prettier/bin-prettier.js usr/lib/node_modules/prettier/doc.js usr/lib/node_modules/prettier/index.js usr/lib/node_modules/prettier/package.json usr/lib/node_modules/prettier/parser-angular.js usr/lib/node_modules/prettier/parser-babel.js usr/lib/node_modules/prettier/parser-flow.js usr/lib/node_modules/prettier/parser-glimmer.js usr/lib/node_modules/prettier/parser-graphql.js usr/lib/node_modules/prettier/parser-html.js usr/lib/node_modules/prettier/parser-markdown.js usr/lib/node_modules/prettier/parser-postcss.js usr/lib/node_modules/prettier/parser-typescript.js usr/lib/node_modules/prettier/parser-yaml.js usr/lib/node_modules/prettier/standalone.js usr/lib/node_modules/prettier/third-party.js usr/local/ usr/local/share/ usr/local/share/ca-certificates/ usr/sbin/ usr/sbin/update-ca-certificates usr/share/ usr/share/ca-certificates/ usr/share/ca-certificates/mozilla/ usr/share/ca-certificates/mozilla/ACCVRAIZ1.crt usr/share/ca-certificates/mozilla/AC_RAIZ_FNMT-RCM.crt usr/share/ca-certificates/mozilla/Actalis_Authentication_Root_CA.crt ... 第三层由 WORKDIR /work 命令创建，它只包含一个条目：$ tar tf 6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/layer.tar work/ 原始 Dockerfile 在 Prettier 的 git 仓库中。via: https://theartofmachinery.com/2021/03/18/reverse_engineering_a_docker_image.html作者：Simon Arneaud 选题：lujun9972 译者：DCOLIVERSUN 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
修改磁盘镜像来创建基于树莓派的家庭实验室
使用树莓派或其它单板机创建一个“家庭私有云”。构建一个家庭实验室可以是一个有趣的方式，可以让你学习的新概念和实验新技术时还能自娱自乐。得益于以 树莓派 为首的单板计算机（SBC）的流行，在舒适的家里就可以轻松构建一个多计算机实验室。比起试图在主流的云服务商建立的相同配置，创建一个“家庭私有云”以花更少的钱来体验到云原生技术，也是一个极好的方法。这篇文章阐述如何修改树莓派或其它的单板机的磁盘镜像，预配置主机的 SSH，并禁用首次启动时强制竞选交互配置的服务。这是一个让你的设备“即启动，即运行”的极好方法，类似于云端实例。之后，你可以使用自动化的流程通过 SSH 连接来进行更专业和更深入的配置。此外, 当向你的实验室添加更多的树莓派时，修改磁盘镜像可以来让你只需要将该镜像写到一个 SD 卡、放入树莓派中就可以了！解压缩和挂载镜像对于这个项目，你需要修改一个服务器磁盘镜像。在测试期间，我使用 Fedora Server 31 ARM。在你下载该磁盘镜像并验证其校验和之后，你需要将其解压缩并挂载其到宿主机的文件系统的某个位置上，以便你可以根据需要修改它。你可以使用 xz 命令通过 --decompress 参数来解压缩 Fedora 服务器镜像：xz --decompress Fedora-Server-armhfp-X-y.z-sda.raw.xz这会留下一个解压缩后的原始磁盘镜像（它会自动地替换 .xz 压缩文件）。这个原始磁盘镜像就像它听起来的那样：一个包含格式化后安装好的磁盘上的所有数据的文件。这包含分区信息、启动分区、root 分区以及其它分区。你需要挂载你打算在其中进行修改的分区，但是要做到这一点，你需要知道磁盘镜像中的分区起始位置和扇区大小，这样你才可以挂载该文件正确的扇区。幸运的是，你可以在一个磁盘镜像上使用 fdisk 命令，就像在实际磁盘上使用一样容易。使用 --list 或 -l 参数来查看分区的列表和其信息：# 使用 fdisk 来列出原始镜像文件的分区： $ fdisk -l Fedora-Server-armhfp-31-1.9-sda.raw Disk Fedora-Server-armhfp-X-y.z-sda.raw: 3.2 GiB, 3242196992 bytes, 6332416 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0xdaad9f57 Device Boot Start End Sectors Size Id Type Fedora-Server-armhfp-X-y.z-sda.raw1 8192 163839 155648 76M c W95 F Fedora-Server-armhfp-X-y.z-sda.raw2 * 163840 1163263 999424 488M 83 Linux Fedora-Server-armhfp-X-y.z-sda.raw3 1163264 6047743 4884480 2.3G 83 Linux你需要的所有信息都可在上面的输出中获得。第 3 行表示扇区大小（包括逻辑和物理的）：512 字节 / 512 字节。设备列表显示的是原始磁盘镜像中的分区。第一个，Fedora-Server-armhfp-X-y.z-sda.raw1 毫无疑问是引导程序分区，因为它是第一个，很小（仅仅 76MB），而且类型被标识为 c，即 W95 FAT32（LBA），这是一个从 SD 卡启动的 FAT32 分区。第二个分区也不是非常大，只有 488MB。这个分区是一个 Linux 原生类型分区（Id 83），它可能是包含内核和 initramfs 的 Linux 启动分区。第三个分区可能是你需要的东西：它有 2.3GB 大小，所以在它其中应该有发行版的主要部分，并且它是一个 Linux 原生分区类型，这也是在预料之中的。这个分区应该包含了你需要修改的分区和数据。第三个分区从扇区 1163264 开始（在 fdisk 的输出中被显示为 Start 列），所以你的挂载偏移量是 595591168，计算方法是将扇区大小（512）乘以起始扇区（1163264）（即 512 * 1163264）。这意味着你需要以偏移量 595591168 挂载该文件，才能挂载到正确位置。装备了这些信息，现在你可以将第三个分区挂载到你的家目录中了：$ mkdir ~/mnt $ sudo mount -o loop,offset=595591168 Fedora-Server-armhfp-X-y.z-sda.raw ~/mnt $ ls ~/mnt直接在磁盘镜像中作业在磁盘镜像被解压缩和被挂载到宿主机上的一个位置后，就可以修改镜像以符合你的需求。在我看来，对镜像进行更改的最简单的方法是使用 chroot 来将你会话的工作根目录更改为挂载镜像的工作根目录。不过，有点棘手。在你改变了根目录后，你的会话将使用新的根目录下的二进制文件。除非你是在一个 ARM 系统做这些所有的操作，否则解压缩后的磁盘镜像的架构将与你正在使用的宿主机系统不同。即使在 chroot 环境中，宿主机系统也无法使用一个不同架构的二进制文件。至少，不能在本机使用。幸运的是，这里有一个解决方案：qemu-user-static。来自 Debian Wiki 的说明:“[qemu-user-static] 提供了用户模式的仿真二进制文件，是静态构建的。在这个模式中，QEMU 可以在一个 CPU 上启动为另一个 CPU 编译的 Linux 进程 …… 如果安装了 binfmt-support 软件包，qemu-user-static 软件包会注册提供的仿真器可以处理的二进制文件格式，以便其能够直接运行其他架构的二进制文件。”这正是你需要在 chroot 环境中非本地架构中工作所需的。如果宿主机系统是 Fedora，使用 DNF 来安装 qemu-user-static 软件包，并重新启动 systemd-binfmt.service：# 使用 DNF 启用非本地的 arch chroot 环境，添加新的二进制文件格式信息 # 输出镜像了精简 $ dnf install qemu-user-static $ systemctl restart systemd-binfmt.service使用这种方法，你一个能够更改根目录到挂载的磁盘镜像，运行 uname 命令来验证一切都在正常：sudo chroot ~/mnt/ /usr/bin/uname -a -r Linux marvin 5.5.16-200.fc31.x86_64 #1 SMP Wed Apr 8 16:43:33 UTC 2020 armv7l armv7l armv7l GNU/Linux在 chroot 环境中运行 uname 将在输出中显示 armv7l，这个原始磁盘镜像的架构， 而不是宿主机的架构。一切如预期，可以继续修改镜像了。修改磁盘镜像现在你可以直接切换到这个基于 ARM 的磁盘镜像中，并在该环境中工作了，你可以对镜像自身镜像修改了。你需要设置该镜像，以便它能够启动并可立即访问，而不需要在树莓派上做任何额外的设置。为此，你需要安装并启用 sshd（OpenSSH 守护进程），并为 SSH 访问添加授权密码。为了使其表现得更像一个云环境，实现在家里建立私有云的梦想，添加一个本地用户，给予该用户 sudo 权限，并（为了像云端的重度用户一样）允许该用户无需密码就可以使用 sudo。所以，你将做的事情是：安装并启用 SSHD（SSHD 已经在 Fedora ARM 镜像中安装并启用，但是你可能需要为你发行版手动执行这些工作）设置一个本地用户允许本地用户来使用 sudo（无需密码，可选）添加授权密钥允许 root 使用授权密码镜像 SSH（可选）我使用 GitHub 功能，它允许你上传你的 SSH 公钥，并在 https://github.com/.keys 处可访问。我发现这是一种很方便的分发公钥的方法，不过我生性多疑，我总是检查下载的密钥是否与我预期的匹配。如果你不想使用这种方法，你可以从你宿主机中复制你公钥到 chroot 环境中，或者你可以将公钥托管在你控制的 Web 服务器上以便使用相同的工作流。要开始修改磁盘镜像，再次切换根目录到挂载的磁盘镜像中，这次启动一个 shell，以便可以运行多个命令：# 为了简洁起见，省略了这些命令的输出(如果有的话) $ sudo chroot ~/mnt /bin/bash # 安装 openssh-server ，并启用它 (在 Fedora 上已经完成) $ dnf install -y openssh-server $ systemctl enable sshd.service # 允许 root 使用授权密码访问 SSH $ mkdir /root/.ssh # 下载或者另外添加授权密码文件，你的公共密码 # 将 URL 替换为你自己公共密码的路径 $ curl <https://github.com/clcollins.keys> -o /root/.ssh/authorized_keys $ chmod 700 /root/.ssh $ chmod 600 /root/.ssh/authorized_keys # 添加一个本地用户，并放置他们到 wheel 组中 # 将组和用户更改为您想要的一切 useradd -g chris -G wheel -m -u 1000 chris # 下载并添加你的授权密码 # 像你上面所做的那样更改 home 目录和URL mkdir /home/chris/.ssh curl <https://github.com/clcollins.keys> -o /home/chris/.ssh/authorized_keys chmod 700 /home/chris/.ssh chmod 600 /home/chris/.ssh/authorized_keys chown -R chris.chris /home/chris/.ssh/ # 允许 wheel 组( 使用你的本地用户) 不需要使用密码来使用 suso echo "%wheel ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/91-wheel-nopasswd这就是树莓派或其它单板机在首次启动时需要完成设置 SSH 的全部工作。不过，每个发行版都有自己的特点。例如，Raspbian 已经包含一个本地用户：pi，并且不使用 wheel 组。因此对于 Raspbian 来说，最好使用现有用户，或者删除 pi 用户，并使用另一个用户来替换它。在 Fedora ARM 的情况下，镜像会在首次引导启动时提示你完成设置。这会破坏你在上面所做的修改的目的，尤其是在设置完成之前，它会完全阻止启动。你的目标是使树莓派的功能类似于私有云的基础设施的一部分一样运行，而这个工作流程包括在主机启动时通过 SSH 远程设置主机。 禁用初始化设置，它由 initial-setup.service 控制：# 对多用户和图形目标禁用 initial-setup.service unlink /etc/systemd/system/multi-user.target.wants/initial-setup.service unlink /etc/systemd/system/graphical.target.wants/initial-setup.service当你在 chroot 环境时，你可以对你系统做任何你想做的其它更改，或者就放在那里，在第一次启动后，按照云原生的工作流通过 SSH 进行配置。重新压缩并安装修改后的镜像完成了这些更改后，剩下的就是重新压缩磁盘镜像，并将其安装其到你的树莓派的 SD 卡上。确保退出 chroot 环境，然后卸载磁盘镜像：$ sudo umount ~/mnt/就像最初解压缩镜像一样，你可以再次使用 xz 命令来压缩镜像。通过使用 --keep 参数，xz 将保留原始的镜像，而不是清理掉它。虽然这会占用更多的磁盘空间，但保留下来的未压缩镜像将允许你对正在处理的镜像进行增量更改，而不需要每次都对其进行解压缩。这对于在测试和调整镜像时节省时间是非常好的。# 压缩压缩磁盘镜像为一个 .xz 文件，但保留原始磁盘镜像 xz --compress Fedora-Server-armhfp-31-1.9-sda.raw --keep压缩过程将花费一些时间，所以趁着这个时间站起来，舒展身体，让你的血液再次流动。在压缩完成后，可以将新的、已修改过的磁盘镜像复制到 SD 卡上，以便与树莓派一起使用。标准的 dd 方法将镜像放置到 SD 卡上也很好用，但是我喜欢使用 Fedora 的 arm-image-installer，因为它因为它在处理未经编辑的镜像时提供了一些选项。它对编辑过的镜像也很好用，并且比 dd 命令更友好一些。确保检查 SD 卡在哪个磁盘驱动器上，并用 --media 参数使用它：# 使用 arm-image-installer 来复制已修改的磁盘镜像到 SD 卡上 arm-image-installer --image=Fedora-Server-armhfp-X-y.z-sda.raw.xz --target=rpi3 --media=/dev/sdc --norootpass --resizefs -y现在，你已经为树莓派或其它单板机准备好了一个新的、已修改的 Fedora Server ARM 镜像，准备好启动并立即 SSH 到你的修改镜像中。这种方法也可以用来做其它的修改，并且你也可以使用其它发行版的原始磁盘镜像，如果你更喜欢它们，而不是 Fedora 的话。这是一个开始构建家庭实验室私有云的良好基础。在以后的文章中，我将指导你使用云技术和自动化建立一个家庭实验室。延伸阅读为了学习如何做这篇文章中的事情，我做了很多研究。以下是我找到的两个对学习如何定制磁盘映像和使用非原生架构最有帮助的资料。它们对我从“不知道自己在做什么”到“我能够完成它！”非常有帮助。如何修改你的自定义 Linux 发行版的原始磁盘镜像使用 DNF 维基via: https://opensource.com/article/20/5/disk-image-raspberry-pi作者：Chris Collins 选题：lujun9972 译者：robsean 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
随着容器和容器技术的兴起，现在所有主流的 Linux 发行版都提供了容器基础镜像。本文介绍了 Fedora 项目如何构建其基本镜像，同时还展示了如何使用它来创建分层图像。基础和分层镜像在看如何构建 Fedora 容器 基础镜像 base image 之前，让我们定义基础镜像和 分层镜像 layered image 。定义基础镜像的简单方法是没有父镜像层的镜像。但这具体意味着什么呢？这意味着基础镜像通常只包含操作系统的根文件系统基础镜像（rootfs）。基础镜像通常提供安装软件以创建分层镜像所需的工具。分层镜像在基础镜像上添加了一组层，以便安装、配置和运行应用。分层镜像在 Dockerfile 中使用 FROM 指令引用基础镜像：FROM fedora:latest如何构建基础镜像Fedora 有一整套用于构建容器镜像的工具。其中包括 podman，它不需要以 root 身份运行。构建 rootfs基础镜像主要由一个 tarball) 构成。这个 tarball 包含一个 rootfs。有不同的方法来构建此 rootfs。Fedora 项目使用 kickstart) 安装方式以及 imagefactory 来创建这些 tarball。在创建 Fedora 基础镜像期间使用的 kickstart 文件可以在 Fedora 的构建系统 Koji 中找到。Fedora-Container-Base 包重新组合了所有基础镜像的构建版本。如果选择了一个构建版本，那么可以访问所有相关文件，包括 kickstart 文件。查看 示例，文件末尾的 %packages 部分定义了要安装的所有软件包。这就是让软件放在基础镜像中的方法。使用 rootfs 构建基础镜像rootfs 完成后，构建基础镜像就很容易了。它只需要一个包含以下指令的 Dockerfile：FROM scratch ADD layer.tar / CMD ["/bin/bash"]这里的重要部分是 FROM scratch 指令，它会创建一个空镜像。然后，接下来的指令将 rootfs 添加到镜像，并设置在运行镜像时要执行的默认命令。让我们使用 Koji 内置的 Fedora rootfs 构建一个基础镜像：$ curl -o fedora-rootfs.tar.xz https://kojipkgs.fedoraproject.org/packages/Fedora-Container-Base/Rawhide/20190902.n.0/images/Fedora-Container-Base-Rawhide-20190902.n.0.x86_64.tar.xz $ tar -xJvf fedora-rootfs.tar.xz 51c14619f9dfd8bf109ab021b3113ac598aec88870219ff457ba07bc29f5e6a2/layer.tar $ mv 51c14619f9dfd8bf109ab021b3113ac598aec88870219ff457ba07bc29f5e6a2/layer.tar layer.tar $ printf "FROM scratch\nADD layer.tar /\nCMD [\"/bin/bash\"]" > Dockerfile $ podman build -t my-fedora . $ podman run -it --rm my-fedora cat /etc/os-release需要从下载的存档中提取包含 rootfs 的 layer.tar 文件。这在 Fedora 生成的镜像已经可以被容器运行时使用才需要。因此，使用 Fedora 生成的镜像，获得基础镜像会更容易。让我们看看它是如何工作的：$ curl -O https://kojipkgs.fedoraproject.org/packages/Fedora-Container-Base/Rawhide/20190902.n.0/images/Fedora-Container-Base-Rawhide-20190902.n.0.x86_64.tar.xz $ podman load --input Fedora-Container-Base-Rawhide-20190902.n.0.x86_64.tar.xz $ podman run -it --rm localhost/fedora-container-base-rawhide-20190902.n.0.x86_64:latest cat /etc/os-release构建分层镜像要构建使用 Fedora 基础镜像的分层镜像，只需在 FROM 行指令中指定 fedora：FROM fedora:latestlatest 标记引用了最新的 Fedora 版本（编写本文时是 Fedora 30）。但是可以使用镜像的标签来使用其他版本。例如，FROM fedora:31 将使用 Fedora 31 基础镜像。Fedora 支持将软件作为容器来构建并发布。这意味着你可以维护 Dockerfile 来使其他人可以使用你的软件。关于在 Fedora 中成为容器镜像维护者的更多信息，请查看 Fedora 容器指南。via: https://fedoramagazine.org/how-to-build-fedora-container-images/作者：Clément Verna 选题：lujun9972 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
树莓派不需要过多介绍。它是一款小巧、价格实惠，只有信用卡大小的电脑，它可以连接到显示器或电视。我们可以连接一个标准的键盘和鼠标，并将其用作一台成熟的台式计算机来完成日常任务，如互联网浏览、播放视频/玩游戏、文字处理和电子表格制作等。它主要是为学校的计算机科学教学而开发的。如今，树莓派被广泛用于大学、中小型组织和研究所来教授编码。如果你有一台树莓派，你可能需要了解一个名为 PiShrink 的 bash 脚本，该脚本可使树莓派镜像更小。PiShrink 将自动缩小镜像，然后在启动时将其调整为 SD 卡的最大大小。这能更快地将镜像复制到 SD 卡中，同时缩小的镜像将更好地压缩。这对于将大容量镜像放入 SD 卡非常有用。在这个简短的指南中，我们将学习如何在类 Unix 系统中将树莓派镜像缩小到更小。安装 PiShrink要在 Linux 机器上安装 PiShrink，请先使用以下命令下载最新版本：$ wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh接下来，将下载的 PiShrink 变成二进制可执行文件：$ chmod +x pishrink.sh最后，移动到目录：$ sudo mv pishrink.sh /usr/local/bin/使树莓派镜像更小你可能已经知道，Raspbian 是所有树莓派型号的官方操作系统。树莓派基金会为 PC 和 Mac 开发了树莓派桌面版本。你可以创建一个 live CD，并在虚拟机中运行它，甚至也可以将其安装在桌面上。树莓派也有少量非官方​​操作系统镜像。为了测试，我从官方下载页面下载了官方的 Raspbian 系统。解压下载的系统镜像：$ unzip 2019-04-08-raspbian-stretch-lite.zip上面的命令将提取当前目录中 2019-04-08-raspbian-stretch-lite.zip 文件的内容。让我们看下提取文件的实际大小：$ du -h 2019-04-08-raspbian-stretch-lite.img 1.7G 2019-04-08-raspbian-stretch-lite.img如你所见，提取的树莓派系统镜像大小为 1.7G。现在，使用 PiShrink 缩小此文件的大小，如下所示：$ sudo pishrink.sh 2019-04-08-raspbian-stretch-lite.img示例输出：Creating new /etc/rc.local rootfs: 39795/107072 files (0.1% non-contiguous), 239386/428032 blocks resize2fs 1.45.0 (6-Mar-2019) resize2fs 1.45.0 (6-Mar-2019) Resizing the filesystem on /dev/loop1 to 280763 (4k) blocks. Begin pass 3 (max = 14) Scanning inode table XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX Begin pass 4 (max = 3728) Updating inode references XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX The filesystem on /dev/loop1 is now 280763 (4k) blocks long. Shrunk 2019-04-08-raspbian-stretch-lite.img from 1.7G to 1.2G正如你在上面的输出中看到的，树莓派镜像的大小已减少到 1.2G。你还可以使用 -s 标志跳过该过程的自动扩展部分。$ sudo pishrink.sh -s 2019-04-08-raspbian-stretch-lite.img newpi.img这将创建一个源镜像文件（即 2019-04-08-raspbian-stretch-lite.img）的副本到一个新镜像文件（newpi.img）并进行处理。有关更多详细信息，请查看最后给出的官方 GitHub 页面。就是这些了。希望本文有用。还有更多好东西，敬请期待！资源：PiShrink 的 GitHub 仓库树莓派网站via: https://www.ostechnix.com/pishrink-make-raspberry-pi-images-smaller/作者：sk 选题：lujun9972 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
清华大学开源软件镜像站推出一键使用服务，将来或支持 42 操作系统
清华大学开源软件镜像站 是国内著名的开源软件镜像站，在去年某个节日推出了“一键使用 TUNA”服务，并发布了一篇热情洋溢的公告，特此将此服务及公告分享给大家： XD# 号外：现推出“一键使用TUNA”服务2018-04-01 TUNA Staff随着 TUNA 协会的现代化建设进入新时代，镜像站面临的主要矛盾已经转化为用户日益增长的高速下载需要和各类软件不简单不直观的配置之间的矛盾。只有正确认识这一主要矛盾，才能确定正确的战略、策略、政策和办法，促进矛盾解决，推动 TUNA 协会进步。必须认识到，TUNA 主要矛盾的变化，是关系全局的历史性变化，对协会工作提出了许多新要求。在新任会长 Xavier Yao (a.k.a. Pero) 的带领下，TUNA 协会的同学们勇于担当，攻坚克难，开发出了适应时代潮流、贴合用户需要的新一代工具“一键使用 TUNA” 脚本，又名 oh-my-tuna 。它目前支持更改下列软件的配置，将其直接指向 TUNA 镜像站：AnacondaArch LinuxTeX Live (by tlmgr)Debian GNU/LinuxHomebrewPyPIUbuntu Linux您可以选择仅更改当前用户的配置，也可以更改全局配置。如果不满意，随时可以撤销这些更改。这一切，只需要一行命令：python oh-my-tuna.py ！项目托管在 TUNA 主页 上，您也可以访问 GitHub 阅读源码、获取更详细的说明，也欢迎各位对本项目作出贡献！在将来，我们的目标是将其支持的系统/软件范围进一步扩展到（包括且不限于）：TUNA 镜像的尽可能多的系统/软件Macrohard TM Windoge TM42TUNA 成员要紧密团结在会长周围，高举开源精神伟大旗帜，锐意进取，埋头苦干，为实现推进镜像站建设、完成 Linux 发行版统一、维护开源界和平与促进共同发展三大历史任务，为决胜全面建成中国最好的镜像站、夺取新时代开源精神伟大胜利、实现社区伟大复兴的开源梦、实现用户对高速下载的向往继续奋斗！
五种优化 Linux 容器大小和构建更小的镜像的方法。Docker 近几年的爆炸性发展让大家逐渐了解到容器和容器镜像的概念。尽管 Linux 容器技术在很早之前就已经出现，但这项技术近来的蓬勃发展却还是要归功于 Docker 对用户友好的命令行界面以及使用 Dockerfile 格式轻松构建镜像的方式。纵然 Docker 大大降低了入门容器技术的难度，但构建一个兼具功能强大、体积小巧的容器镜像的过程中，有很多技巧需要了解。第一步：清理不必要的文件这一步和在普通服务器上清理文件没有太大的区别，而且要清理得更加仔细。一个小体积的容器镜像在传输方面有很大的优势，同时，在磁盘上存储不必要的数据的多个副本也是对资源的一种浪费。因此，这些技术对于容器来说应该比有大量专用内存的服务器更加需要。清理容器镜像中的缓存文件可以有效缩小镜像体积。下面的对比是使用 dnf 安装 Nginx 构建的镜像，分别是清理和没有清理 yum 缓存文件的结果：# Dockerfile with cache FROM fedora:28 LABEL maintainer Chris Collins <collins.christopher@gmail.com> RUN dnf install -y nginx ----- # Dockerfile w/o cache FROM fedora:28 LABEL maintainer Chris Collins <collins.christopher@gmail.com> RUN dnf install -y nginx \ && dnf clean all \ && rm -rf /var/cache/yum ----- [chris@krang] $ docker build -t cache -f Dockerfile . [chris@krang] $ docker images --format "{{.Repository}}: {{.Size}}" | head -n 1 cache: 464 MB [chris@krang] $ docker build -t no-cache -f Dockerfile-wo-cache . [chris@krang] $ docker images --format "{{.Repository}}: {{.Size}}" | head -n 1 no-cache: 271 MB从上面的结果来看，清理缓存文件的效果相当显著。和清除了元数据和缓存文件的容器镜像相比，不清除的镜像体积接近前者的两倍。除此以外，包管理器缓存文件、Ruby gem 的临时文件、nodejs 缓存文件，甚至是下载的源码 tarball 最好都全部清理掉。层：一个潜在的隐患很不幸（当你往下读，你会发现这是不幸中的万幸），根据容器中的层的概念，不能简单地向 Dockerfile 中写一句 RUN rm -rf /var/cache/yum 就完事儿了。因为 Dockerfile 的每一条命令都以一个层的形式存储，并一层层地叠加。所以，如果你是这样写的：RUN dnf install -y nginx RUN dnf clean all RUN rm -rf /var/cache/yum你的容器镜像就会包含三层，而 RUN dnf install -y nginx 这一层仍然会保留着那些缓存文件，然后在另外两层中被移除。但缓存实际上仍然是存在的，当你把一个文件系统挂载在另外一个文件系统之上时，文件仍然在那里，只不过你见不到也访问不到它们而已。在上一节的示例中，你会看到正确的做法是将几条命令链接起来，在产生缓存文件的同一条 Dockerfile 指令里把缓存文件清理掉：RUN dnf install -y nginx \ && dnf clean all \ && rm -rf /var/cache/yum这样就把几条命令连成了一条命令，在最终的镜像中只占用一个层。这样只会浪费一点缓存的好处，稍微多耗费一点点构建容器镜像的时间，但被清理掉的缓存文件就不会留存在最终的镜像中了。作为一个折衷方法，只需要把一些相关的命令（例如 yum install 和 yum clean all、下载文件、解压文件、移除 tarball 等等）连接成一个命令，就可以在最终的容器镜像中节省出大量体积，你也能够利用 Docker 的缓存加快开发速度。层还有一个更隐蔽的特性。每一层都记录了文件的更改，这里的更改并不仅仅已有的文件累加起来，而是包括文件属性在内的所有更改。因此即使是对文件使用了 chmod 操作也会被在新的层创建文件的副本。下面是一次 docker images 命令的输出内容。其中容器镜像 layer_test_1 是在 CentOS 基础镜像中增加了一个 1GB 大小的文件后构建出来的镜像，而容器镜像 layer_test_2 是使用了 FROM layer_test_1 语句创建出来的，除了执行一条 chmod u+x 命令没有做任何改变。layer_test_2 latest e11b5e58e2fc 7 seconds ago 2.35 GB layer_test_1 latest 6eca792a4ebe 2 minutes ago 1.27 GB如你所见，layer_test_2 镜像比 layer_test_1 镜像大了 1GB 以上。尽管事实上 layer_test_1 只是 layer_test_2 的前一层，但隐藏在这第二层中有一个额外的 1GB 的文件。在构建容器镜像的过程中，如果在单独一层中进行移动、更改、删除文件，都会出现类似的结果。专用镜像和公用镜像有这么一个亲身经历：我们部门重度依赖于 Ruby on Rails，于是我们开始使用容器。一开始我们就建立了一个正式的 Ruby 的基础镜像供所有的团队使用，为了简单起见（以及在“这就是我们自己在服务器上瞎鼓捣的”想法的指导下），我们使用 rbenv 将 Ruby 最新的 4 个版本都安装到了这个镜像当中，目的是让开发人员只用这个单一的镜像就可以将使用不同版本 Ruby 的应用程序迁移到容器中。我们当时还认为这是一个虽然非常大但兼容性相当好的镜像，因为这个镜像可以同时满足各个团队的使用。实际上这是费力不讨好的。如果维护独立的、版本略微不同的镜像中，可以很轻松地实现镜像的自动化维护。同时，选择特定版本的特定镜像，还有助于在引入破坏性改变，在应用程序接近生命周期结束前提前做好预防措施，以免产生不可控的后果。庞大的公用镜像也会对资源造成浪费，当我们后来将这个庞大的镜像按照 Ruby 版本进行拆分之后，我们最终得到了共享一个基础镜像的多个镜像，如果它们都放在一个服务器上，会额外多占用一点空间，但是要比安装了多个版本的巨型镜像要小得多。这个例子也不是说构建一个灵活的镜像是没用的，但仅对于这个例子来说，从一个公共镜像创建根据用途而构建的镜像最终将节省存储资源和维护成本，而在受益于公共基础镜像的好处的同时，每个团队也能够根据需要来做定制化的配置。从零开始：将你需要的内容添加到空白镜像中有一些和 Dockerfile 一样易用的工具可以轻松创建非常小的兼容 Docker 的容器镜像，这些镜像甚至不需要包含一个完整的操作系统，就可以像标准的 Docker 基础镜像一样小。我曾经写过一篇关于 Buildah 的文章，我想在这里再一次推荐一下这个工具。因为它足够的灵活，可以使用宿主机上的工具来操作一个空白镜像并安装打包好的应用程序，而且这些工具不会被包含到镜像当中。Buildah 取代了 docker build 命令。可以使用 Buildah 将容器的文件系统挂载到宿主机上并进行交互。下面来使用 Buildah 实现上文中 Nginx 的例子（现在忽略了缓存的处理）：#!/usr/bin/env bash set -o errexit # Create a container container=$(buildah from scratch) # Mount the container filesystem mountpoint=$(buildah mount $container) # Install a basic filesystem and minimal set of packages, and nginx dnf install --installroot $mountpoint --releasever 28 glibc-minimal-langpack nginx --setopt install_weak_deps=false -y # Save the container to an image buildah commit --format docker $container nginx # Cleanup buildah unmount $container # Push the image to the Docker daemon’s storage buildah push nginx:latest docker-daemon:nginx:latest 你会发现这里使用的已经不再是 Dockerfile 了，而是普通的 Bash 脚本，而且是从框架（或空白）镜像开始构建的。上面这段 Bash 脚本将容器的根文件系统挂载到了宿主机上，然后使用宿主机的命令来安装应用程序，这样的话就不需要把软件包管理器放置到容器镜像中了。这样所有无关的内容（基础镜像之外的部分，例如 dnf）就不再会包含在镜像中了。在这个例子当中，构建出来的镜像大小只有 304 MB，比使用 Dockerfile 构建的镜像减少了 100 MB 以上。[chris@krang] $ docker images |grep nginx docker.io/nginx buildah 2505d3597457 4 minutes ago 304 MB注：这个镜像是使用上面的构建脚本构建的，镜像名称中前缀的 docker.io 只是在推送到镜像仓库时加上的。对于一个 300MB 级别的容器基础镜像来说，能缩小 100MB 已经是很显著的节省了。使用软件包管理器来安装 Nginx 会带来大量的依赖项，如果能够使用宿主机直接从源代码对应用程序进行编译然后构建到容器镜像中，节省出来的空间还可以更多，因为这个时候可以精细的选用必要的依赖项，非必要的依赖项一概不构建到镜像中。Tom Sweeney 有一篇文章《用 Buildah 构建更小的容器》，如果你想在这方面做深入的优化，不妨参考一下。通过 Buildah 可以构建一个不包含完整操作系统和代码编译工具的容器镜像，大幅缩减了容器镜像的体积。对于某些类型的镜像，我们可以进一步采用这种方式，创建一个只包含应用程序本身的镜像。使用静态链接的二进制文件来构建镜像按照这个思路，我们甚至可以更进一步舍弃容器内部的管理和构建工具。例如，如果我们足够专业，不需要在容器中进行排错调试，是不是可以不要 Bash 了？是不是可以不要 GNU 核心套件了？是不是可以不要 Linux 基础文件系统了？如果你使用的编译型语言支持静态链接库，将应用程序所需要的所有库和函数都编译成二进制文件，那么程序所需要的函数和库都可以复制和存储在二进制文件本身里面。这种做法在 Golang 社区中已经十分常见，下面我们使用由 Go 语言编写的应用程序进行展示：以下这个 Dockerfile 基于 golang:1.8 镜像构建一个小的 Hello World 应用程序镜像：FROM golang:1.8 ENV GOOS=linux ENV appdir=/go/src/gohelloworld COPY ./ /go/src/goHelloWorld WORKDIR /go/src/goHelloWorld RUN go get RUN go build -o /goHelloWorld -a CMD ["/goHelloWorld"]构建出来的镜像中包含了二进制文件、源代码以及基础镜像层，一共 716MB。但对于应用程序运行唯一必要的只有编译后的二进制文件，其余内容在镜像中都是多余的。如果在编译的时候通过指定参数 CGO_ENABLED=0 来禁用 cgo，就可以在编译二进制文件的时候忽略某些函数的 C 语言库：GOOS=linux CGO_ENABLED=0 go build -a goHelloWorld.go编译出来的二进制文件可以加到一个空白（或框架）镜像：FROM scratch COPY goHelloWorld / CMD ["/goHelloWorld"]来看一下两次构建的镜像对比：[ chris@krang ] $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE goHello scratch a5881650d6e9 13 seconds ago 1.55 MB goHello builder 980290a100db 14 seconds ago 716 MB从镜像体积来说简直是天差地别了。基于 golang:1.8 镜像构建出来带有 goHelloWorld 二进制的镜像（带有 builder 标签）体积是基于空白镜像构建的只包含该二进制文件的镜像的 460 倍！后者的整个镜像大小只有 1.55MB，也就是说，有 713MB 的数据都是非必要的。正如上面提到的，这种缩减镜像体积的方式在 Golang 社区非常流行，因此不乏这方面的文章。Kelsey Hightower 有一篇文章专门介绍了如何处理这些库的依赖关系。压缩镜像层除了前面几节中讲到的将多个命令链接成一个命令的技巧，还可以对镜像进行压缩。镜像压缩的实质是导出它，删除掉镜像构建过程中的所有中间层，然后保存镜像的当前状态为单个镜像层。这样可以进一步将镜像缩小到更小的体积。在 Docker 1.13 之前，压缩镜像层的的过程可能比较麻烦，需要用到 docker-squash 之类的工具来导出容器的内容并重新导入成一个单层的镜像。但 Docker 在 Docker 1.13 中引入了 --squash 参数，可以在构建过程中实现同样的功能：FROM fedora:28 LABEL maintainer Chris Collins <collins.christopher@gmail.com> RUN dnf install -y nginx RUN dnf clean all RUN rm -rf /var/cache/yum [chris@krang] $ docker build -t squash -f Dockerfile-squash --squash . [chris@krang] $ docker images --format "{{.Repository}}: {{.Size}}" | head -n 1 squash: 271 MB通过这种方式使用 Dockerfile 构建出来的镜像有 271MB 大小，和上面连接多条命令的方案构建出来的镜像体积一样，因此这个方案也是有效的，但也有一个潜在的问题，而且是另一种问题。“什么？还有另外的问题？”好吧，有点像以前一样的问题，以另一种方式引发了问题。过头了：过度压缩、太小太专用了容器镜像之间可以共享镜像层。基础镜像或许大小上有几 Mb，但它只需要拉取/存储一次，并且每个镜像都能复用它。所有共享基础镜像的实际镜像大小是基础镜像层加上每个特定改变的层的差异内容，因此，如果有数千个基于同一个基础镜像的容器镜像，其体积之和也有可能只比一个基础镜像大不了多少。因此，这就是过度使用压缩或专用镜像层的缺点。将不同镜像压缩成单个镜像层，各个容器镜像之间就没有可以共享的镜像层了，每个容器镜像都会占有单独的体积。如果你只需要维护少数几个容器镜像来运行很多容器，这个问题可以忽略不计；但如果你要维护的容器镜像很多，从长远来看，就会耗费大量的存储空间。回顾上面 Nginx 压缩的例子，我们能看出来这种情况并不是什么大的问题。在这个镜像中，有 Fedora 操作系统和 Nginx 应用程序，没有缓存，并且已经被压缩。但我们一般不会使用一个原始的 Nginx，而是会修改配置文件，以及引入其它代码或应用程序来配合 Nginx 使用，而要做到这些，Dockerfile 就变得更加复杂了。如果使用普通的镜像构建方式，构建出来的容器镜像就会带有 Fedora 操作系统的镜像层、一个安装了 Nginx 的镜像层（带或不带缓存）、为 Nginx 作自定义配置的其它多个镜像层，而如果有其它容器镜像需要用到 Fedora 或者 Nginx，就可以复用这个容器镜像的前两层。[ App 1 Layer ( 5 MB) ] [ App 2 Layer (6 MB) ] [ Nginx Layer ( 21 MB) ] ------------------^ [ Fedora Layer (249 MB) ] 如果使用压缩镜像层的构建方式，Fedora 操作系统会和 Nginx 以及其它配置内容都被压缩到同一层里面，如果有其它容器镜像需要使用到 Fedora，就必须重新引入 Fedora 基础镜像，这样每个容器镜像都会额外增加 249MB 的大小。[ Fedora + Nginx + App 1 (275 MB)] [ Fedora + Nginx + App 2 (276 MB) ] 当你构建了大量在功能上趋于分化的的小型容器镜像时，这个问题就会暴露出来了。就像生活中的每一件事一样，关键是要做到适度。根据镜像层的实现原理，如果一个容器镜像变得越小、越专用化，就越难和其它容器镜像共享基础的镜像层，这样反而带来不好的效果。对于仅在基础镜像上做微小变动构建出来的多个容器镜像，可以考虑共享基础镜像层。如上所述，一个镜像层本身会带有一定的体积，但只要存在于镜像仓库中，就可以被其它容器镜像复用。这种情况下，数千个镜像也许要比单个镜像占用更少的空间。[ specific app ] [ specific app 2 ] [ customizations ]--------------^ [ base layer ]一个容器镜像变得越小、越专用化，就越难和其它容器镜像共享基础的镜像层，最终会不必要地占用越来越多的存储空间。 [ specific app 1 ] [ specific app 2 ] [ specific app 3 ]总结减少处理容器镜像时所需的存储空间和带宽的方法有很多，其中最直接的方法就是减小容器镜像本身的大小。在使用容器的过程中，要经常留意容器镜像是否体积过大，根据不同的情况采用上述提到的清理缓存、压缩到一层、将二进制文件加入在空白镜像中等不同的方法，将容器镜像的体积缩减到一个有效的大小。via: https://opensource.com/article/18/7/building-container-images作者：Chris Collins 选题：lujun9972 译者：HankChow 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
或许你已经了解到 Docker 容器镜像是一个轻量、独立、含有运行某个应用所需全部软件的可执行包，这也是为什么容器镜像会经常被开发者用于构建和分发应用。假如你很好奇一个 Docker 镜像里面包含了什么东西，那么这篇简要的指南或许会帮助到你。今天，我们将学会使用一个名为 Dive 的工具来分析和探索 Docker 镜像每层的内容。通过分析 Docker 镜像，我们可以发现在各个层之间可能重复的文件并通过移除它们来减小 Docker 镜像的大小。Dive 工具不仅仅是一个 Docker 镜像分析工具，它还可以帮助我们来构建镜像。Dive 是一个用 Go 编程语言编写的自由开源工具。安装 Dive首先从该项目的 发布页 下载最新版本，然后像下面展示的那样根据你所使用的发行版来安装它。假如你正在使用 Debian 或者 Ubuntu，那么可以运行下面的命令来下载并安装它。$ wget https://github.com/wagoodman/dive/releases/download/v0.0.8/dive_0.0.8_linux_amd64.deb$ sudo apt install ./dive_0.0.8_linux_amd64.deb在 RHEL 或 CentOS 系统中$ wget https://github.com/wagoodman/dive/releases/download/v0.0.8/dive_0.0.8_linux_amd64.rpm$ sudo rpm -i dive_0.0.8_linux_amd64.rpmDive 也可以使用 Linuxbrew 包管理器来安装。$ brew tap wagoodman/dive$ brew install dive至于其他的安装方法，请参考 Dive 项目的 GitHub 网页。分析并探索 Docker 镜像的内容要分析一个 Docker 镜像，只需要运行加上 Docker 镜像 ID 的 dive 命令就可以了。你可以使用 sudo docker images 来得到 Docker 镜像的 ID。$ sudo dive ea4c82dcd15a上面命令中的 ea4c82dcd15a 是某个镜像的 ID。然后 dive 命令将快速地分析给定 Docker 镜像的内容并将它在终端中展示出来。正如你在上面的截图中看到的那样，在终端的左边一栏列出了给定 Docker 镜像的各个层及其详细内容，浪费的空间大小等信息。右边一栏则给出了给定 Docker 镜像每一层的内容。你可以使用 Ctrl+空格 来在左右栏之间切换，使用 UP/DOWN 光标键来在目录树中进行浏览。下面是 dive 的快捷键列表：Ctrl+空格 —— 在左右栏之间切换空格 —— 展开或收起目录树Ctrl+A —— 文件树视图：展示或隐藏增加的文件Ctrl+R —— 文件树视图：展示或隐藏被移除的文件Ctrl+M —— 文件树视图：展示或隐藏被修改的文件Ctrl+U —— 文件树视图：展示或隐藏未修改的文件Ctrl+L —— 层视图：展示当前层的变化Ctrl+A —— 层视图：展示总的变化Ctrl+/ —— 筛选文件Ctrl+C —— 退出在上面的例子中，我使用了 sudo 权限，这是因为我的 Docker 镜像存储在 /var/lib/docker/ 目录中。假如你的镜像保存在你的家目录 （$HOME）或者在其他不属于 root 用户的目录，你就没有必要使用 sudo 命令。你还可以使用下面的单个命令来构建一个 Docker 镜像并立刻分析该镜像：$ dive build -t <some-tag>Dive 工具仍处于 beta 阶段，所以可能会存在 bug。假如你遇到了 bug，请在该项目的 GitHub 主页上进行报告。好了，这就是今天的全部内容。现在你知道如何使用 Dive 工具来探索和分析 Docker 容器镜像的内容以及利用它构建镜像。希望本文对你有所帮助。更多精彩内容即将呈现，请保持关注！干杯！via: https://www.ostechnix.com/how-to-analyze-and-explore-the-contents-of-docker-images/作者：SK 选题：lujun9972 译者：FSSlc 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出