Quadlet:在 systemd 下运行 Podman 容器

2024-01-02, 更新于: 2024-02-19 标签: #container,#linux,#selfhosting 阅读时间: ~11分钟 Quadlet 允许你将你的 Podman 容器作为 systemd 服务运行。这对于在后台运行容器,以及在服务器重启后自动启动它们特别有用。

在 systemd 下运行 Podman 容器并不是什么新鲜事。实际上,Podman 长期以来都支持使用 podman generate systemd 命令来实现这一点。但是现在这个命令会显示一个弃用警告,提示迁移到 Quadlet。

有好几个月,我都懒得在我的家庭服务器上进行迁移。为什么要动一个已经可以正常运行的系统呢?但是现在我终于有时间来做这件事了,我真的很喜欢 Quadlet!我认为 Podman 终于有了一个 Docker Compose 的替代方案,它甚至更加灵活和强大!

在这篇博文中,我将解释如何使用 Quadlet 与 rootless Podman,并从旧的 podman generate systemd 方法进行迁移。

注意: 如果你想知道为什么需要 systemd:当没有守护进程时(因为 Podman 是 daemonless 的),需要一些东西来启动容器。

如果你是少数讨厌 systemd 的人,请离开,不要捣乱。

建议在移动设备上使用横向模式浏览

目录

已弃用的方法

让我们看看旧方法是如何工作的,然后再将它与 Quadlet 进行比较。你也可以跳到 Quadlet 部分。

首先,你需要创建一个容器。在之前一篇关于使用 rootless Podman 容器化 PostgreSQL的文章中,我创建了一个类似于以下命令的容器:

podman create \
--name test-db \
-p 5432:5432 \
-v ~/volumes/test-db:/var/lib/postgresql/data:Z \
-e POSTGRES_PASSWORD=CHANGE_ME \
--label "io.containers.autoupdate=registry" \
 docker.io/library/postgres:16

该命令的详细信息在之前的文章中已经解释过了。唯一没有解释的选项是 --label "io.containers.autoupdate=registry"。这个选项启用了使用 podman auto-update 更新容器镜像的功能,这将在本文后面进行解释。

创建容器后,你可以运行以下命令:

podman generate systemd test-db -fn --new

该命令的选项现在并不重要,但它们也在之前的文章中解释过了。

要使用这个生成的服务文件,你需要将它放在 ~/.config/systemd/user 目录下。要启用并启动它,你需要运行以下命令:

systemctl --user enable --now container-test-db

问题所在

旧方法的问题在于,它需要你运行命令来…

  1. 创建一个容器
  2. 生成一个服务文件
  3. 移动服务文件(如果它不在指定的目录中)
  4. 启用服务

特别是创建容器的命令通常很长。这意味着如果你想以后能够重新运行这些命令,你必须创建一个包含这些命令的 shell 脚本。

为了减少重复,我创建了以下 fish 函数,以便在创建容器的 fish 脚本中调用:

function podman-default-create
set -l container_name $argv[1]
podman create \
--name $container_name \
--replace \
    $argv[2..]; or return 1
podman generate systemd --no-header --new --name $container_name >~/.config/systemd/user/container-$container_name.service; or return 1
systemctl --user enable --now container-$container_name
end

你不需要理解上面函数的细节。我想用它来展示的是,旧方法太 hacky 了,并且涉及使用冗余命令。

你可能会想,肯定有一种更简单的方法。特别是如果你体验过 Docker Compose 提供的便利性。但这并不是唯一的问题。旧方法非常不灵活!

如果你想自定义服务文件并使用所有 systemd 功能,你需要_手动_编辑它_每次生成之后_!

Quadlet

让我们来看看使用 Quadlet 的新方法。

首先,你创建目录 ~/.config/containers/systemd。然后,你在其中放置一个 .container 文件。例如,这里是 test-db.container 文件:

[Container]
Image=docker.io/library/postgres:16
AutoUpdate=registry
PublishPort=5432:5432
Volume=%h/volumes/test-db:/var/lib/postgresql/data:Z
Environment=POSTGRES_PASSWORD=CHANGE_ME
[Service]
Restart=always
[Install]
WantedBy=default.target

这是一个普通的 systemd 服务文件,但带有一个特殊的 [Container] 部分。这个部分有许多文档化的选项。几乎所有这些选项都映射到可以使用 Podman (podman create) 创建容器的命令行选项。对于这个例子,我们感兴趣的是以下几个:

重要的是使用 systemd 说明符 %h 而不是 ~ 作为用户主目录。

[Service] 部分中,我们使用 Restart 选项,并将其设置为 always 以始终重新启动容器(除非手动停止)。

为了在启动时自动启动容器,我们在 [Install] 部分中将 WantedBy 选项设置为 default.target

注意: 我以为将 WantedBy 设置为 multi-user.target 会起作用,因为它是服务器上的默认目标。但在 rootless 容器的情况下,它不起作用。

multi-user.target 未在 systemd 的用户模式下定义。你可以通过运行命令 systemctl --user status multi-user.target 来验证这一点。它仅在系统模式下定义(systemctl status multi-user.target 没有 --user)。

由于我们使用用户服务进行 systemd 管理,我们必须为我们的用户启用 linger,以便在用户未登录的情况下启动容器:

loginctl enable-linger

⚠️ 警告 ⚠️ 必须启用 linger 才能在服务器重启后自动启动容器!

为了让 systemd 发现新的服务文件,运行 systemctl --user daemon-reload。现在,你可以使用 systemctl --user start test-db 启动容器。

你可以通过运行 systemctl --user status test-db 来检查容器服务的状态。你还可以通过运行 podman ps 来验证 Podman 容器是否正在运行。你应该找到容器 systemd-test-db

