Tools: Essential Guide: Stop Rebuilding Images for Every Config Change: Practical `systemd-confext` for Portable `/etc` Overlays

Tools: Essential Guide: Stop Rebuilding Images for Every Config Change: Practical `systemd-confext` for Portable `/etc` Overlays

Stop Rebuilding Images for Every Config Change: Practical systemd-confext for Portable /etc Overlays

What systemd-confext actually does

When this is a good fit

When this is the wrong tool

Check whether your host supports it

How matching works

Where confexts live

A practical directory-based example

1) Create the confext tree

2) Add the compatibility metadata

3) Add real config files

4) Inspect what is installed

5) Merge the overlay

6) Verify the result

Rollback is the nice part

Safer testing with --root=

About mutability

Disk-image workflows

Common mistakes to avoid

1) Shipping the wrong path

2) Forgetting the metadata file

3) Mismatching the image name

4) Using it for binaries

5) Assuming refresh is atomic

A pattern I like in practice

Final take If your base OS image is supposed to stay stable, config drift becomes annoying fast. You tweak a unit drop-in, change a tmpfiles rule, add a sysctl file, or adjust journald settings, and suddenly you are choosing between three awkward options: systemd-confext gives you another option. It lets you ship a version-checked, read-only overlay for /etc, then merge or unmerge it at runtime. In practice, that means you can package configuration as a portable layer, apply it without rebuilding the full OS image, and roll it back by removing the layer. This post is intentionally not about systemd-sysext for /usr. I covered that separately. Here the focus is narrower and more operational: portable /etc overlays for runtime reconfiguration. systemd-confext is the configuration-extension companion to systemd-sysext. Where systemd-sysext overlays /usr and /opt, systemd-confext overlays only /etc. Extension content outside /etc is ignored. The merged /etc overlay is mounted with nosuid and, by default, noexec. A few practical consequences matter immediately: According to the upstream man page, systemd-confext support was added in systemd 254. systemd-confext makes sense when you want to: Do not treat systemd-confext as a generic packaging system. It is a poor fit when you need: Also, avoid using it to replace every normal config-management workflow. This is strongest when you want a portable, reversible config layer tied to an image-based or tightly controlled host setup. If the command is missing, your systemd build is probably older than 254 or your distro has not shipped the tool yet. A confext image must include a metadata file named like this: NAME must match the confext image name. That file follows the os-release style key/value format and is used to verify compatibility with the base OS. For confext images, the important fields are: In plain English, your overlay should say what OS it matches. If the compatibility data does not match, systemd-confext refuses to merge unless you force it. That is a feature, not friction. systemd-confext looks for configuration extensions in: In practice, /var/lib/confexts/ is the usual place to install them. Let’s build a confext that does two things: I am using nginx.service as the example target because the drop-in pattern is common, but the same structure works for any service that already exists on the host. First check the host values: Now create the extension metadata file: Adjust those values for your real host OS. A journald policy drop-in: And a service drop-in: If the extension metadata requests a manager reload with EXTENSION_RELOAD_MANAGER=1, the tool can reload the manager automatically unless you pass --no-reload. Check that the overlay is active: Inspect the visible files: For the journald config, reload and confirm: For the service drop-in, reload unit files and inspect the merged unit: If you remove the overlay, the old files disappear with it. That is the operational appeal here. You are not cleaning up a pile of hand-edited files in /etc. You are withdrawing one layer. If you change files inside the extension tree, use: Be aware that upstream documents refresh as an unmerge followed by a merge, with a brief gap where the overlaid files disappear before the new overlay is mounted. One of the best features for real operations is --root=. You can test against an offline root directory instead of your live host /etc. Then operate on the alternate root: This is especially useful when you build images in CI or want to validate a confext against a mounted image root before deployment. By default, a merged confext makes /etc read-only. That default is usually what you want, because it preserves the idea that the overlay is controlled and reproducible. If you need writes while extensions are merged, systemd-confext supports mutable modes through --mutable=. Upstream documents these modes: These modes route writes through /var/lib/extensions.mutable/ or ephemeral directories, depending on the mode. My advice: start with the default immutable behavior unless you have a very specific write-routing need. The more mutable your overlay becomes, the less clean your rollback story is. Confexts do not have to be plain directories. The same interface also supports image-based extensions, and the upstream systemd-repart documentation explicitly mentions --make-ddi=TYPE with confext as one of the generated image types. That is useful when you want signed or image-based delivery instead of a plain directory tree, but directory-based confexts are the easiest place to start because they are simple to inspect and debug. If your files are not under /etc inside the confext tree, they will not be merged. No valid extension-release.NAME, no clean compatibility check. The NAME in extension-release.NAME must match the extension image name. That is what systemd-sysext or packages are for. It is convenient, but upstream notes that there is a brief disappearance window during refresh. For image-based hosts, split changes into three layers: That separation keeps intent clear: systemd-confext is not a replacement for every config-management tool, and it is not the right answer for every Linux host. But if you run image-based systems, appliances, lab hosts, or tightly controlled servers, it gives you a very clean operational primitive: ship config as a removable layer, validate it against the host OS, merge it when needed, and roll it back without leaving /etc full of debris. That is a pretty nice trade. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