容器具有服务文件的名称(test-db.container,不带 .container 扩展名),并以 systemd- 作为前缀,以避免与未由 systemd 管理的容器发生冲突。但是你可以使用 [Container] 部分中的 ContainerName 选项手动设置容器的名称。

它更好吗?

我的第一印象是:“好吧,现在我必须将所有 podman create 选项映射到 [Container] 部分中的等效项。好处在哪里?”。

但在迁移了所有容器后,我发现了以下好处:

依赖

假设我们有一个应用程序容器,它依赖于我们创建的数据库容器。

你希望在启动应用程序容器时自动启动数据库容器。你还希望确保应用程序容器在数据库容器之后启动。否则,应用程序容器可能无法启动。

我们如何表达这种依赖关系?

让我们以 OxiTraffic 为例(无耻的插播广告 😅)。

这是应该放在 ~/.config/containers/systemd 中的容器服务文件 oxitraffic.container

[Container]
Image=docker.io/mo8it/oxitraffic:0.9.2
AutoUpdate=registry
Volume=%h/volumes/oxitraffic/config.toml:/volumes/config.toml:Z,ro
Volume=%h/volumes/oxitraffic/logs:/var/log/oxitraffic:Z
[Unit]
Requires=test-db.service
After=test-db.service
[Service]
Restart=always
[Install]
WantedBy=default.target

新的部分是 [Unit]。我们将 Requires 选项设置为 test-db.service,以便仅在数据库启动时才启动应用程序。我们还设置了 After 选项,以确保这两个容器不会并行启动。

请注意,我们在引用此容器服务时使用 test-db.service,而不是 test-db.container

为了使应用程序与数据库通信,应使用 [Container] 部分中的 Network 选项将网络添加到这两个容器中,但网络连接不在本文的讨论范围之内。

文件太多了吗?

在我们的示例中,我们创建了两个文件,一个用于应用程序容器,另一个用于数据库容器。这是否意味着使用 Quadlet 的多容器应用程序更复杂,因为你不能像使用 Docker Compose 一样将它们放在一个文件中?

这取决于你如何定义这种上下文中的复杂性。将内容拆分到多个文件中是否总是导致更高的复杂性?

对我来说,将所有内容放在同一个文件中更复杂。我不得不维护数百行代码和数十个容器的 Docker Compose 文件……这并不有趣!将每个容器放在自己的文件中对我来说减少了精神负担,因为我在处理它的文件时只需要考虑这一个容器。当然,你需要指定它对其他容器的依赖关系,但你不需要考虑这些其他容器的细节。

Mailcow 的 Docker Compose 文件 是一个可怕的大型 Docker Compose 文件的例子。

注意: Docker Compose 支持拆分到多个文件。

因此我们需要多个文件。但我们仍然应该将相关的文件分组在一起!Quadlet 支持将单元文件放置在 ~/.config/containers/systemd 目录中的目录中。对于我们的示例,你可以创建一个 oxitraffic 目录,并将这两个文件都放在其中。

更新镜像

现在,我们的容器在后台运行,并在服务器重启后自动启动。如果有一种简单的方法来更新这些容器的镜像,而无需为每个容器运行 podman pull 然后重新启动更新后的容器,那不是很好吗?

例如,如果为 PostgreSQL 16 上传了一个新镜像(使用我们使用的镜像标签 16),则应更新该镜像并重新启动容器。

使用 Docker,你需要像 Watchtower 这样的东西。但是 Podman 提供了一个开箱即用的工具!

如果设置了 AutoUpdate=registry,则只需运行 podman auto-update,Podman 将检查注册表是否具有与使用的标签兼容的较新镜像。在这种情况下,将拉取镜像并重新启动容器。就是这么简单 😍

当然,如果你对 OxiTraffic 使用像 latest 这样的标签而不是像 0.9.2 这样的具体版本,这可能会很危险。因为推送到 latest 标签的下一个版本可能包含重大更改!如果你为 PostgreSQL 镜像使用 latest 标签,那就更戏剧化了,因为在将 PostgreSQL 升级到新的主要版本时始终需要手动迁移。

因此,始终使用不会导致重大更改的标签!相信我,这不仅仅是 Podman 更新的问题。前段时间,我在尝试部署使用 latest 标签的 Docker 容器时,也付出了惨痛的代价。

就我个人而言,我每隔几天在服务器上手动运行 podman auto-update,以查看更新了什么,并确保容器之后仍然运行正常。

podman-compose 怎么样?

有 Python 脚本 podman-compose,它使用 Podman 运行 Compose 文件。但我不认为它是 Docker Compose 的长期替代方案,原因有很多:

Quadlet 与 Podman 的 rootless、daemonless 设计更加一致。

如果你被 Compose 文件困扰并且想尝试 Quadlet,请查看 podlet,它可以帮助你进行迁移。

更多资源

将这篇文章作为介绍。我强烈建议阅读手册页 podman-systemd.unit,以更深入地了解 Quadlet。不过,你不需要阅读每个受支持选项的文档。

Quadlet 不仅适用于容器。它还可以管理 pod、网络和卷(参见手册页)。

如果你不熟悉编写 systemd 单元文件(像我一样),我还建议查看手册页 systemd.unitsystemd.service

podlet 是一个很棒的 Rust 工具,可以帮助你进行迁移。它可以从 Podman 命令甚至(Docker)Compose 文件创建 Quadlet 文件。

查看 blog.while-true-do 上类似的博客文章,以获得另一种视角和第二个示例。它是我最喜欢的与 Linux 相关的博客 🥰

最后,如果你想将我的迁移作为示例,则可以比较 之前之后