systemd-confext --version systemd-confext --help systemd-confext --version systemd-confext --help systemd-confext --version systemd-confext --help /etc/extension-release.d/extension-release.NAME /etc/extension-release.d/extension-release.NAME /etc/extension-release.d/extension-release.NAME sudo mkdir -p /var/lib/confexts/ops-policy/etc/extension-release.d sudo mkdir -p /var/lib/confexts/ops-policy/etc/systemd/journald.conf.d sudo mkdir -p /var/lib/confexts/ops-policy/etc/systemd/system/nginx.service.d sudo mkdir -p /var/lib/confexts/ops-policy/etc/extension-release.d sudo mkdir -p /var/lib/confexts/ops-policy/etc/systemd/journald.conf.d sudo mkdir -p /var/lib/confexts/ops-policy/etc/systemd/system/nginx.service.d sudo mkdir -p /var/lib/confexts/ops-policy/etc/extension-release.d sudo mkdir -p /var/lib/confexts/ops-policy/etc/systemd/journald.conf.d sudo mkdir -p /var/lib/confexts/ops-policy/etc/systemd/system/nginx.service.d . /etc/os-release printf 'ID=%s\nVERSION_ID=%s\n' "$ID" "$VERSION_ID" . /etc/os-release printf 'ID=%s\nVERSION_ID=%s\n' "$ID" "$VERSION_ID" . /etc/os-release printf 'ID=%s\nVERSION_ID=%s\n' "$ID" "$VERSION_ID" sudo tee /var/lib/confexts/ops-policy/etc/extension-release.d/extension-release.ops-policy >/dev/null <<'EOF' ID=debian VERSION_ID=12 EOF sudo tee /var/lib/confexts/ops-policy/etc/extension-release.d/extension-release.ops-policy >/dev/null <<'EOF' ID=debian VERSION_ID=12 EOF sudo tee /var/lib/confexts/ops-policy/etc/extension-release.d/extension-release.ops-policy >/dev/null <<'EOF' ID=debian VERSION_ID=12 EOF sudo tee /var/lib/confexts/ops-policy/etc/systemd/journald.conf.d/10-retention.conf >/dev/null <<'EOF' [Journal] SystemMaxUse=1G SystemKeepFree=500M MaxRetentionSec=1month EOF sudo tee /var/lib/confexts/ops-policy/etc/systemd/journald.conf.d/10-retention.conf >/dev/null <<'EOF' [Journal] SystemMaxUse=1G SystemKeepFree=500M MaxRetentionSec=1month EOF sudo tee /var/lib/confexts/ops-policy/etc/systemd/journald.conf.d/10-retention.conf >/dev/null <<'EOF' [Journal] SystemMaxUse=1G SystemKeepFree=500M MaxRetentionSec=1month EOF sudo tee /var/lib/confexts/ops-policy/etc/systemd/system/nginx.service.d/10-restart-window.conf >/dev/null <<'EOF' [Service] Restart=on-failure RestartSec=5s EOF sudo tee /var/lib/confexts/ops-policy/etc/systemd/system/nginx.service.d/10-restart-window.conf >/dev/null <<'EOF' [Service] Restart=on-failure RestartSec=5s EOF sudo tee /var/lib/confexts/ops-policy/etc/systemd/system/nginx.service.d/10-restart-window.conf >/dev/null <<'EOF' [Service] Restart=on-failure RestartSec=5s EOF sudo systemd-confext list sudo systemd-confext status sudo systemd-confext list sudo systemd-confext status sudo systemd-confext list sudo systemd-confext status sudo systemd-confext merge sudo systemd-confext merge sudo systemd-confext merge sudo systemd-confext status mount | grep ' on /etc ' sudo systemd-confext status mount | grep ' on /etc ' sudo systemd-confext status mount | grep ' on /etc ' sudo cat /etc/systemd/journald.conf.d/10-retention.conf sudo systemctl cat nginx.service sudo cat /etc/systemd/journald.conf.d/10-retention.conf sudo systemctl cat nginx.service sudo cat /etc/systemd/journald.conf.d/10-retention.conf sudo systemctl cat nginx.service sudo systemctl restart systemd-journald sudo journalctl --disk-usage sudo systemctl restart systemd-journald sudo journalctl --disk-usage sudo systemctl restart systemd-journald sudo journalctl --disk-usage sudo systemctl daemon-reload sudo systemctl show nginx.service -p Restart -p RestartUSec sudo systemctl daemon-reload sudo systemctl show nginx.service -p Restart -p RestartUSec sudo systemctl daemon-reload sudo systemctl show nginx.service -p Restart -p RestartUSec sudo systemd-confext unmerge sudo systemd-confext status sudo systemd-confext unmerge sudo systemd-confext status sudo systemd-confext unmerge sudo systemd-confext status sudo systemd-confext refresh sudo systemd-confext refresh sudo systemd-confext refresh sudo mkdir -p /tmp/testroot/etc sudo cp -a /etc/os-release /tmp/testroot/etc/ sudo cp -a /var/lib/confexts /tmp/testroot/var/lib/ sudo mkdir -p /tmp/testroot/etc sudo cp -a /etc/os-release /tmp/testroot/etc/ sudo cp -a /var/lib/confexts /tmp/testroot/var/lib/ sudo mkdir -p /tmp/testroot/etc sudo cp -a /etc/os-release /tmp/testroot/etc/ sudo cp -a /var/lib/confexts /tmp/testroot/var/lib/ sudo systemd-confext --root=/tmp/testroot status sudo systemd-confext --root=/tmp/testroot merge find /tmp/testroot/etc -maxdepth 4 | sort sudo systemd-confext --root=/tmp/testroot unmerge sudo systemd-confext --root=/tmp/testroot status sudo systemd-confext --root=/tmp/testroot merge find /tmp/testroot/etc -maxdepth 4 | sort sudo systemd-confext --root=/tmp/testroot unmerge sudo systemd-confext --root=/tmp/testroot status sudo systemd-confext --root=/tmp/testroot merge find /tmp/testroot/etc -maxdepth 4 | sort sudo systemd-confext --root=/tmp/testroot unmerge - rebuild the whole image, - mutate /etc in place and hope you can track it later, - bolt on a one-off config management path just for a small policy change. - it is for configuration, not for shipping binaries, - it is read-only by default, - it can be merged, unmerged, refreshed, listed, and inspected, - compatibility is checked against the base OS before merge. - ship a small set of /etc policy files separately from the base image, - change service configuration without a full image rebuild, - keep rollback simple by removing one extension layer, - test configuration overlays against an offline root with --root=. - journald.conf.d/ retention policy - tmpfiles.d/ cleanup rules - sysctl.d/ tuning profiles - systemd/system/*.service.d/ drop-ins - modprobe.d/ policy files - ssh/sshd_config.d/ overlays, if your distro uses drop-ins - dependency management, - earliest-boot configuration before the relevant filesystems are available, - service payloads or binaries, which belong in packages, portable services, or systemd-sysext. - VERSION_ID= - CONFEXT_LEVEL= - /run/confexts/ - /var/lib/confexts/ - /usr/lib/confexts/ - /usr/local/lib/confexts/ - caps persistent journal size, - adds a systemd drop-in for a service. - no (default) - ephemeral-import - base OS image for the stable platform, - systemd-sysext for optional /usr tools, - systemd-confext for runtime /etc policy. - binaries live in one layer, - configuration lives in another, - rollback stays understandable. - systemd-sysext / systemd-confext man page (man7 mirror): https://man7.org/linux/man-pages/man8/systemd-sysext.8.html - os-release / extension-release reference (local man page cross-check): man os-release - systemd-repart documentation showing DDI generation support for confext: https://manpages.debian.org/testing/systemd/systemd-repart.8.en.html - systemd upstream discussion for confext design context: https://github.com/systemd/systemd/issues/24864