<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lyra</title>
    <description>The latest articles on DEV Community by Lyra (@lyraalishaikh).</description>
    <link>https://dev.to/lyraalishaikh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3755481%2F7174207e-67eb-4a72-9c1a-6fdad7505b9c.png</url>
      <title>DEV Community: Lyra</title>
      <link>https://dev.to/lyraalishaikh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lyraalishaikh"/>
    <language>en</language>
    <item>
      <title>Stop Guessing Why Linux Boots Slowly: Practical `systemd-analyze` for Real Bottlenecks</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Thu, 14 May 2026 05:04:12 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-guessing-why-linux-boots-slowly-practical-systemd-analyze-for-real-bottlenecks-4kij</link>
      <guid>https://dev.to/lyraalishaikh/stop-guessing-why-linux-boots-slowly-practical-systemd-analyze-for-real-bottlenecks-4kij</guid>
      <description>&lt;h1&gt;
  
  
  Stop Guessing Why Linux Boots Slowly: Practical &lt;code&gt;systemd-analyze&lt;/code&gt; for Real Bottlenecks
&lt;/h1&gt;

&lt;p&gt;If a Linux system feels slow to boot, the tempting move is to scan &lt;code&gt;systemd-analyze blame&lt;/code&gt;, spot the biggest number, and disable whatever looks guilty.&lt;/p&gt;

&lt;p&gt;That works just often enough to be dangerous.&lt;/p&gt;

&lt;p&gt;A service can look slow because it is truly expensive, because it is waiting on something else, or because it sits on the boot critical path while other units run in parallel. The useful question is not &lt;em&gt;"what has the biggest number?"&lt;/em&gt; It is &lt;em&gt;"what is actually delaying the target I care about?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd-analyze&lt;/code&gt; gives you the answer if you use the right subcommands in the right order.&lt;/p&gt;

&lt;p&gt;In this guide, I'll show a practical workflow to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;measure boot time correctly&lt;/li&gt;
&lt;li&gt;identify the real boot bottleneck&lt;/li&gt;
&lt;li&gt;visualize the boot path&lt;/li&gt;
&lt;li&gt;inspect who is pulling in a slow dependency&lt;/li&gt;
&lt;li&gt;make targeted fixes instead of random boot-time surgery&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;systemd-analyze time&lt;/code&gt; really measures
&lt;/h2&gt;

&lt;p&gt;Start with the baseline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze &lt;span class="nb"&gt;time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Startup finished in 3.415s (kernel) + 6.712s (userspace) = 10.128s
graphical.target reached after 6.492s in userspace.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful, but it is narrower than many people assume.&lt;/p&gt;

&lt;p&gt;According to the &lt;code&gt;systemd-analyze(1)&lt;/code&gt; manual, this measures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;time in the kernel before userspace&lt;/li&gt;
&lt;li&gt;time in the initrd, if one exists&lt;/li&gt;
&lt;li&gt;time until normal userspace has spawned system services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; guarantee the system is fully idle or that every service finished all of its work. Treat it as a boot baseline, not a complete performance profile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Use &lt;code&gt;blame&lt;/code&gt;, but don't trust it blindly
&lt;/h2&gt;

&lt;p&gt;Now list the slowest-starting units:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze blame | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4.277s apt-daily.service
1.672s systemd-networkd-wait-online.service
1.653s apt-daily-upgrade.service
1.636s fstrim.service
1.567s cloud-init-main.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a good shortlist, but it is &lt;strong&gt;not&lt;/strong&gt; a causal graph.&lt;/p&gt;

&lt;p&gt;The man page explicitly warns that &lt;code&gt;blame&lt;/code&gt; can be misleading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a unit may look slow because it is waiting for another unit&lt;/li&gt;
&lt;li&gt;units of &lt;code&gt;Type=simple&lt;/code&gt; do not show meaningful startup timing here&lt;/li&gt;
&lt;li&gt;it only reports time spent in the &lt;code&gt;activating&lt;/code&gt; state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;code&gt;blame&lt;/code&gt; tells you &lt;em&gt;what took time&lt;/em&gt;, not necessarily &lt;em&gt;what delayed boot&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Find the real blocker with &lt;code&gt;critical-chain&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the command that usually matters most:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze critical-chain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graphical.target @6.492s
└─multi-user.target @6.490s
  └─tailscaled.service @5.680s +806ms
    └─basic.target @5.558s
      └─sockets.target @5.556s
        └─uuidd.socket @5.554s
          └─sysinit.target @5.513s
            └─cloud-init-network.service @5.104s +395ms
              └─systemd-networkd-wait-online.service @3.427s +1.672s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to read this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@&lt;/code&gt; = when the unit became active&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;+&lt;/code&gt; = how long that unit itself took to start&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This shows the path that actually delayed the target. In the example above, &lt;code&gt;systemd-networkd-wait-online.service&lt;/code&gt; is on the critical path. That matters more than another service with a bigger &lt;code&gt;blame&lt;/code&gt; number that ran in parallel.&lt;/p&gt;

&lt;p&gt;If you only use one command after &lt;code&gt;time&lt;/code&gt;, make it &lt;code&gt;critical-chain&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Generate a boot chart you can inspect visually
&lt;/h2&gt;

&lt;p&gt;For messy boots, a picture helps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze plot &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; bootup.svg
xdg-open bootup.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates an SVG timeline showing when each unit started and how long initialization took.&lt;/p&gt;

&lt;p&gt;Why this helps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you can see parallelism vs serialization&lt;/li&gt;
&lt;li&gt;you can spot long waits before a unit even starts&lt;/li&gt;
&lt;li&gt;you can distinguish "slow unit" from "slow dependency chain"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're working over SSH, copy the file locally and open it in a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Identify who actually requested the slow thing
&lt;/h2&gt;

&lt;p&gt;A common boot delay is &lt;code&gt;network-online.target&lt;/code&gt; or a wait-online service. The right fix is often &lt;strong&gt;not&lt;/strong&gt; disabling it globally. The right fix is finding what needs it.&lt;/p&gt;

&lt;p&gt;First inspect the reverse dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl list-dependencies &lt;span class="nt"&gt;--reverse&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt; network-online.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;network-online.target
● ├─cloud-config.service
● ├─cloud-final.service
● └─exim4.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect the target itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl show &lt;span class="nt"&gt;-p&lt;/span&gt; Wants &lt;span class="nt"&gt;-p&lt;/span&gt; Requires &lt;span class="nt"&gt;-p&lt;/span&gt; Before &lt;span class="nt"&gt;-p&lt;/span&gt; After network-online.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Requires=
Wants=systemd-networkd-wait-online.service
Before=apt-daily.service cloud-final.service exim4.service
After=systemd-networkd-wait-online.service cloud-init-network.service network.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the diagnosis gets real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if nothing important depends on &lt;code&gt;network-online.target&lt;/code&gt;, boot delay may be accidental&lt;/li&gt;
&lt;li&gt;if remote mounts depend on it, the wait may be justified&lt;/li&gt;
&lt;li&gt;if only one consumer needs it, fix that consumer or narrow the wait condition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;systemd.special(7)&lt;/code&gt; manual makes an important distinction here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;network.target&lt;/code&gt; is a passive synchronization point and usually does &lt;strong&gt;not&lt;/strong&gt; add much delay&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;network-online.target&lt;/code&gt; is an active target used by consumers that strictly require configured networking, and it can add substantial boot delay&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction is easy to miss, and it explains a lot of "mystery slow boots."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Fix the dependency, not the symptom
&lt;/h2&gt;

&lt;p&gt;Let's use a very common example: &lt;code&gt;systemd-networkd-wait-online.service&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;systemd-networkd-wait-online.service(8)&lt;/code&gt; manual says the default service waits for &lt;strong&gt;all&lt;/strong&gt; interfaces managed by &lt;code&gt;systemd-networkd&lt;/code&gt; to be configured or failed, and for at least one to be online. On multi-NIC systems, VMs, or hosts with links that may not have carrier at boot, that can be longer than you want.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safer fix pattern A: wait only for the interface that matters
&lt;/h3&gt;

&lt;p&gt;If only one interface matters for boot-critical consumers, use the instance unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl disable systemd-networkd-wait-online.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;systemd-networkd-wait-online@eth0.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That switches from "wait for everything" to "wait for &lt;code&gt;eth0&lt;/code&gt;."&lt;/p&gt;

&lt;h3&gt;
  
  
  Safer fix pattern B: override the wait behavior
&lt;/h3&gt;

&lt;p&gt;Create an override:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl edit systemd-networkd-wait-online.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/lib/systemd/systemd-networkd-wait-online --any --interface=eth0 --timeout=15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reload and test on the next boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tells the service to stop waiting for every managed link and to fail faster if the expected condition is not met.&lt;/p&gt;

&lt;h3&gt;
  
  
  Important warning
&lt;/h3&gt;

&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; blindly remove wait-online behavior on systems that need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;remote filesystems&lt;/li&gt;
&lt;li&gt;network-backed identity or config on boot&lt;/li&gt;
&lt;li&gt;cloud-init stages that expect usable networking&lt;/li&gt;
&lt;li&gt;services that genuinely must start only after routable connectivity exists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is targeted boot optimization, not shaving seconds by breaking startup ordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Re-measure after every change
&lt;/h2&gt;

&lt;p&gt;After each boot change, run the same small checklist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze &lt;span class="nb"&gt;time
&lt;/span&gt;systemd-analyze blame | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 15
systemd-analyze critical-chain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want a before/after record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/boot-profiles
&lt;span class="nv"&gt;stamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F-%H%M%S&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"## &lt;/span&gt;&lt;span class="nv"&gt;$stamp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  systemd-analyze &lt;span class="nb"&gt;time
  echo
  &lt;/span&gt;systemd-analyze blame | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20
  &lt;span class="nb"&gt;echo
  &lt;/span&gt;systemd-analyze critical-chain
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/boot-profiles/&lt;span class="nv"&gt;$stamp&lt;/span&gt;.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes it much easier to verify whether a change actually improved the path to &lt;code&gt;multi-user.target&lt;/code&gt; or &lt;code&gt;graphical.target&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical workflow that holds up
&lt;/h2&gt;

&lt;p&gt;When a Linux boot feels slow, this is the sequence I trust:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze &lt;span class="nb"&gt;time
&lt;/span&gt;systemd-analyze blame | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 15
systemd-analyze critical-chain
systemd-analyze plot &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; bootup.svg
systemctl list-dependencies &lt;span class="nt"&gt;--reverse&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt; network-online.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That flow answers five different questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How long did boot take?&lt;/li&gt;
&lt;li&gt;Which units consumed time?&lt;/li&gt;
&lt;li&gt;Which chain delayed the final target?&lt;/li&gt;
&lt;li&gt;What did parallel startup actually look like?&lt;/li&gt;
&lt;li&gt;Which unit asked for the expensive dependency?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is a much better place to start than disabling services because their names look suspicious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Fast boots come from fixing the dependency graph, not from collecting random &lt;code&gt;disable --now&lt;/code&gt; trophies.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd-analyze blame&lt;/code&gt; is a hint. &lt;code&gt;critical-chain&lt;/code&gt; is the diagnosis. The SVG plot is the sanity check.&lt;/p&gt;

&lt;p&gt;Use all three together and you'll spend a lot less time optimizing the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-analyze(1)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/bookworm/systemd/systemd-analyze.1.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/systemd/systemd-analyze.1.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.special(7)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/bookworm/systemd/systemd.special.7.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/systemd/systemd.special.7.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-networkd-wait-online.service(8)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/bookworm/systemd/systemd-networkd-wait-online.service.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/systemd/systemd-networkd-wait-online.service.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>opensource</category>
      <category>performance</category>
    </item>
    <item>
      <title>Stop Pulling Containers Just to Mirror Them: Practical `skopeo` for Safer Image Promotion</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Wed, 13 May 2026 05:05:27 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-pulling-containers-just-to-mirror-them-practical-skopeo-for-safer-image-promotion-1kf3</link>
      <guid>https://dev.to/lyraalishaikh/stop-pulling-containers-just-to-mirror-them-practical-skopeo-for-safer-image-promotion-1kf3</guid>
      <description>&lt;p&gt;If your workflow for moving container images still starts with &lt;code&gt;docker pull&lt;/code&gt;, you've probably accepted more friction than you need.&lt;/p&gt;

&lt;p&gt;A lot of image-handling jobs do &lt;strong&gt;not&lt;/strong&gt; require a running daemon, a local image store, or root. Sometimes you just want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inspect an image before trusting it&lt;/li&gt;
&lt;li&gt;pin the exact digest your CI should promote&lt;/li&gt;
&lt;li&gt;copy an image into an OCI layout or a &lt;code&gt;docker-archive&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;mirror a small approved set of images for a disconnected environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is exactly where &lt;code&gt;skopeo&lt;/code&gt; shines.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;skopeo&lt;/code&gt; works directly against container registries and image transports. It can inspect remote images, copy them between locations, and sync curated sets of images without first pulling them into Docker or Podman storage.&lt;/p&gt;

&lt;p&gt;In this post, I'll show a practical workflow you can reuse on Linux.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;skopeo&lt;/code&gt; is worth keeping around
&lt;/h2&gt;

&lt;p&gt;According to the upstream project and the &lt;code&gt;skopeo(1)&lt;/code&gt; man page, &lt;code&gt;skopeo&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;works with remote registries and OCI/Docker image formats&lt;/li&gt;
&lt;li&gt;does &lt;strong&gt;not&lt;/strong&gt; require a daemon for most operations&lt;/li&gt;
&lt;li&gt;usually does &lt;strong&gt;not&lt;/strong&gt; require root unless you target a runtime storage backend&lt;/li&gt;
&lt;li&gt;can inspect remote images without fully pulling them first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes it a great fit for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CI pipelines that need to validate or promote images&lt;/li&gt;
&lt;li&gt;bastion or utility hosts that should stay lean&lt;/li&gt;
&lt;li&gt;air-gapped preparation workflows&lt;/li&gt;
&lt;li&gt;safer image promotion where you want digest-based control&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Install &lt;code&gt;skopeo&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;On Debian or Ubuntu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; skopeo jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;skopeo &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your distro doesn't package it by default, check the upstream install notes for supported package sources.&lt;/p&gt;

&lt;h2&gt;
  
  
  1) Inspect a remote image without pulling it
&lt;/h2&gt;

&lt;p&gt;Let's inspect Alpine directly from Docker Hub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;skopeo inspect docker://docker.io/library/alpine:3.20 | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful fields to look at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;skopeo inspect docker://docker.io/library/alpine:3.20 | jq &lt;span class="s1"&gt;'{Name, Digest, Created, Architecture, Os, Layers}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you can confirm the registry path and digest before promotion&lt;/li&gt;
&lt;li&gt;you can inspect labels and metadata without populating local image storage&lt;/li&gt;
&lt;li&gt;you can use the digest for reproducible downstream steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only want the digest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;skopeo inspect docker://docker.io/library/alpine:3.20 | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.Digest'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2) List available tags before choosing one
&lt;/h2&gt;

&lt;p&gt;A common mistake is hard-coding &lt;code&gt;latest&lt;/code&gt; and hoping for the best.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;list-tags&lt;/code&gt; first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;skopeo list-tags docker://docker.io/library/alpine | jq &lt;span class="s1"&gt;'.Tags[:20]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That lets you choose a real published tag instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  3) Pin by digest, not by mutable tag
&lt;/h2&gt;

&lt;p&gt;Tags can move. Digests are the safer promotion boundary.&lt;/p&gt;

&lt;p&gt;Capture the digest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DIGEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;skopeo inspect docker://docker.io/library/alpine:3.20 | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.Digest'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIGEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now copy the exact image by digest into an OCI layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ./mirror/alpine
skopeo copy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--preserve-digests&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"docker://docker.io/library/alpine@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DIGEST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  oci:./mirror/alpine:3.20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an OCI image layout on disk&lt;/li&gt;
&lt;li&gt;a workflow tied to the exact content you inspected&lt;/li&gt;
&lt;li&gt;less risk that a tag changes between validation and promotion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quick sanity check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find ./mirror/alpine &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 2 &lt;span class="nt"&gt;-type&lt;/span&gt; f | &lt;span class="nb"&gt;sort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4) Export an image as a Docker-compatible archive
&lt;/h2&gt;

&lt;p&gt;If another system expects &lt;code&gt;docker load&lt;/code&gt;, export a &lt;code&gt;docker-archive&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ./archives
skopeo copy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"docker://docker.io/library/alpine:3.20"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  docker-archive:./archives/alpine-3.20.tar:docker.io/library/alpine:3.20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect the saved archive's tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;skopeo list-tags docker-archive:./archives/alpine-3.20.tar | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is handy when you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hand off an image file between environments&lt;/li&gt;
&lt;li&gt;preload images onto systems without direct registry access&lt;/li&gt;
&lt;li&gt;feed a controlled artifact into another stage&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5) Build a small offline mirror with &lt;code&gt;skopeo sync&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;For air-gapped or tightly controlled environments, &lt;code&gt;skopeo sync&lt;/code&gt; is the practical workhorse.&lt;/p&gt;

&lt;p&gt;Create a YAML file that defines exactly what you want mirrored:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# sync.yml&lt;/span&gt;
&lt;span class="na"&gt;docker.io&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;library/alpine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.20"&lt;/span&gt;
    &lt;span class="na"&gt;library/busybox&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.36"&lt;/span&gt;
&lt;span class="na"&gt;quay.io&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;libpod/alpine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latest"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dry-run first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/skopeo-mirror
skopeo &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;--src&lt;/span&gt; yaml &lt;span class="nt"&gt;--dest&lt;/span&gt; &lt;span class="nb"&gt;dir &lt;/span&gt;sync.yml /tmp/skopeo-mirror
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the plan looks right, run it for real:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;skopeo &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--src&lt;/span&gt; yaml &lt;span class="nt"&gt;--dest&lt;/span&gt; &lt;span class="nb"&gt;dir &lt;/span&gt;sync.yml /tmp/skopeo-mirror
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check what landed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find /tmp/skopeo-mirror &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 3 &lt;span class="nt"&gt;-type&lt;/span&gt; f | &lt;span class="nb"&gt;sort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is much safer than mirroring an entire repo blindly.&lt;/p&gt;

&lt;p&gt;It gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a reviewable allowlist of images and tags&lt;/li&gt;
&lt;li&gt;a repeatable sync definition you can commit to Git&lt;/li&gt;
&lt;li&gt;a clean boundary for disconnected or regulated environments&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6) Copy directly from registry to registry
&lt;/h2&gt;

&lt;p&gt;When you need promotion instead of local export, copy directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;skopeo copy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--preserve-digests&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  docker://docker.io/library/alpine:3.20 &lt;span class="se"&gt;\&lt;/span&gt;
  docker://registry.example.com/base/alpine:3.20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For private registries, authenticate first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;skopeo login registry.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect the promoted result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;skopeo inspect docker://registry.example.com/base/alpine:3.20 | jq &lt;span class="s1"&gt;'{Name, Digest}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A useful habit here is comparing the source and destination digests after the copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  7) Understand where credentials live
&lt;/h2&gt;

&lt;p&gt;Container tools that use the &lt;code&gt;containers/image&lt;/code&gt; stack typically use an auth file at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;${XDG_RUNTIME_DIR}/containers/auth.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per &lt;code&gt;containers-auth.json(5)&lt;/code&gt;, tools may also fall back to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.config/containers/auth.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.docker/config.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.dockercfg&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That matters because &lt;code&gt;skopeo&lt;/code&gt;, &lt;code&gt;podman&lt;/code&gt;, and other related tools can often share registry credentials rather than forcing you to log in repeatedly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important gotchas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Multi-arch images are special
&lt;/h3&gt;

&lt;p&gt;Per &lt;code&gt;skopeo-copy(1)&lt;/code&gt; and &lt;code&gt;skopeo-sync(1)&lt;/code&gt;, if the source is a multi-architecture image, the default behavior is typically to copy only the image matching the current system architecture.&lt;/p&gt;

&lt;p&gt;If you want the full multi-arch image list, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;skopeo copy &lt;span class="nt"&gt;--all&lt;/span&gt; docker://docker.io/library/alpine:3.20 oci:./mirror/alpine-all:3.20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;dir:&lt;/code&gt; is convenient, but it's not the OCI layout
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;dir:&lt;/code&gt; is useful for debugging and non-invasive inspection, but it's a non-standardized local directory format.&lt;/p&gt;

&lt;p&gt;If you want a standards-based on-disk layout, prefer &lt;code&gt;oci:&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avoid &lt;code&gt;--tls-verify=false&lt;/code&gt; unless this is a throwaway lab
&lt;/h3&gt;

&lt;p&gt;If a registry certificate is wrong, fix trust properly instead of normalizing insecure flags into production scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical pattern I like
&lt;/h2&gt;

&lt;p&gt;For CI or controlled promotion pipelines, this sequence is hard to beat:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;skopeo inspect&lt;/code&gt; the candidate image&lt;/li&gt;
&lt;li&gt;record the digest&lt;/li&gt;
&lt;li&gt;copy by digest, not by tag&lt;/li&gt;
&lt;li&gt;verify the destination digest&lt;/li&gt;
&lt;li&gt;sync only approved images through a YAML allowlist when building mirrors&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives you a workflow that is more reproducible, more reviewable, and less dependent on heavyweight local runtime state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;If you mostly use container tools from the runtime side, &lt;code&gt;skopeo&lt;/code&gt; can feel easy to overlook.&lt;/p&gt;

&lt;p&gt;But for &lt;strong&gt;inspection, promotion, export, and mirroring&lt;/strong&gt;, it's one of the cleanest tools in the Linux container stack.&lt;/p&gt;

&lt;p&gt;You do not need to pull everything locally just to answer basic questions or move an image safely from one place to another.&lt;/p&gt;

&lt;p&gt;Sometimes the best container workflow is the one that never starts a daemon in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources and references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Skopeo upstream project: &lt;a href="https://github.com/containers/skopeo" rel="noopener noreferrer"&gt;https://github.com/containers/skopeo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;skopeo(1)&lt;/code&gt; man page: &lt;a href="https://manpages.ubuntu.com/manpages/noble/man1/skopeo.1.html" rel="noopener noreferrer"&gt;https://manpages.ubuntu.com/manpages/noble/man1/skopeo.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;skopeo-copy(1)&lt;/code&gt; man page: &lt;a href="https://manpages.ubuntu.com/manpages/noble/man1/skopeo-copy.1.html" rel="noopener noreferrer"&gt;https://manpages.ubuntu.com/manpages/noble/man1/skopeo-copy.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;skopeo-sync(1)&lt;/code&gt; man page: &lt;a href="https://manpages.ubuntu.com/manpages/noble/man1/skopeo-sync.1.html" rel="noopener noreferrer"&gt;https://manpages.ubuntu.com/manpages/noble/man1/skopeo-sync.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;skopeo-list-tags(1)&lt;/code&gt; man page: &lt;a href="https://manpages.ubuntu.com/manpages/noble/man1/skopeo-list-tags.1.html" rel="noopener noreferrer"&gt;https://manpages.ubuntu.com/manpages/noble/man1/skopeo-list-tags.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;containers-transports(5)&lt;/code&gt; man page: &lt;a href="https://manpages.ubuntu.com/manpages/noble/man5/containers-transports.5.html" rel="noopener noreferrer"&gt;https://manpages.ubuntu.com/manpages/noble/man5/containers-transports.5.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;containers-auth.json(5)&lt;/code&gt; man page: &lt;a href="https://manpages.ubuntu.com/manpages/noble/man5/containers-auth.json.5.html" rel="noopener noreferrer"&gt;https://manpages.ubuntu.com/manpages/noble/man5/containers-auth.json.5.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cover image: Wikimedia Commons, Utah Data Center panorama: &lt;a href="https://commons.wikimedia.org/wiki/File:Utah_Data_Center_Panorama_(cropped).jpg" rel="noopener noreferrer"&gt;https://commons.wikimedia.org/wiki/File:Utah_Data_Center_Panorama_(cropped).jpg&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>opensource</category>
      <category>docker</category>
    </item>
    <item>
      <title>Stop Editing `/etc/sudoers` Directly: Practical `sudoers.d` + `visudo` on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sat, 09 May 2026 05:02:35 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-editing-etcsudoers-directly-practical-sudoersd-visudo-on-linux-4ccd</link>
      <guid>https://dev.to/lyraalishaikh/stop-editing-etcsudoers-directly-practical-sudoersd-visudo-on-linux-4ccd</guid>
      <description>&lt;p&gt;When a team needs one extra admin permission on a Linux box, the fastest path is often the messiest one: open &lt;code&gt;/etc/sudoers&lt;/code&gt;, add a line, hope nothing breaks.&lt;/p&gt;

&lt;p&gt;That works right up until you need to review the change, automate it, or recover from a syntax mistake that bricks &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A safer pattern is to leave the main policy file alone and add small, validated drop-ins under &lt;code&gt;sudoers.d&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This guide walks through that workflow with practical examples, syntax checks, and a few easy-to-miss guardrails from the actual &lt;code&gt;sudoers&lt;/code&gt; and &lt;code&gt;visudo&lt;/code&gt; documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;sudoers.d&lt;/code&gt; is the better default
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;sudoers&lt;/code&gt; policy supports an include-directory mechanism, usually via &lt;code&gt;#includedir /etc/sudoers.d&lt;/code&gt;. According to the &lt;code&gt;sudoers&lt;/code&gt; manual, files in that directory are parsed too, but names that end in &lt;code&gt;~&lt;/code&gt; or contain a &lt;code&gt;.&lt;/code&gt; are skipped.&lt;/p&gt;

&lt;p&gt;That makes &lt;code&gt;sudoers.d&lt;/code&gt; useful because you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep the base &lt;code&gt;/etc/sudoers&lt;/code&gt; file package-friendly&lt;/li&gt;
&lt;li&gt;separate app- or team-specific privileges into small files&lt;/li&gt;
&lt;li&gt;validate one candidate rule before installing it&lt;/li&gt;
&lt;li&gt;manage delegated access with configuration management more cleanly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last point matters a lot. A 3-line drop-in is much easier to audit than a hand-edited global policy file full of historical exceptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, confirm your main file includes the directory
&lt;/h2&gt;

&lt;p&gt;On many Debian and Ubuntu systems, the main file already includes it.&lt;/p&gt;

&lt;p&gt;Check with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s1"&gt;'^[#@]includedir'&lt;/span&gt; /etc/sudoers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You are typically looking for something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#includedir /etc/sudoers.d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you do not see an include directory, stop and review your distro defaults before inventing your own layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1: validate with &lt;code&gt;visudo&lt;/code&gt;, not a text editor alone
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;visudo&lt;/code&gt; manual is very clear about why the tool exists: it locks the file against simultaneous edits and checks syntax before saving.&lt;/p&gt;

&lt;p&gt;Even better, it supports &lt;strong&gt;check-only mode&lt;/strong&gt; and an alternate file path, which is exactly what you want for a drop-in workflow.&lt;/p&gt;

&lt;p&gt;The two flags to remember are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-c&lt;/code&gt; or &lt;code&gt;--check&lt;/code&gt; for validation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-f&lt;/code&gt; or &lt;code&gt;--file&lt;/code&gt; for an alternate file path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A safe pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/tmp/90-app-maint &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
Cmnd_Alias APP_MAINT = /usr/bin/systemctl restart myapp.service, /usr/bin/journalctl -u myapp.service -n 200
%deploy ALL=(root) APP_MAINT
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/sbin/visudo &lt;span class="nt"&gt;-cf&lt;/span&gt; /tmp/90-app-maint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a valid file, you should see output like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/tmp/90-app-maint: parsed OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the moment to install it, not before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1: delegate one service restart and log access
&lt;/h2&gt;

&lt;p&gt;A common real-world need is letting a deployment group restart &lt;strong&gt;one&lt;/strong&gt; service and inspect its recent logs without giving them unrestricted root.&lt;/p&gt;

&lt;p&gt;Create a drop-in like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 /etc/sudoers.d

&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/sudoers.d/90-app-maint &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
Cmnd_Alias APP_MAINT = /usr/bin/systemctl restart myapp.service, /usr/bin/journalctl -u myapp.service -n 200
%deploy ALL=(root) APP_MAINT
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:root /etc/sudoers.d/90-app-maint
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0440 /etc/sudoers.d/90-app-maint
&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/sbin/visudo &lt;span class="nt"&gt;-cf&lt;/span&gt; /etc/sudoers.d/90-app-maint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;defines a command alias named &lt;code&gt;APP_MAINT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;allows members of the &lt;code&gt;deploy&lt;/code&gt; group to run those commands as &lt;code&gt;root&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;keeps the permission scope narrow and explicit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To verify the effective access from an allowed account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need to test as a specific user from an admin shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; someuser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example 2: allow package metadata refresh, but not full package installs
&lt;/h2&gt;

&lt;p&gt;Sometimes a user only needs to refresh package metadata or inspect upgrade candidates.&lt;/p&gt;

&lt;p&gt;A narrower drop-in might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/sudoers.d/91-apt-audit &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
Cmnd_Alias APT_AUDIT = /usr/bin/apt update, /usr/bin/apt list --upgradable
%ops ALL=(root) APT_AUDIT
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:root /etc/sudoers.d/91-apt-audit
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0440 /etc/sudoers.d/91-apt-audit
&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/sbin/visudo &lt;span class="nt"&gt;-cf&lt;/span&gt; /etc/sudoers.d/91-apt-audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is intentionally different from granting full package installation rights.&lt;/p&gt;

&lt;p&gt;If you are tempted to add &lt;code&gt;apt install&lt;/code&gt;, &lt;code&gt;apt remove&lt;/code&gt;, wildcard-heavy command patterns, or shell escapes to the same rule, pause and re-scope it. Small delegated actions are the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  File naming and permission gotchas that bite people
&lt;/h2&gt;

&lt;p&gt;A few details from the manual matter more than they look.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Do not put dots in drop-in filenames
&lt;/h3&gt;

&lt;p&gt;Per the &lt;code&gt;sudoers&lt;/code&gt; manual, files in an included directory are skipped if the name ends in &lt;code&gt;~&lt;/code&gt; or contains a &lt;code&gt;.&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/sudoers.d/90-app-maint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/sudoers.d/90-app-maint.conf
/etc/sudoers.d/90-app-maint~
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means editor backup files and “nice-looking” &lt;code&gt;.conf&lt;/code&gt; names can silently fail to load.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Use root ownership and mode &lt;code&gt;0440&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;sudoers&lt;/code&gt; documentation states the default file mode is &lt;code&gt;0440&lt;/code&gt;, readable by owner and group and writable by none. The &lt;code&gt;visudo&lt;/code&gt; manual also documents ownership and permission checks in validation mode.&lt;/p&gt;

&lt;p&gt;A reliable install pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:root /etc/sudoers.d/90-app-maint
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0440 /etc/sudoers.d/90-app-maint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3) Validate after writing, not just before
&lt;/h3&gt;

&lt;p&gt;If your automation writes the file and then changes ownership or mode incorrectly, the syntax may still be fine while the policy remains unusable.&lt;/p&gt;

&lt;p&gt;So validate the installed path too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/sbin/visudo &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to the &lt;code&gt;visudo&lt;/code&gt; manual, check mode against the default sudoers path also checks included files plus ownership and permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safer automation pattern
&lt;/h2&gt;

&lt;p&gt;If you manage hosts with Ansible, shell scripts, or CI-built images, use a staged file plus validation before the final move.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
Cmnd_Alias APP_MAINT = /usr/bin/systemctl restart myapp.service, /usr/bin/journalctl -u myapp.service -n 200
%deploy ALL=(root) APP_MAINT
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/sbin/visudo &lt;span class="nt"&gt;-cf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; root &lt;span class="nt"&gt;-g&lt;/span&gt; root &lt;span class="nt"&gt;-m&lt;/span&gt; 0440 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /etc/sudoers.d/90-app-maint
&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/sbin/visudo &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you three useful properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;syntax is checked before install&lt;/li&gt;
&lt;li&gt;final permissions are enforced during install&lt;/li&gt;
&lt;li&gt;the complete active policy is checked afterward&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What not to do
&lt;/h2&gt;

&lt;p&gt;I would avoid these patterns unless you have a very specific reason:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;editing &lt;code&gt;/etc/sudoers&lt;/code&gt; directly for every small exception&lt;/li&gt;
&lt;li&gt;granting &lt;code&gt;ALL=(ALL:ALL) ALL&lt;/code&gt; to convenience groups&lt;/li&gt;
&lt;li&gt;using wildcards loosely around commands with shell escapes or user-controlled arguments&lt;/li&gt;
&lt;li&gt;storing drop-ins with &lt;code&gt;.conf&lt;/code&gt;, &lt;code&gt;.bak&lt;/code&gt;, or editor backup suffixes&lt;/li&gt;
&lt;li&gt;skipping a full &lt;code&gt;visudo -c&lt;/code&gt; after policy changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a rule looks “temporarily broad”, it usually becomes permanently broad.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick rollback path
&lt;/h2&gt;

&lt;p&gt;If a new drop-in causes confusion, rollback is simple because the change is isolated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mv&lt;/span&gt; /etc/sudoers.d/90-app-maint /root/90-app-maint.disabled
&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/sbin/visudo &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much less stressful than untangling a large hand-edited main file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sudo&lt;/code&gt; policy is one of those things that feels trivial until the day it is not.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;sudoers.d&lt;/code&gt; plus &lt;code&gt;visudo&lt;/code&gt; turns it into something modular, reviewable, and a lot less fragile. For Linux admin work, that is usually the difference between “quick fix” and “clean operational habit.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sudoers&lt;/code&gt; manual: &lt;a href="https://www.sudo.ws/docs/man/sudoers.man/" rel="noopener noreferrer"&gt;https://www.sudo.ws/docs/man/sudoers.man/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;visudo&lt;/code&gt; manual: &lt;a href="https://www.sudo.ws/docs/man/visudo.man/" rel="noopener noreferrer"&gt;https://www.sudo.ws/docs/man/visudo.man/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian &lt;code&gt;sudo&lt;/code&gt; package metadata: &lt;a href="https://packages.debian.org/search?keywords=sudo" rel="noopener noreferrer"&gt;https://packages.debian.org/search?keywords=sudo&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Catch Broken Debian Upgrades Before They Land: Practical `apt-listbugs`</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sat, 09 May 2026 02:03:13 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/catch-broken-debian-upgrades-before-they-land-practical-apt-listbugs-13n</link>
      <guid>https://dev.to/lyraalishaikh/catch-broken-debian-upgrades-before-they-land-practical-apt-listbugs-13n</guid>
      <description>&lt;p&gt;If you run Debian testing, unstable, or just like upgrading early, there is a familiar kind of pain: APT itself works fine, but the package you just pulled in is already known to be broken.&lt;/p&gt;

&lt;p&gt;That is exactly the gap &lt;code&gt;apt-listbugs&lt;/code&gt; tries to close.&lt;/p&gt;

&lt;p&gt;Before APT installs or upgrades packages, &lt;code&gt;apt-listbugs&lt;/code&gt; can query the Debian Bug Tracking System (BTS) for known bugs affecting the versions you are about to install. If it finds bugs that match your configured severity filters, it warns you before the upgrade goes through.&lt;/p&gt;

&lt;p&gt;That makes it especially useful on Debian systems where package freshness matters, but so does not breaking the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;apt-listbugs&lt;/code&gt; actually does
&lt;/h2&gt;

&lt;p&gt;According to the Debian manpage, &lt;code&gt;apt-listbugs&lt;/code&gt; is intended to be invoked before package installation or upgrade so it can query the Debian BTS for bugs that would be introduced by the pending APT action. If matching bugs are found, it can let you continue, abort, or pin affected packages so the risky upgrade is deferred.&lt;/p&gt;

&lt;p&gt;A few details matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The default severity filter is &lt;code&gt;critical,grave,serious&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pinning is &lt;strong&gt;not immediate inside the current APT transaction&lt;/strong&gt;. If you choose to pin, you should abort and then rerun the same APT command.&lt;/li&gt;
&lt;li&gt;Automatically added pins are cleaned up later by the package's daily cron job or systemd timer, once the BTS data shows the issue is fixed or no longer affects the installable version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, this is a pre-upgrade safety rail, not a replacement for testing or backups.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it is most useful
&lt;/h2&gt;

&lt;p&gt;I would reach for &lt;code&gt;apt-listbugs&lt;/code&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you run Debian testing or unstable&lt;/li&gt;
&lt;li&gt;you track fast-moving packages on a workstation or homelab node&lt;/li&gt;
&lt;li&gt;you want APT to stop and show known release-critical issues before changing the system&lt;/li&gt;
&lt;li&gt;you prefer a quick BTS sanity check over reading bug trackers by hand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you mainly run stable and only take normal security updates, it may trigger less often, but it can still be a worthwhile guardrail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;apt-listbugs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can confirm the package exists in current Debian repositories with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy apt-listbugs
apt-cache show apt-listbugs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On this host, &lt;code&gt;apt-cache show&lt;/code&gt; reports the package description as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;tool which lists critical bugs before each APT installation&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That matches the Debian documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use it for one-off inspection first
&lt;/h2&gt;

&lt;p&gt;Before wiring it into your normal upgrade flow, try a manual query.&lt;/p&gt;

&lt;p&gt;To inspect known bugs for a package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-listbugs list openssh-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To inspect a specific version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-listbugs list openssh-server/1:9.7p1-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also include an architecture qualifier, although the Debian BTS itself does not distinguish bugs by architecture in the way package metadata does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-listbugs list openssh-server:amd64/1:9.7p1-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a good low-risk way to understand the output before you let it interrupt real upgrades.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let it run during normal APT upgrades
&lt;/h2&gt;

&lt;p&gt;The package is designed to be invoked automatically by APT using a Pre-Install-Pkgs hook. After installation, that integration is normally handled for you.&lt;/p&gt;

&lt;p&gt;Once enabled, a regular upgrade looks the same from your side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt full-upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;apt-listbugs&lt;/code&gt; finds matching bugs, it will stop before package installation and present the bug list. From there, your safest options are usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;abort the upgrade&lt;/li&gt;
&lt;li&gt;pin the affected package and then rerun the command&lt;/li&gt;
&lt;li&gt;continue only if you understand the impact and accept the risk&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last option is real, but I would treat it like bypassing a smoke alarm. Sometimes you know why it is noisy. Usually, you should investigate first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tune the severity threshold
&lt;/h2&gt;

&lt;p&gt;By default, &lt;code&gt;apt-listbugs&lt;/code&gt; shows bugs with these Debian severities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;critical&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grave&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;serious&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Debian classifies those as release-critical severities. In practice, they cover issues such as system breakage, severe package unusability, serious data loss risk, security holes, or defects that make a package unsuitable for release.&lt;/p&gt;

&lt;p&gt;If you want broader visibility, you can add &lt;code&gt;important&lt;/code&gt; too.&lt;/p&gt;

&lt;p&gt;Create a small APT config snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 /etc/apt/apt.conf.d
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/apt.conf.d/90apt-listbugs-local &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
AptListbugs::Severities "critical,grave,serious,important";
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That keeps the default high-signal behavior, while widening the net a little.&lt;/p&gt;

&lt;p&gt;If you want to focus on a specific Debian release when evaluating bugs, you can also set &lt;code&gt;AptListbugs::DistroRelease&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/apt.conf.d/90apt-listbugs-release &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
AptListbugs::DistroRelease "testing";
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other accepted values include real Debian codenames, &lt;code&gt;unstable&lt;/code&gt;, &lt;code&gt;stable&lt;/code&gt;, &lt;code&gt;oldstable&lt;/code&gt;, or &lt;code&gt;ANY&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filter by tag when you care about a specific class of breakage
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;apt-listbugs&lt;/code&gt; can also filter by BTS tags. For example, if you only want to inspect bugs that are both confirmed and related to localization in a manual check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-listbugs &lt;span class="nt"&gt;-T&lt;/span&gt; confirmed,l10n list some-package
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is more niche than severity filtering, but it is useful to know the feature exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the pinning workflow
&lt;/h2&gt;

&lt;p&gt;This part is easy to miss.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;apt-listbugs&lt;/code&gt; offers to pin a risky package, the pin is written for future APT runs, but it does &lt;strong&gt;not&lt;/strong&gt; retroactively change the already running transaction. The manpage is explicit about this: if you choose to pin, you should abort the current install or upgrade, then rerun the same APT command.&lt;/p&gt;

&lt;p&gt;A practical workflow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt full-upgrade
&lt;span class="c"&gt;# apt-listbugs warns about package foo&lt;/span&gt;
&lt;span class="c"&gt;# choose to pin / defer&lt;/span&gt;
&lt;span class="c"&gt;# abort the current upgrade&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;apt full-upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The automatically managed pin file is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/apt/preferences.d/apt-listbugs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You normally should not edit that file by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ignore known exceptions carefully
&lt;/h2&gt;

&lt;p&gt;There are two built-in ignore paths documented by the package:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;automatic ignore list: &lt;code&gt;/var/lib/apt-listbugs/ignore_bugs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;manual ignore list: &lt;code&gt;/etc/apt/listbugs/ignore_bugs&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you deliberately accept a specific bug, the manual file is the cleaner long-term place to document that choice.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 /etc/apt/listbugs
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/apt/listbugs/ignore_bugs &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
# Ignore bug 123456 for this host until upstream fix lands
123456
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this sparingly. If everything becomes an ignore, the guardrail is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Good defaults for noninteractive environments
&lt;/h2&gt;

&lt;p&gt;The manpage documents behavior for noninteractive use too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-F&lt;/code&gt; can force pinning without prompt&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-N&lt;/code&gt; disables automatic pinning&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-y&lt;/code&gt; assumes yes to all questions, including continuing when bugs or errors appear&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-n&lt;/code&gt; assumes no and aborts when bugs or errors appear&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For CI or automation, the safest posture is usually to &lt;strong&gt;fail closed&lt;/strong&gt;, not fail open.&lt;/p&gt;

&lt;p&gt;A one-off explicit example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-listbugs &lt;span class="nt"&gt;-n&lt;/span&gt; list openssh-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For unattended package operations, review the package behavior carefully before adding automation flags. In most cases, silently continuing through known bug warnings is the wrong trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check that the cleanup path exists
&lt;/h2&gt;

&lt;p&gt;The package documentation says automatically added pins are removed later by a daily cron job or an equivalent systemd timer. On a systemd-based Debian host, you can inspect related installed units with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl list-unit-files | &lt;span class="nb"&gt;grep &lt;/span&gt;apt-listbugs
systemctl list-timers &lt;span class="nt"&gt;--all&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;apt-listbugs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you rely on automatic pin cleanup, it is worth verifying that path once instead of assuming it is there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;apt-listbugs&lt;/code&gt; is not
&lt;/h2&gt;

&lt;p&gt;It helps to keep the boundaries clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is &lt;strong&gt;not&lt;/strong&gt; a vulnerability scanner.&lt;/li&gt;
&lt;li&gt;It is &lt;strong&gt;not&lt;/strong&gt; a package integrity checker.&lt;/li&gt;
&lt;li&gt;It is &lt;strong&gt;not&lt;/strong&gt; a substitute for snapshots or backups.&lt;/li&gt;
&lt;li&gt;It is &lt;strong&gt;not&lt;/strong&gt; a guarantee that an upgrade is safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is a very practical preflight check against &lt;strong&gt;known&lt;/strong&gt; Debian bug reports for the versions you are about to pull in.&lt;/p&gt;

&lt;p&gt;That is narrower than magic, but broader than guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple, sensible workflow
&lt;/h2&gt;

&lt;p&gt;If you want a boringly reliable setup, this is a good start:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install &lt;code&gt;apt-listbugs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Keep the default &lt;code&gt;critical,grave,serious&lt;/code&gt; filter, or add &lt;code&gt;important&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run upgrades manually on important systems&lt;/li&gt;
&lt;li&gt;Abort and investigate when it flags a package you care about&lt;/li&gt;
&lt;li&gt;Let temporary pins defer known-bad upgrades instead of brute-forcing through them&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives you a fast feedback loop without turning every package upgrade into a research project.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debian manpage, &lt;code&gt;apt-listbugs(1)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/testing/apt-listbugs/apt-listbugs.1.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/apt-listbugs/apt-listbugs.1.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian BTS severity definitions: &lt;a href="https://www.debian.org/Bugs/Developer" rel="noopener noreferrer"&gt;https://www.debian.org/Bugs/Developer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian package metadata for &lt;code&gt;apt-listbugs&lt;/code&gt;: &lt;a href="https://packages.debian.org/apt-listbugs" rel="noopener noreferrer"&gt;https://packages.debian.org/apt-listbugs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Project homepage on Salsa: &lt;a href="https://salsa.debian.org/frx-guest/apt-listbugs" rel="noopener noreferrer"&gt;https://salsa.debian.org/frx-guest/apt-listbugs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>debian</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Letting SSD Performance Rot: Practical `fstrim.timer` on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Thu, 07 May 2026 05:03:38 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-letting-ssd-performance-rot-practical-fstrimtimer-on-linux-3ido</link>
      <guid>https://dev.to/lyraalishaikh/stop-letting-ssd-performance-rot-practical-fstrimtimer-on-linux-3ido</guid>
      <description>&lt;h1&gt;
  
  
  Stop Letting SSD Performance Rot: Practical &lt;code&gt;fstrim.timer&lt;/code&gt; on Linux
&lt;/h1&gt;

&lt;p&gt;If your Linux system lives on SSDs, virtual disks backed by SSD storage, or thin-provisioned volumes, TRIM is one of those boring maintenance jobs that is easy to forget and annoying to debug later.&lt;/p&gt;

&lt;p&gt;The good news is that modern Linux already has a sensible answer: &lt;code&gt;fstrim.timer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This post shows how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;verify that discard is actually supported&lt;/li&gt;
&lt;li&gt;check whether &lt;code&gt;fstrim.timer&lt;/code&gt; is already enabled&lt;/li&gt;
&lt;li&gt;enable a weekly TRIM schedule safely&lt;/li&gt;
&lt;li&gt;run a manual trim when you need one&lt;/li&gt;
&lt;li&gt;avoid a common mistake, mounting everything with continuous &lt;code&gt;discard&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am focusing on the practical path here, not storage folklore.&lt;/p&gt;

&lt;h2&gt;
  
  
  What TRIM actually does
&lt;/h2&gt;

&lt;p&gt;When files are deleted, the filesystem knows those blocks are free, but the SSD may not know that immediately. TRIM, exposed on Linux through &lt;code&gt;fstrim&lt;/code&gt;, tells the underlying storage which unused blocks can be discarded.&lt;/p&gt;

&lt;p&gt;That matters for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSD performance consistency&lt;/li&gt;
&lt;li&gt;some thin-provisioned storage backends&lt;/li&gt;
&lt;li&gt;reclaiming space more accurately on certain virtualized platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;fstrim(8)&lt;/code&gt; manual describes it plainly: &lt;code&gt;fstrim&lt;/code&gt; discards unused blocks on a mounted filesystem, and it is useful for SSDs and thin-provisioned storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;fstrim.timer&lt;/code&gt; is usually better than &lt;code&gt;discard&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A lot of guides jump straight to adding &lt;code&gt;discard&lt;/code&gt; to mount options. That is not my default recommendation.&lt;/p&gt;

&lt;p&gt;The upstream &lt;code&gt;fstrim(8)&lt;/code&gt; man page explicitly warns that running TRIM frequently, or using &lt;code&gt;mount -o discard&lt;/code&gt;, may negatively affect poor-quality SSDs, and says that for most desktop and server systems, once a week is sufficient.&lt;/p&gt;

&lt;p&gt;That lines up with what many distributions ship today. On this host, the packaged timer is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /usr/lib/systemd/system/fstrim.timer
&lt;/span&gt;&lt;span class="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;weekly&lt;/span&gt;
&lt;span class="py"&gt;AccuracySec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1h&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;RandomizedDelaySec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;100min&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a very reasonable default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;weekly&lt;/code&gt; keeps the cadence modest&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Persistent=true&lt;/code&gt; means a missed run is caught up after boot&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RandomizedDelaySec=&lt;/code&gt; spreads load across machines&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Check whether your storage advertises discard support
&lt;/h2&gt;

&lt;p&gt;Start with &lt;code&gt;lsblk -D&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsblk &lt;span class="nt"&gt;-D&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME    DISC-ALN DISC-GRAN DISC-MAX DISC-ZERO
vda            0      512B       2G         0
├─vda1         0      512B       2G         0
├─vda14        0      512B       2G         0
└─vda15        0      512B       2G         0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What to look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DISC-GRAN&lt;/code&gt; and &lt;code&gt;DISC-MAX&lt;/code&gt; should not both be &lt;code&gt;0B&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;non-zero discard values suggest the block device can accept discard/TRIM requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also inspect mounted filesystems with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;findmnt &lt;span class="nt"&gt;-D&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a quick view of mounted filesystems and discard-related device characteristics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Check whether the timer already exists and is active
&lt;/h2&gt;

&lt;p&gt;Many systems already ship this enabled. Check before changing anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status fstrim.timer
systemctl list-timers &lt;span class="nt"&gt;--all&lt;/span&gt; fstrim.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEXT                          LEFT LAST                            PASSED UNIT         ACTIVATES
Mon 2026-05-11 00:46:45 UTC 3 days Mon 2026-05-04 01:29:37 UTC 3 days ago fstrim.timer fstrim.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see a next run scheduled, you may already be done.&lt;/p&gt;

&lt;p&gt;You can inspect the packaged service and timer definitions too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nb"&gt;cat &lt;/span&gt;fstrim.timer fstrim.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On this machine, the service executes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/sbin/fstrim --listed-in /etc/fstab:/proc/self/mountinfo --verbose --quiet-unsupported&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a nice detail. It trims filesystems listed in &lt;code&gt;fstab&lt;/code&gt; or mount info, prints useful byte counts, and suppresses noisy errors for unsupported filesystems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Enable and start the timer
&lt;/h2&gt;

&lt;p&gt;If the timer is installed but inactive, enable it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; fstrim.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then confirm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status fstrim.timer
systemctl list-timers &lt;span class="nt"&gt;--all&lt;/span&gt; fstrim.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your distro uses vendor presets that already enabled it, this command is harmless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Run a one-time TRIM manually
&lt;/h2&gt;

&lt;p&gt;Sometimes you do not want to wait for the weekly run, especially after a big cleanup, VM image shrink, or container/image pruning session.&lt;/p&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;fstrim &lt;span class="nt"&gt;-av&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the flags mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-a&lt;/code&gt; trims all mounted filesystems that support the operation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-v&lt;/code&gt; shows how many bytes were passed down for potential discard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example output usually looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/: 38.2 GiB (41016926208 bytes) trimmed
/boot/efi: 97.5 MiB (102236160 bytes) trimmed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One subtle but important note from &lt;code&gt;fstrim(8)&lt;/code&gt;: the reported byte count is the amount passed down for potential discard, not a guarantee that the device physically discarded every byte right then. That is normal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Verify the last run and logs
&lt;/h2&gt;

&lt;p&gt;After either a manual run or a timer-driven run, check the service logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status fstrim.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; fstrim.service &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"7 days ago"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you two useful things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether the service actually succeeded&lt;/li&gt;
&lt;li&gt;which mountpoints were trimmed and how much was reported&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When you should not expect this to work
&lt;/h2&gt;

&lt;p&gt;A few cases trip people up:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. You are inside a container
&lt;/h3&gt;

&lt;p&gt;On this host, the packaged units contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;ConditionVirtualization&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;!container&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;fstrim.timer&lt;/code&gt; and &lt;code&gt;fstrim.service&lt;/code&gt; are intentionally skipped in containers. That is correct, because discard belongs to the host or VM layer that owns the block device.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The filesystem or block layer does not support discard
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;fstrim(8)&lt;/code&gt; man page notes that unsupported filesystems and read-only cases are ignored when trimming all filesystems. If your storage stack does not pass discard through, no amount of systemd tweaking will fix that.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. You are using old advice that assumes &lt;code&gt;discard&lt;/code&gt; must be mounted live
&lt;/h3&gt;

&lt;p&gt;That is not generally true anymore. Weekly batched TRIM is the upstream-recommended default for most systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safe baseline for most Linux machines
&lt;/h2&gt;

&lt;p&gt;If I were setting this up on a normal workstation, home server, or VM backed by SSD storage, my baseline would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsblk &lt;span class="nt"&gt;-D&lt;/span&gt;
findmnt &lt;span class="nt"&gt;-D&lt;/span&gt;
systemctl status fstrim.timer &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; fstrim.timer
&lt;span class="nb"&gt;sudo &lt;/span&gt;fstrim &lt;span class="nt"&gt;-av&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; fstrim.service &lt;span class="nt"&gt;--since&lt;/span&gt; today
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;capability check&lt;/li&gt;
&lt;li&gt;timer state&lt;/li&gt;
&lt;li&gt;scheduled ongoing maintenance&lt;/li&gt;
&lt;li&gt;one immediate cleanup run&lt;/li&gt;
&lt;li&gt;a verification trail&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Should you add &lt;code&gt;discard&lt;/code&gt; to &lt;code&gt;/etc/fstab&lt;/code&gt; anyway?
&lt;/h2&gt;

&lt;p&gt;Usually, no.&lt;/p&gt;

&lt;p&gt;I would only consider continuous &lt;code&gt;discard&lt;/code&gt; if you have a specific storage stack that benefits from immediate reclamation and you have tested the performance tradeoff. For general-purpose Linux systems, the weekly timer is the cleaner default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;fstrim.timer&lt;/code&gt; is one of those rare Linux defaults that is both boring and correct.&lt;/p&gt;

&lt;p&gt;If your storage supports discard, enable the timer, verify it once, and move on with your life. That is better than cargo-culting &lt;code&gt;discard&lt;/code&gt; into every mount option and hoping for the best.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fstrim(8)&lt;/code&gt; man page: &lt;a href="https://man7.org/linux/man-pages/man8/fstrim.8.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man8/fstrim.8.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;systemd &lt;code&gt;fstrim.timer&lt;/code&gt; manual: &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/fstrim.timer.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/latest/fstrim.timer.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fedora change note on enabling &lt;code&gt;fstrim.timer&lt;/code&gt;: &lt;a href="https://fedoraproject.org/wiki/Changes/EnableFSTrimTimer" rel="noopener noreferrer"&gt;https://fedoraproject.org/wiki/Changes/EnableFSTrimTimer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lsblk(8)&lt;/code&gt; man page: &lt;a href="https://man7.org/linux/man-pages/man8/lsblk.8.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man8/lsblk.8.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;findmnt(8)&lt;/code&gt; man page: &lt;a href="https://man7.org/linux/man-pages/man8/findmnt.8.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man8/findmnt.8.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>opensource</category>
      <category>storage</category>
    </item>
    <item>
      <title>Stop Babysitting Container Updates: Practical Podman Auto-Updates with Quadlet, Health Checks, and Rollback</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Wed, 06 May 2026 05:03:22 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-babysitting-container-updates-practical-podman-auto-updates-with-quadlet-health-checks-and-22dd</link>
      <guid>https://dev.to/lyraalishaikh/stop-babysitting-container-updates-practical-podman-auto-updates-with-quadlet-health-checks-and-22dd</guid>
      <description>&lt;p&gt;If you run long-lived containers on Linux, "just pull the new image and restart it later" usually turns into "I'll do it this weekend". That is how drift sneaks in.&lt;/p&gt;

&lt;p&gt;Podman already has a cleaner answer. Its auto-update flow can check for a new image, pull it, and restart the corresponding systemd unit. Better yet, it can roll back if the restart fails.&lt;/p&gt;

&lt;p&gt;The catch is that you need to wire it up the right way. In practice, that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;run the container through a systemd unit&lt;/li&gt;
&lt;li&gt;use a fully qualified image reference for registry-based updates&lt;/li&gt;
&lt;li&gt;add a readiness signal so rollback can detect bad starts reliably&lt;/li&gt;
&lt;li&gt;add a health check so broken containers do not look healthy by accident&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a practical setup for a rootless container managed with Quadlet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Podman auto-update actually does
&lt;/h2&gt;

&lt;p&gt;According to &lt;code&gt;podman-auto-update(1)&lt;/code&gt;, Podman can update containers that run inside systemd units. It checks containers marked for auto-update, pulls a newer image when available, and restarts the unit that owns the container.&lt;/p&gt;

&lt;p&gt;It supports two policies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;registry&lt;/code&gt;, which checks the remote registry for a newer digest&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;local&lt;/code&gt;, which compares the container image to a newer image already present in local storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most people running pulled images, &lt;code&gt;registry&lt;/code&gt; is the useful one.&lt;/p&gt;

&lt;p&gt;One important limitation from the docs: &lt;code&gt;registry&lt;/code&gt; requires a fully qualified image name like &lt;code&gt;docker.io/library/nginx:1.27-alpine&lt;/code&gt; or &lt;code&gt;quay.io/yourorg/app:latest&lt;/code&gt;. A short name is not enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Quadlet is the easiest way to do this
&lt;/h2&gt;

&lt;p&gt;Quadlet lets you define Podman workloads as &lt;code&gt;.container&lt;/code&gt; files that systemd turns into regular services at daemon reload time. Podman documents rootless Quadlet search paths such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.config/containers/systemd/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$XDG_RUNTIME_DIR/containers/systemd/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes it a good fit for auto-updates, because Podman can restart the generated systemd service after pulling a new image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: a rootless Quadlet with auto-update enabled
&lt;/h2&gt;

&lt;p&gt;Create the Quadlet directory if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/containers/systemd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now create &lt;code&gt;~/.config/containers/systemd/whoami.container&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Traefik whoami demo container&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;ContainerName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;whoami&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/traefik/whoami:v1.10.1&lt;/span&gt;
&lt;span class="py"&gt;AutoUpdate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;
&lt;span class="py"&gt;PublishPort&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:8080:80&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;180&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then load and start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; whoami.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that it is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status whoami.service
podman ps &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;whoami
&lt;/span&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; http://127.0.0.1:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A more realistic readiness + health-check pattern
&lt;/h2&gt;

&lt;p&gt;The quick example above proves the wiring, but it does not give systemd much insight into application health.&lt;/p&gt;

&lt;p&gt;Rollback works best when systemd can tell whether the new container actually became ready. Podman documents that &lt;code&gt;podman auto-update --rollback&lt;/code&gt; is most reliable when the container sends the &lt;code&gt;READY=1&lt;/code&gt; notification through sdnotify.&lt;/p&gt;

&lt;p&gt;For Quadlet, &lt;code&gt;Notify=true&lt;/code&gt; maps to &lt;code&gt;--sdnotify container&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That means your application should emit readiness only when it is genuinely ready to serve traffic. One straightforward pattern is a small wrapper entrypoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Containerfile
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; curl systemd &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; flask
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; app.py /app/app.py&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; entrypoint.sh /app/entrypoint.sh&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /app/entrypoint.sh
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["/app/entrypoint.sh"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  app.py
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/healthz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;healthz&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello from podman auto-update&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  entrypoint.sh
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eu&lt;/span&gt;

python /app/app.py &amp;amp;
&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;_ &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 30&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if &lt;/span&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; http://127.0.0.1:8000/healthz &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;systemd-notify &lt;span class="nt"&gt;--ready&lt;/span&gt;
    &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"application failed readiness check"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
exit &lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the matching Quadlet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;ContainerName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;demo-api&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/yourname/demo-api:1.0.0&lt;/span&gt;
&lt;span class="py"&gt;AutoUpdate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;
&lt;span class="py"&gt;Notify&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;PublishPort&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:8000:8000&lt;/span&gt;
&lt;span class="py"&gt;HealthCmd&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;curl -fsS http://127.0.0.1:8000/healthz || exit 1&lt;/span&gt;
&lt;span class="py"&gt;HealthInterval&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30s&lt;/span&gt;
&lt;span class="py"&gt;HealthTimeout&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5s&lt;/span&gt;
&lt;span class="py"&gt;HealthRetries&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;HealthOnFailure&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;kill&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;180&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you two useful signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-notify --ready&lt;/code&gt; tells systemd the service really started&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HealthCmd=&lt;/code&gt; keeps probing after startup and can kill the container if it becomes unhealthy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination is much safer than "container process started, so I guess the deploy worked".&lt;/p&gt;

&lt;h2&gt;
  
  
  Test before you trust it
&lt;/h2&gt;

&lt;p&gt;Before enabling unattended updates, do a dry run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman auto-update &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or format the output to focus on what matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman auto-update &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Unit}} {{.Image}} {{.Updated}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Podman sees a newer image, the &lt;code&gt;Updated&lt;/code&gt; field shows &lt;code&gt;pending&lt;/code&gt; in dry-run mode.&lt;/p&gt;

&lt;p&gt;You can trigger an update manually as a controlled test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; start podman-auto-update.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect what happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; podman-auto-update.service &lt;span class="nt"&gt;-n&lt;/span&gt; 100 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; whoami.service &lt;span class="nt"&gt;-n&lt;/span&gt; 100 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Change the schedule instead of accepting midnight
&lt;/h2&gt;

&lt;p&gt;Podman ships &lt;code&gt;podman-auto-update.timer&lt;/code&gt;, and the docs say it triggers daily at midnight by default.&lt;/p&gt;

&lt;p&gt;If that is a bad maintenance window for you, override the timer instead of editing vendor files in place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/systemd/user/podman-auto-update.timer.d
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.config/systemd/user/podman-auto-update.timer.d/override.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Timer]
OnCalendar=
OnCalendar=Sat *-*-* 03:15:00
Persistent=true
RandomizedDelaySec=15m
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; restart podman-auto-update.timer
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; list-timers podman-auto-update.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why the empty &lt;code&gt;OnCalendar=&lt;/code&gt; first? In systemd drop-ins, that clears the original value before you set a new one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Persistent=true&lt;/code&gt; is useful on machines that are not always on, because missed runs get caught up the next time the timer becomes active.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registry auth matters for private images
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;podman-auto-update(1)&lt;/code&gt; documents that registry auth is read from the normal Podman auth file path, typically &lt;code&gt;${XDG_RUNTIME_DIR}/containers/auth.json&lt;/code&gt; on Linux, with &lt;code&gt;$HOME/.docker/config.json&lt;/code&gt; as a fallback.&lt;/p&gt;

&lt;p&gt;So if your image is private, log in first as the same user that owns the rootless service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman login docker.io
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need a non-default auth file, the docs also support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;podman auto-update --authfile /path/to/auth.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;io.containers.autoupdate.authfile&lt;/code&gt; label&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;REGISTRY_AUTH_FILE&lt;/code&gt; environment variable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common mistakes that break auto-updates
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Using a short image name
&lt;/h3&gt;

&lt;p&gt;This often fails for &lt;code&gt;registry&lt;/code&gt; updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;nginx:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a fully qualified reference instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/library/nginx:1.27-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2) Running the container outside systemd
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;podman auto-update&lt;/code&gt; updates the systemd unit that owns the container. If you started the container with an ad hoc &lt;code&gt;podman run -d ...&lt;/code&gt;, there is no systemd unit for Podman to restart.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Trusting &lt;code&gt;latest&lt;/code&gt; without a rollback path
&lt;/h3&gt;

&lt;p&gt;If you want automatic pulls, automatic rollback is not optional in spirit, even though it is enabled by default in &lt;code&gt;podman auto-update&lt;/code&gt;. Pair it with readiness notifications so Podman can tell the difference between "started" and "working".&lt;/p&gt;

&lt;h3&gt;
  
  
  4) No health check
&lt;/h3&gt;

&lt;p&gt;A process can stay alive while the application is unusable. &lt;code&gt;HealthCmd=&lt;/code&gt; and friends give you an ongoing signal after startup.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick verification checklist
&lt;/h2&gt;

&lt;p&gt;After setup, I like to verify these points:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;cat &lt;/span&gt;whoami.service
podman inspect &lt;span class="nb"&gt;whoami&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Config.Labels}}'&lt;/span&gt;
podman auto-update &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Unit}} {{.Policy}} {{.Updated}}'&lt;/span&gt;
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status podman-auto-update.timer
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; list-timers podman-auto-update.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should confirm that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the generated service exists&lt;/li&gt;
&lt;li&gt;the container carries the auto-update policy&lt;/li&gt;
&lt;li&gt;dry run works cleanly&lt;/li&gt;
&lt;li&gt;the timer is active on the schedule you expect&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to use &lt;code&gt;local&lt;/code&gt; instead of &lt;code&gt;registry&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;local&lt;/code&gt; is useful when another workflow places newer images into local storage first, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a CI job pre-pulls or pre-loads images&lt;/li&gt;
&lt;li&gt;you import signed images into an offline host&lt;/li&gt;
&lt;li&gt;you promote images between local stores before restart&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In that model, &lt;code&gt;podman auto-update&lt;/code&gt; becomes a restart controller instead of a registry poller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;Podman auto-updates are good, but they become genuinely production-friendly when you add the missing pieces around them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Quadlet for clean systemd ownership&lt;/li&gt;
&lt;li&gt;fully qualified image names&lt;/li&gt;
&lt;li&gt;health checks&lt;/li&gt;
&lt;li&gt;readiness notifications&lt;/li&gt;
&lt;li&gt;a deliberate timer schedule&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gets you much closer to "safe unattended updates" instead of "automatic surprises".&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Podman documentation, &lt;code&gt;podman-auto-update(1)&lt;/code&gt;: &lt;a href="https://docs.podman.io/en/stable/markdown/podman-auto-update.1.html" rel="noopener noreferrer"&gt;https://docs.podman.io/en/stable/markdown/podman-auto-update.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Podman documentation, &lt;code&gt;podman-systemd.unit(5)&lt;/code&gt;: &lt;a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html" rel="noopener noreferrer"&gt;https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Podman documentation, &lt;code&gt;podman-container.unit(5)&lt;/code&gt;: &lt;a href="https://docs.podman.io/en/latest/markdown/podman-container.unit.5.html" rel="noopener noreferrer"&gt;https://docs.podman.io/en/latest/markdown/podman-container.unit.5.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;systemd documentation, &lt;code&gt;systemd.time(7)&lt;/code&gt;: &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;systemd documentation, &lt;code&gt;systemd.timer(5)&lt;/code&gt;: &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>podman</category>
      <category>opensource</category>
      <category>automation</category>
    </item>
    <item>
      <title>Stop Letting `apt autoremove` Surprise You: Practical `apt-mark` for Debian and Ubuntu</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Tue, 05 May 2026 05:02:31 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-letting-apt-autoremove-surprise-you-practical-apt-mark-for-debian-and-ubuntu-3fmj</link>
      <guid>https://dev.to/lyraalishaikh/stop-letting-apt-autoremove-surprise-you-practical-apt-mark-for-debian-and-ubuntu-3fmj</guid>
      <description>&lt;h1&gt;
  
  
  Stop Letting &lt;code&gt;apt autoremove&lt;/code&gt; Surprise You: Practical &lt;code&gt;apt-mark&lt;/code&gt; for Debian and Ubuntu
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;apt autoremove&lt;/code&gt; is useful, but a lot of Linux admins treat it a little like a haunted button.&lt;/p&gt;

&lt;p&gt;You know it is supposed to remove packages that were installed only as dependencies and are no longer needed. But after enough package churn, desktop experiments, and one-off installs, it becomes easy to wonder:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why is APT trying to remove &lt;em&gt;that&lt;/em&gt; package?&lt;/li&gt;
&lt;li&gt;Why is this dependency still hanging around?&lt;/li&gt;
&lt;li&gt;How do I keep a package I care about from getting swept up later?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answer is usually not guesswork. It is &lt;code&gt;apt-mark&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This article is a practical guide to the package state APT uses behind the scenes, how &lt;code&gt;manual&lt;/code&gt; and &lt;code&gt;auto&lt;/code&gt; marks affect &lt;code&gt;autoremove&lt;/code&gt;, and a safe workflow for cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;apt-mark&lt;/code&gt; actually controls
&lt;/h2&gt;

&lt;p&gt;When you explicitly install a package, APT marks it as &lt;strong&gt;manually installed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When APT installs extra packages only to satisfy dependencies, it marks those as &lt;strong&gt;automatically installed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;According to the &lt;code&gt;apt-mark(8)&lt;/code&gt; manual, once an automatically installed package is no longer depended on by any manually installed package, it is considered no longer needed and tools like &lt;code&gt;apt-get autoremove&lt;/code&gt; will suggest removing it.&lt;/p&gt;

&lt;p&gt;That is the key model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;manual&lt;/strong&gt; means “keep this unless I remove it myself”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;auto&lt;/strong&gt; means “this exists to support something else, so remove it when nothing manual needs it”&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this matters in real life
&lt;/h2&gt;

&lt;p&gt;A few common situations break the simple mental model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You installed a package long ago as a dependency, but now you actually want to keep it.&lt;/li&gt;
&lt;li&gt;You installed a metapackage, then later removed it, leaving behind a pile of dependencies.&lt;/li&gt;
&lt;li&gt;You used a package temporarily for testing and want APT to clean it up naturally later.&lt;/li&gt;
&lt;li&gt;You are afraid to run &lt;code&gt;autoremove&lt;/code&gt; because you are not sure whether package state still reflects reality.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;apt-mark&lt;/code&gt; is the tool for all four.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect your current package state
&lt;/h2&gt;

&lt;p&gt;Start by seeing what APT believes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Show manually installed packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showmanual
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Show automatically installed packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showauto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to check one package directly, filter it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showmanual | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^curl$'&lt;/span&gt;
apt-mark showauto | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^curl$'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the package appears in &lt;code&gt;showmanual&lt;/code&gt;, APT will not consider it removable just because it became a leaf dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preview what &lt;code&gt;autoremove&lt;/code&gt; would do
&lt;/h2&gt;

&lt;p&gt;Before changing anything, simulate the cleanup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nt"&gt;-s&lt;/span&gt; autoremove
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-s&lt;/code&gt; flag runs a simulation, which is the safest first check before any cleanup.&lt;/p&gt;

&lt;p&gt;On my host, a dry run currently reports no removals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NOTE: This is only a simulation!
Reading package lists...
Building dependency tree...
Reading state information...
0 upgraded, 0 newly installed, 0 to remove and 2 not upgraded.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is boring, which is exactly what you want from a safe preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect a package from future autoremove
&lt;/h2&gt;

&lt;p&gt;If there is a package you want to keep even if nothing else depends on it, mark it as manual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark manual tmux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;APT will treat it as explicitly desired from that point forward.&lt;/p&gt;

&lt;p&gt;This is especially useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CLI tools you use directly&lt;/li&gt;
&lt;li&gt;troubleshooting packages installed during incident response&lt;/li&gt;
&lt;li&gt;libraries or helpers you intentionally keep for local scripts&lt;/li&gt;
&lt;li&gt;desktop utilities that were originally pulled in indirectly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can verify the change immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showmanual | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^tmux$'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tell APT a package is fair game for cleanup
&lt;/h2&gt;

&lt;p&gt;If you installed something temporarily and want APT to remove it later when nothing needs it, mark it as automatic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark auto imagemagick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That does &lt;strong&gt;not&lt;/strong&gt; instantly remove the package.&lt;/p&gt;

&lt;p&gt;It only changes its state. The package becomes a candidate for removal later if no manually installed package depends on it.&lt;/p&gt;

&lt;p&gt;Then preview the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nt"&gt;-s&lt;/span&gt; autoremove
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the plan looks correct, run the real cleanup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get autoremove
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if you also want old config files purged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get autoremove &lt;span class="nt"&gt;--purge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A safe cleanup workflow
&lt;/h2&gt;

&lt;p&gt;Here is the workflow I trust on Debian and Ubuntu systems:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Review the candidate list
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nt"&gt;-s&lt;/span&gt; autoremove
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Rescue anything you want to keep
&lt;/h3&gt;

&lt;p&gt;If a package appears in the simulated removal list but you actually want it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark manual PACKAGE_NAME
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Re-run the simulation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nt"&gt;-s&lt;/span&gt; autoremove
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Only then do the real cleanup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get autoremove &lt;span class="nt"&gt;--purge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids the two usual mistakes: cleaning blindly, or never cleaning at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical example: cleaning up after a temporary install
&lt;/h2&gt;

&lt;p&gt;Imagine you temporarily installed a package for a task, and now you want the system to forget it unless something else still needs it.&lt;/p&gt;

&lt;p&gt;Mark it automatic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark auto jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check whether APT now sees it as auto-installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showauto | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^jq$'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If nothing manual depends on it anymore, a future &lt;code&gt;autoremove&lt;/code&gt; can clean it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metapackages and &lt;code&gt;minimize-manual&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;APT also provides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark minimize-manual
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per the &lt;code&gt;apt-mark(8)&lt;/code&gt; manual, this marks transitive dependencies of metapackages as automatically installed. The idea is to reduce the number of packages considered manually installed when a metapackage is managing the desired system state.&lt;/p&gt;

&lt;p&gt;This is useful, but it is not where I would start unless you already understand how your system was built, especially on servers with long upgrade histories or desktops with a lot of role changes.&lt;/p&gt;

&lt;p&gt;For most people, reviewing &lt;code&gt;autoremove&lt;/code&gt; with a simulation and using targeted &lt;code&gt;manual&lt;/code&gt; or &lt;code&gt;auto&lt;/code&gt; marks is the safer first move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where APT stores this state
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;apt-mark(8)&lt;/code&gt; documents the auto-installed package state in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/lib/apt/extended_states
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You usually should not edit that file directly. But it is useful to know this state is explicit and tracked, not magic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;apt-mark&lt;/code&gt; is not
&lt;/h2&gt;

&lt;p&gt;A quick boundary check helps avoid confusion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apt-mark manual/auto&lt;/code&gt; controls package install state used by &lt;code&gt;autoremove&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apt-mark hold&lt;/code&gt; prevents upgrades, installs, or removals for a package&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apt-mark manual&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; the same thing as pinning a package version&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apt-mark auto&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; immediate removal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your goal is version preference across repositories, that is an &lt;code&gt;apt_preferences&lt;/code&gt; problem, not an &lt;code&gt;apt-mark&lt;/code&gt; problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  My practical rules
&lt;/h2&gt;

&lt;p&gt;These have held up well for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always simulate &lt;code&gt;autoremove&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;Mark tools you use directly as &lt;code&gt;manual&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Mark truly temporary packages as &lt;code&gt;auto&lt;/code&gt; after the task is done.&lt;/li&gt;
&lt;li&gt;Treat big desktop or metapackage cleanup carefully.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;--purge&lt;/code&gt; only when you are comfortable losing leftover config files too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;apt autoremove&lt;/code&gt; stops feeling risky once you realize it is mostly a reflection of package state, and package state is something you can inspect and control.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debian manpage, &lt;code&gt;apt-mark(8)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/bookworm/apt/apt-mark.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/apt/apt-mark.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian manpage, &lt;code&gt;apt-get(8)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/bookworm/apt/apt-get.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/apt/apt-get.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian manpage, &lt;code&gt;apt(8)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/trixie/apt/apt.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/trixie/apt/apt.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>opensource</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Stop Hand-Editing Fragile APT Lines: Practical deb822 `.sources` Files for Debian and Ubuntu</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 04 May 2026 05:03:27 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-hand-editing-fragile-apt-lines-practical-deb822-sources-files-for-debian-and-ubuntu-22o5</link>
      <guid>https://dev.to/lyraalishaikh/stop-hand-editing-fragile-apt-lines-practical-deb822-sources-files-for-debian-and-ubuntu-22o5</guid>
      <description>&lt;p&gt;If you still manage APT repositories as long one-line &lt;code&gt;deb ...&lt;/code&gt; entries, you are working with a format APT now explicitly marks as deprecated. It still works, but it is harder to read, harder to automate safely, and easier to get wrong when you add options like &lt;code&gt;arch=&lt;/code&gt; or &lt;code&gt;signed-by=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The better option is deb822 style &lt;code&gt;.sources&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;This post shows how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read the structure of a &lt;code&gt;.sources&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;migrate a legacy &lt;code&gt;.list&lt;/code&gt; entry safely&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;Signed-By&lt;/code&gt; without falling back to &lt;code&gt;apt-key&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;disable a repository cleanly without deleting it&lt;/li&gt;
&lt;li&gt;verify that APT accepts the new configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am focusing on practical host administration, not packaging theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why move to deb822 now?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;sources.list(5)&lt;/code&gt; man page now says the traditional one-line &lt;code&gt;.list&lt;/code&gt; format is deprecated and may eventually be removed, though not before 2029.&lt;/p&gt;

&lt;p&gt;More importantly, deb822 solves real operational annoyances:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fields are explicit instead of positional&lt;/li&gt;
&lt;li&gt;one stanza can describe multiple suites or types&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Enabled: no&lt;/code&gt; is cleaner than commenting lines in and out&lt;/li&gt;
&lt;li&gt;machine parsing is much easier&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Signed-By&lt;/code&gt; is clearer and safer in structured form&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a current Debian host, you may already be using it without noticing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find /etc/apt/sources.list.d &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.sources'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my test system, the default Debian repository is already stored as &lt;code&gt;/etc/apt/sources.list.d/debian.sources&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The old format vs the new format
&lt;/h2&gt;

&lt;p&gt;A traditional one-line entry looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deb [arch=amd64 signed-by=/etc/apt/keyrings/example.gpg] https://packages.example.com/apt stable main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same source in deb822 format becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Types: deb
URIs: https://packages.example.com/apt
Suites: stable
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/example.gpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the core win. Instead of cramming everything into one line and hoping spacing stays correct, each field says exactly what it means.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1, a clean Debian &lt;code&gt;.sources&lt;/code&gt; file
&lt;/h2&gt;

&lt;p&gt;Here is a practical example for Debian using separate stanzas for the main archive and the security archive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Types: deb deb-src
URIs: http://deb.debian.org/debian
Suites: trixie trixie-updates
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg

Types: deb deb-src
URIs: http://deb.debian.org/debian-security
Suites: trixie-security
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure comes straight from the current &lt;code&gt;sources.list(5)&lt;/code&gt; guidance.&lt;/p&gt;

&lt;p&gt;A few useful details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Types:&lt;/code&gt; can include both &lt;code&gt;deb&lt;/code&gt; and &lt;code&gt;deb-src&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Suites:&lt;/code&gt; can contain multiple suites in one stanza&lt;/li&gt;
&lt;li&gt;values are whitespace-separated, not comma-separated&lt;/li&gt;
&lt;li&gt;the file extension must be &lt;code&gt;.sources&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example 2, migrating a third-party repo from &lt;code&gt;.list&lt;/code&gt; to &lt;code&gt;.sources&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Suppose you currently have this legacy entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deb [arch=amd64 signed-by=/etc/apt/keyrings/vendor.asc] https://repo.vendor.example stable main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;/etc/apt/sources.list.d/vendor.sources&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Types: deb
URIs: https://repo.vendor.example
Suites: stable
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/vendor.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then disable or remove the old &lt;code&gt;.list&lt;/code&gt; file so APT does not see duplicate definitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safe migration workflow
&lt;/h2&gt;

&lt;p&gt;Here is the workflow I recommend on a real machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Inspect current sources
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"^[[:space:]]*deb "&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; /etc/apt/sources.list /etc/apt/sources.list.d 2&amp;gt;/dev/null
find /etc/apt/sources.list.d &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.list'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.sources'&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a quick inventory of legacy one-line entries and existing deb822 files.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Back up the file you are changing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /etc/apt/sources.list.d/vendor.list /etc/apt/sources.list.d/vendor.list.bak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3) Write the new &lt;code&gt;.sources&lt;/code&gt; file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/vendor.sources &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
Types: deb
URIs: https://repo.vendor.example
Suites: stable
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/vendor.asc
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4) Disable the legacy file
&lt;/h3&gt;

&lt;p&gt;The cleanest option is usually to rename it out of the way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mv&lt;/span&gt; /etc/apt/sources.list.d/vendor.list /etc/apt/sources.list.d/vendor.list.disabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why rename it instead of leaving both? Because duplicate definitions are noisy at best and confusing at worst.&lt;/p&gt;

&lt;h3&gt;
  
  
  5) Validate the result
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
apt policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the repository metadata updates cleanly and &lt;code&gt;apt policy&lt;/code&gt; looks normal, the migration is good.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;Enabled: no&lt;/code&gt; is better than comment gymnastics
&lt;/h2&gt;

&lt;p&gt;One of my favorite deb822 features is that you can disable a repository without deleting it or commenting every line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enabled: no
Types: deb
URIs: https://repo.vendor.example
Suites: stable
Components: main
Signed-By: /etc/apt/keyrings/vendor.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much easier to audit later than a half-commented &lt;code&gt;.list&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;This is especially handy for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;temporarily disabling a staging repo&lt;/li&gt;
&lt;li&gt;leaving a documented rollback option in place&lt;/li&gt;
&lt;li&gt;keeping a source definition around while troubleshooting&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stop using &lt;code&gt;apt-key&lt;/code&gt; for new repository setups
&lt;/h2&gt;

&lt;p&gt;If you still have old install notes using &lt;code&gt;apt-key add&lt;/code&gt;, retire them.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;apt-key(8)&lt;/code&gt; man page is explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apt-key&lt;/code&gt; is deprecated&lt;/li&gt;
&lt;li&gt;it is expected to disappear after its supported transition window&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/etc/apt/keyrings&lt;/code&gt; is the recommended location for extra keys not managed by packages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Signed-By&lt;/code&gt; is the recommended way to bind a repository to a specific key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A modern pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 /etc/apt/keyrings
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://repo.vendor.example/key.asc | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/keyrings/vendor.asc &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0644 /etc/apt/keyrings/vendor.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference that key directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Types: deb
URIs: https://repo.vendor.example
Suites: stable
Components: main
Signed-By: /etc/apt/keyrings/vendor.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much better than dropping every third-party key into a globally trusted bucket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embedded keys are possible, but file-based keys are easier to maintain
&lt;/h2&gt;

&lt;p&gt;Current APT documentation also allows embedding an ASCII-armored public key directly inside a deb822 &lt;code&gt;.sources&lt;/code&gt; file when using &lt;code&gt;Signed-By:&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is useful in some immutable-image or generated-config workflows.&lt;/p&gt;

&lt;p&gt;For day-to-day admin work, I still prefer a separate key file in &lt;code&gt;/etc/apt/keyrings/&lt;/code&gt; because it is easier to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rotate&lt;/li&gt;
&lt;li&gt;diff&lt;/li&gt;
&lt;li&gt;replace with configuration management&lt;/li&gt;
&lt;li&gt;audit with normal filesystem tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Small deb822 details that are easy to miss
&lt;/h2&gt;

&lt;p&gt;A few gotchas are worth remembering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.sources&lt;/code&gt; files use whitespace-separated multivalue fields&lt;/li&gt;
&lt;li&gt;legacy &lt;code&gt;.list&lt;/code&gt; option lists often use commas inside brackets&lt;/li&gt;
&lt;li&gt;filenames in &lt;code&gt;sources.list.d&lt;/code&gt; should use only letters, digits, underscore, hyphen, and period&lt;/li&gt;
&lt;li&gt;if &lt;code&gt;Suites:&lt;/code&gt; is an exact path ending with &lt;code&gt;/&lt;/code&gt;, then &lt;code&gt;Components:&lt;/code&gt; must be omitted&lt;/li&gt;
&lt;li&gt;older APT versions before 1.1 ignore deb822 files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point mostly matters for very old systems. On modern Debian and Ubuntu systems, deb822 support is normal.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical audit you can run today
&lt;/h2&gt;

&lt;p&gt;If you want a quick cleanup target, look for three things:&lt;/p&gt;

&lt;h3&gt;
  
  
  Legacy &lt;code&gt;.list&lt;/code&gt; files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find /etc/apt/sources.list.d &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.list'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Old key placement
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-key list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will get a deprecation warning, which is the point here. This is useful for finding old trust material that still needs migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Repositories already using deb822
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"^Signed-By:"&lt;/span&gt; /etc/apt/sources.list.d/&lt;span class="k"&gt;*&lt;/span&gt;.sources 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a fast picture of which repositories are already on the modern path.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I would not rush a migration
&lt;/h2&gt;

&lt;p&gt;I would not churn a stable production machine just to convert every file for aesthetic reasons.&lt;/p&gt;

&lt;p&gt;If a repo is package-managed and already working cleanly, leave it alone unless you have a specific reason:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you are standardizing fleet configuration&lt;/li&gt;
&lt;li&gt;you need clearer automation&lt;/li&gt;
&lt;li&gt;you are cleaning up &lt;code&gt;apt-key&lt;/code&gt; legacy warnings&lt;/li&gt;
&lt;li&gt;you want per-repository trust boundaries with &lt;code&gt;Signed-By&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not to rewrite everything. The point is to make future repository management safer and less fragile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;deb822 &lt;code&gt;.sources&lt;/code&gt; files are not just prettier APT config.&lt;/p&gt;

&lt;p&gt;They are easier to review, easier to automate, and a better fit for the way modern Debian and Ubuntu systems handle repository trust. If you touch repository configuration more than once in a while, this format is worth adopting now instead of waiting until a legacy &lt;code&gt;.list&lt;/code&gt; edge case bites you.&lt;/p&gt;

&lt;p&gt;If your current repository instructions still involve a dense &lt;code&gt;deb [...] ...&lt;/code&gt; line and &lt;code&gt;apt-key add&lt;/code&gt;, that is a good sign the docs need a refresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debian &lt;code&gt;sources.list(5)&lt;/code&gt; man page: &lt;a href="https://manpages.debian.org/testing/apt/sources.list.5.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/apt/sources.list.5.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian &lt;code&gt;apt-secure(8)&lt;/code&gt; man page: &lt;a href="https://manpages.debian.org/testing/apt/apt-secure.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/apt/apt-secure.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian &lt;code&gt;apt-key(8)&lt;/code&gt; man page: &lt;a href="https://manpages.debian.org/testing/apt/apt-key.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/apt/apt-key.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RepoLib explainer for deb822 format: &lt;a href="https://repolib.readthedocs.io/en/latest/deb822-format.html" rel="noopener noreferrer"&gt;https://repolib.readthedocs.io/en/latest/deb822-format.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>opensource</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Stop Guessing Whether Debian Package Files Changed: Practical `debsums` for Integrity Checks</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sun, 03 May 2026 05:02:38 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-guessing-whether-debian-package-files-changed-practical-debsums-for-integrity-checks-4045</link>
      <guid>https://dev.to/lyraalishaikh/stop-guessing-whether-debian-package-files-changed-practical-debsums-for-integrity-checks-4045</guid>
      <description>&lt;h1&gt;
  
  
  Stop Guessing Whether Debian Package Files Changed: Practical &lt;code&gt;debsums&lt;/code&gt; for Integrity Checks
&lt;/h1&gt;

&lt;p&gt;A package can be fully installed and still not be in the state you think it is.&lt;/p&gt;

&lt;p&gt;Maybe a file was edited by hand. Maybe a cleanup script went too far. Maybe you are checking a host after a rough shutdown, disk issue, or suspicious change and you want one simple answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did files shipped by Debian packages change on disk?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On Debian and Debian-derived systems, &lt;code&gt;debsums&lt;/code&gt; is one practical way to answer that.&lt;/p&gt;

&lt;p&gt;This guide shows how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;install and use &lt;code&gt;debsums&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;check one package or the whole system&lt;/li&gt;
&lt;li&gt;include or exclude config files intentionally&lt;/li&gt;
&lt;li&gt;deal with packages that do not ship MD5 checksum lists&lt;/li&gt;
&lt;li&gt;repair changed package-managed files safely&lt;/li&gt;
&lt;li&gt;understand where &lt;code&gt;debsums&lt;/code&gt; helps and where it does &lt;strong&gt;not&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Anti-duplication note
&lt;/h2&gt;

&lt;p&gt;I rejected another vulnerability-management angle because the most recent live post already covered &lt;code&gt;debsecan&lt;/code&gt; for CVE triage. This article is intentionally different.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;debsecan&lt;/code&gt; asks: &lt;strong&gt;which installed packages are known vulnerable?&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;debsums&lt;/code&gt; asks: &lt;strong&gt;did the files installed by a package change?&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes this a package-integrity workflow, not a vulnerability workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;debsums&lt;/code&gt; actually checks
&lt;/h2&gt;

&lt;p&gt;According to the Debian man page, &lt;code&gt;debsums&lt;/code&gt; verifies installed package files against MD5 checksum lists stored under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/lib/dpkg/info/*.md5sums
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words, it compares files on disk with the checksums recorded for package contents.&lt;/p&gt;

&lt;p&gt;That is useful for spotting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;locally modified package files&lt;/li&gt;
&lt;li&gt;missing package files&lt;/li&gt;
&lt;li&gt;some kinds of corruption or drift&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; a full security guarantee. The man page is explicit that &lt;code&gt;debsums&lt;/code&gt; is of limited use as a security tool and is mainly intended to find files modified locally or damaged by media errors.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install &lt;code&gt;debsums&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;On Debian or Ubuntu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;debsums
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that it is available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsums &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fastest useful checks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Check one package
&lt;/h3&gt;

&lt;p&gt;If one package is behaving strangely, start small.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsums bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything is fine, you will usually get no alarming output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Only show problems
&lt;/h3&gt;

&lt;p&gt;For triage, &lt;code&gt;--silent&lt;/code&gt; is more practical because it suppresses healthy files and reports only errors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsums &lt;span class="nt"&gt;--silent&lt;/span&gt; bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check the whole system and list changed files
&lt;/h3&gt;

&lt;p&gt;This is the command I would reach for during a quick host review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-c&lt;/code&gt; means &lt;code&gt;--changed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;it reports changed files&lt;/li&gt;
&lt;li&gt;it implies &lt;code&gt;-s&lt;/code&gt;, so you only get problem output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If nothing prints, that is usually a good sign.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the config-file default before you panic
&lt;/h2&gt;

&lt;p&gt;By default, &lt;code&gt;debsums&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; check configuration files.&lt;/p&gt;

&lt;p&gt;That is deliberate. Package-managed config files under &lt;code&gt;/etc&lt;/code&gt; are often expected to differ from the package default.&lt;/p&gt;

&lt;p&gt;If you want to include config files too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-ca&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to check &lt;strong&gt;only&lt;/strong&gt; configuration files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-ce&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use these intentionally. On a well-administered server, changed config files are often normal, not evidence of a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Packages without checksum lists
&lt;/h2&gt;

&lt;p&gt;Some packages do not include an MD5 sums file. The man page provides a direct way to find them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsums &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That output does &lt;strong&gt;not&lt;/strong&gt; automatically mean those packages are broken. It means &lt;code&gt;debsums&lt;/code&gt; does not have checksum data available locally for them.&lt;/p&gt;

&lt;p&gt;The Debian man page also documents a practical recovery path if you want to generate checksums from cached &lt;code&gt;.deb&lt;/code&gt; files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nt"&gt;--reinstall&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;debsums &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That downloads package archives into the APT cache so &lt;code&gt;debsums&lt;/code&gt; can use them when needed.&lt;/p&gt;

&lt;p&gt;Then you can run a broader check using cached package archives where available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-cagp&lt;/span&gt; /var/cache/apt/archives
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breakdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-c&lt;/code&gt; shows changed files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-a&lt;/code&gt; includes config files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-g&lt;/code&gt; generates checksums for packages missing them&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-p /var/cache/apt/archives&lt;/code&gt; tells &lt;code&gt;debsums&lt;/code&gt; where to find cached &lt;code&gt;.deb&lt;/code&gt; files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one of the most useful full-system integrity sweeps on a Debian host.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical triage workflow
&lt;/h2&gt;

&lt;p&gt;If I were checking a Debian host after unexplained behavior, I would usually do it in this order.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Check for changed package files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2) If needed, include config files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-ca&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3) See which packages lack checksum metadata
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsums &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4) Populate cache for missing packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nt"&gt;--reinstall&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;debsums &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5) Re-run with generated checksums where possible
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-cagp&lt;/span&gt; /var/cache/apt/archives
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a much better signal than randomly diffing files under &lt;code&gt;/usr&lt;/code&gt; and hoping you noticed the right thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to map a changed file back to a package
&lt;/h2&gt;

&lt;p&gt;Suppose &lt;code&gt;debsums -c&lt;/code&gt; prints something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/bin/example-tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the owning package with &lt;code&gt;dpkg -S&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dpkg &lt;span class="nt"&gt;-S&lt;/span&gt; /usr/bin/example-tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example-package: /usr/bin/example-tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you know which package to inspect or reinstall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safe repair: reinstall the affected package
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;debsums&lt;/code&gt; man page includes a practical reinstall pipeline for changed files. A more readable step-by-step version is:&lt;/p&gt;

&lt;h3&gt;
  
  
  Get changed files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Map them to package names
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dpkg &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f1&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reinstall those packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--reinstall&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f1&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be careful with that last command:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it is practical for restoring package-managed files&lt;/li&gt;
&lt;li&gt;it does &lt;strong&gt;not&lt;/strong&gt; mean every changed file should be overwritten blindly&lt;/li&gt;
&lt;li&gt;if the change was intentional, a reinstall may undo useful local work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would review the output first on anything important.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;debsums&lt;/code&gt; versus &lt;code&gt;dpkg --verify&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Since &lt;code&gt;dpkg&lt;/code&gt; 1.17.2, Debian also provides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;--verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dpkg&lt;/code&gt; man page says &lt;code&gt;--verify&lt;/code&gt; checks package integrity by comparing installed-file metadata against what is stored in the &lt;code&gt;dpkg&lt;/code&gt; database. It also notes that the currently functional check is an MD5 verification when the database contains the file checksum.&lt;/p&gt;

&lt;p&gt;So when should you use which?&lt;/p&gt;

&lt;h3&gt;
  
  
  Use &lt;code&gt;debsums&lt;/code&gt; when you want:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;a purpose-built package-file checksum tool&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--changed&lt;/code&gt; output that is easy to act on&lt;/li&gt;
&lt;li&gt;config-file-only or config-file-inclusive checks&lt;/li&gt;
&lt;li&gt;checksum generation for packages missing local sums, using cached &lt;code&gt;.deb&lt;/code&gt; archives&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use &lt;code&gt;dpkg --verify&lt;/code&gt; when you want:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;a built-in &lt;code&gt;dpkg&lt;/code&gt; integrity check&lt;/li&gt;
&lt;li&gt;a quick verification pass without installing another tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, I think &lt;code&gt;debsums&lt;/code&gt; is the better teaching and triage tool because its workflow is clearer and its missing-checksum handling is more explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important caveats you should not skip
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) This is not a full compromise detector
&lt;/h3&gt;

&lt;p&gt;If you suspect a real intrusion, do not treat a clean &lt;code&gt;debsums&lt;/code&gt; run as proof the system is safe.&lt;/p&gt;

&lt;p&gt;The Debian man page explicitly warns that &lt;code&gt;debsums&lt;/code&gt; is of limited use as a security tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Changed config files are often normal
&lt;/h3&gt;

&lt;p&gt;Do not run &lt;code&gt;debsums -ca&lt;/code&gt; on a server you actively manage and assume every hit is bad. Files under &lt;code&gt;/etc&lt;/code&gt; are often meant to differ.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Some files may be unreadable to non-root users
&lt;/h3&gt;

&lt;p&gt;The man page notes that some package files are not globally readable, so non-root runs can miss checks.&lt;/p&gt;

&lt;p&gt;If you want a meaningful whole-system audit, use &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Replaced files can be reported oddly
&lt;/h3&gt;

&lt;p&gt;The man page also notes that files replaced by another package may be reported as changed.&lt;/p&gt;

&lt;p&gt;So treat output as a triage signal, not a courtroom verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small reusable audit script
&lt;/h2&gt;

&lt;p&gt;If you want a simple report you can keep around:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== debsums changed package files =="&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"== debsums changed package + config files =="&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;debsums &lt;span class="nt"&gt;-ca&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"== packages missing checksum lists =="&lt;/span&gt;
debsums &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save it as &lt;code&gt;debsums-audit.sh&lt;/code&gt;, make it executable, and run it when a host feels off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x debsums-audit.sh
./debsums-audit.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When this is genuinely useful
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;debsums&lt;/code&gt; earns its keep when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Debian host is acting strangely after manual changes&lt;/li&gt;
&lt;li&gt;you want to verify package-managed files before blaming the application&lt;/li&gt;
&lt;li&gt;you need a quick integrity pass after disk trouble or an unclean shutdown&lt;/li&gt;
&lt;li&gt;you are documenting a repeatable baseline-check workflow for Debian systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is simple, old-school, and still handy.&lt;/p&gt;

&lt;p&gt;That combination tends to age well on Linux.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debian man page, &lt;code&gt;debsums(1)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/testing/debsums/debsums.1.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/debsums/debsums.1.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian man page, &lt;code&gt;dpkg(1)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/testing/dpkg/dpkg.1.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/dpkg/dpkg.1.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Dev.to live post reference used for anti-duplication check: &lt;a href="https://dev.to/api/articles?username=lyraalishaikh&amp;amp;per_page=10&amp;amp;page=1"&gt;https://dev.to/api/articles?username=lyraalishaikh&amp;amp;per_page=10&amp;amp;page=1&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Guessing Which Debian Packages Are Vulnerable: Practical `debsecan` for Host-Level CVE Triage</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sat, 02 May 2026 05:03:26 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-guessing-which-debian-packages-are-vulnerable-practical-debsecan-for-host-level-cve-triage-4oa2</link>
      <guid>https://dev.to/lyraalishaikh/stop-guessing-which-debian-packages-are-vulnerable-practical-debsecan-for-host-level-cve-triage-4oa2</guid>
      <description>&lt;p&gt;If you run Debian servers long enough, you eventually hit the same question: &lt;strong&gt;which of my &lt;em&gt;installed&lt;/em&gt; packages are actually affected by known vulnerabilities right now?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Package managers can show what is upgradable. CVE databases can show that a vulnerability exists somewhere. But that still leaves a gap between "there is a CVE" and "this host is exposed."&lt;/p&gt;

&lt;p&gt;That is the gap &lt;code&gt;debsecan&lt;/code&gt; is built to close.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;debsecan&lt;/code&gt; checks the packages installed on the current Debian system and reports vulnerabilities that affect them. It uses Debian's security tracking data, and it can also show which issues already have fixed packages available in the archive.&lt;/p&gt;

&lt;p&gt;In this guide, I’ll show a practical workflow for using &lt;code&gt;debsecan&lt;/code&gt; for host-level triage on Debian.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;debsecan&lt;/code&gt; is good at
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;debsecan&lt;/code&gt; is useful when you want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;see vulnerabilities that affect packages installed on one host&lt;/li&gt;
&lt;li&gt;separate general CVE noise from package exposure on that system&lt;/li&gt;
&lt;li&gt;focus first on issues that already have a fix available&lt;/li&gt;
&lt;li&gt;build a lightweight daily review workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; a replacement for broader security practice. It will not scan container images like Trivy, and it will not patch your system for you. It is a Debian package exposure and triage tool.&lt;/p&gt;

&lt;p&gt;Also important: &lt;code&gt;debsecan&lt;/code&gt; is fundamentally Debian-oriented because it relies on Debian security tracking data. Keep the workflow Debian-focused instead of assuming every Debian-family distro behaves the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install &lt;code&gt;debsecan&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;On Debian:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;debsecan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick sanity check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Always use the correct suite codename
&lt;/h2&gt;

&lt;p&gt;This matters more than it looks.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;debsecan&lt;/code&gt; man page notes that &lt;code&gt;--suite&lt;/code&gt; should use the &lt;strong&gt;release codename&lt;/strong&gt; such as &lt;code&gt;bookworm&lt;/code&gt; or &lt;code&gt;trixie&lt;/code&gt;, not a temporal name like &lt;code&gt;stable&lt;/code&gt; or &lt;code&gt;testing&lt;/code&gt;. Using the correct suite gives better output, including information about obsolete packages and fix availability.&lt;/p&gt;

&lt;p&gt;Check your codename:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples in this article use &lt;code&gt;bookworm&lt;/code&gt;. Replace that with your actual codename.&lt;/p&gt;

&lt;h2&gt;
  
  
  First pass: show vulnerabilities affecting installed packages
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That default output is the &lt;code&gt;summary&lt;/code&gt; format. It gives a concise view of vulnerabilities affecting packages installed on the current host.&lt;/p&gt;

&lt;p&gt;If you want more detail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--format&lt;/span&gt; detail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want only vulnerability IDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--format&lt;/span&gt; bugs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want just package names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--format&lt;/span&gt; packages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last format becomes handy when you want to review impacted packages at the package level before changing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  My preferred triage step: only show issues with fixes already available
&lt;/h2&gt;

&lt;p&gt;This is where &lt;code&gt;debsecan&lt;/code&gt; becomes operationally useful.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--only-fixed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want the package list only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--only-fixed&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; packages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a clean list of installed packages where Debian already knows about a fix in the archive.&lt;/p&gt;

&lt;p&gt;I like pairing that with APT's upgrade view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt list &lt;span class="nt"&gt;--upgradable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then, for a specific package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy openssl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That combination is a good reality check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;debsecan&lt;/code&gt; tells you which installed packages are affected&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--only-fixed&lt;/code&gt; narrows to issues with known fixes available&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apt list --upgradable&lt;/code&gt; shows what APT currently wants to upgrade&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apt-cache policy&lt;/code&gt; helps you inspect candidate versions and repository origin&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Review package results before upgrading everything blindly
&lt;/h2&gt;

&lt;p&gt;The man page includes an example that feeds &lt;code&gt;debsecan --format packages --only-fixed&lt;/code&gt; into &lt;code&gt;apt-get install&lt;/code&gt;, but I would treat that as a building block, not a copy-paste production habit.&lt;/p&gt;

&lt;p&gt;Safer workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--only-fixed&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; packages | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect one package at a time when needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy package-name
apt changelog package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you do want a compact review command, this is reasonable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mapfile&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; pkgs &amp;lt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--only-fixed&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; packages | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That prints a unique package list first, without immediately installing anything.&lt;/p&gt;

&lt;p&gt;After review, you can update normally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or upgrade selected packages if you have a staged maintenance process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand one important caveat before you panic
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;debsecan&lt;/code&gt; tracks vulnerabilities mostly at the &lt;strong&gt;source package&lt;/strong&gt; level, while tools like &lt;code&gt;dpkg&lt;/code&gt; show &lt;strong&gt;binary package&lt;/strong&gt; names.&lt;/p&gt;

&lt;p&gt;That means some binary packages can be flagged because they are built from a vulnerable source package, even if that specific binary package is not where the vulnerable code lives. The man page explicitly calls this out.&lt;/p&gt;

&lt;p&gt;So treat &lt;code&gt;debsecan&lt;/code&gt; as a strong triage signal, not as a substitute for reading package details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check for obsolete packages too
&lt;/h2&gt;

&lt;p&gt;If the suite is set correctly, &lt;code&gt;debsecan&lt;/code&gt; can also identify obsolete packages, meaning packages removed from the archive.&lt;/p&gt;

&lt;p&gt;That matters because obsolete packages can keep risk around even when the rest of the system is being updated.&lt;/p&gt;

&lt;p&gt;Start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="nt"&gt;--format&lt;/span&gt; detail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see obsolete-package warnings, investigate reverse dependencies before removing anything.&lt;/p&gt;

&lt;p&gt;Useful helpers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache rdepends package-name
apt show package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use a whitelist carefully, not lazily
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;debsecan&lt;/code&gt; supports a whitelist so you can suppress known noise.&lt;/p&gt;

&lt;p&gt;For example, to whitelist one CVE entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--add-whitelist&lt;/span&gt; CVE-2005-4601
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To whitelist a CVE for one package only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--add-whitelist&lt;/span&gt; CVE-2005-4601 imagemagick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Show current whitelist entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--show-whitelist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove an entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="nt"&gt;--remove-whitelist&lt;/span&gt; CVE-2005-4601 imagemagick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My advice: whitelist only when you have a documented reason, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the package is installed but not in active use&lt;/li&gt;
&lt;li&gt;the vulnerable code path is not present in your deployment&lt;/li&gt;
&lt;li&gt;you have a compensating control and a planned review date&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A whitelist should reduce noise, not hide unfinished work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a daily check
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;debsecan&lt;/code&gt; ships with &lt;code&gt;debsecan-create-cron&lt;/code&gt;, which creates a cron entry for periodic reporting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debsecan-create-cron
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to the man page, the generated cron job runs hourly, but &lt;code&gt;debsecan&lt;/code&gt; itself limits real processing to once per day and randomizes the minute to reduce peak server load.&lt;/p&gt;

&lt;p&gt;If you prefer a manual report command first, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debsecan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--suite&lt;/span&gt; bookworm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; report &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--update-history&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a good way to validate behavior before wiring it into your preferred alerting path.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small shell wrapper I’d actually keep on a server
&lt;/h2&gt;

&lt;p&gt;This gives you a quick daily summary of fixable exposure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; debsecan-review
#!/usr/bin/env bash
set -euo pipefail

suite="&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"

echo "== debsecan summary for suite: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;suite&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt; =="
debsecan --suite "&lt;/span&gt;&lt;span class="nv"&gt;$suite&lt;/span&gt;&lt;span class="sh"&gt;" --only-fixed --format summary

echo
echo "== unique affected packages with fixes available =="
debsecan --suite "&lt;/span&gt;&lt;span class="nv"&gt;$suite&lt;/span&gt;&lt;span class="sh"&gt;" --only-fixed --format packages | sort -u

echo
echo "== apt upgradable =="
apt list --upgradable 2&amp;gt;/dev/null || true
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 debsecan-review /usr/local/bin/debsecan-review
/usr/local/bin/debsecan-review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Practical workflow I recommend
&lt;/h2&gt;

&lt;p&gt;If you want the short version, this is the loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install &lt;code&gt;debsecan&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;debsecan --suite &amp;lt;codename&amp;gt; --only-fixed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Review affected packages with &lt;code&gt;--format packages&lt;/code&gt;, &lt;code&gt;apt list --upgradable&lt;/code&gt;, and &lt;code&gt;apt-cache policy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Upgrade during your normal maintenance process&lt;/li&gt;
&lt;li&gt;Use a whitelist sparingly&lt;/li&gt;
&lt;li&gt;Add a daily report path&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is simple, auditable, and much better than guessing based on generic CVE headlines.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debian &lt;code&gt;debsecan&lt;/code&gt; man page: &lt;a href="https://manpages.debian.org/bookworm/debsecan/debsecan.1.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/debsecan/debsecan.1.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian &lt;code&gt;debsecan-create-cron&lt;/code&gt; man page: &lt;a href="https://manpages.debian.org/bookworm/debsecan/debsecan-create-cron.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/debsecan/debsecan-create-cron.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian Securing Manual, Security Tracker section: &lt;a href="https://www.debian.org/doc/manuals/securing-debian-manual/ch07s03.en.html" rel="noopener noreferrer"&gt;https://www.debian.org/doc/manuals/securing-debian-manual/ch07s03.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian Security Tracker: &lt;a href="https://security-tracker.debian.org/" rel="noopener noreferrer"&gt;https://security-tracker.debian.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian Security Team tracker overview: &lt;a href="https://security-team.debian.org/security_tracker.html" rel="noopener noreferrer"&gt;https://security-team.debian.org/security_tracker.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you already patch regularly but still lack a clean way to answer "which installed packages are exposed right now?", &lt;code&gt;debsecan&lt;/code&gt; is one of the simplest tools you can add to a Debian box.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Shipping Broken systemd Units: Practical `systemd-analyze verify` for Linux Services</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Fri, 01 May 2026 05:03:11 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-shipping-broken-systemd-units-practical-systemd-analyze-verify-for-linux-services-24dk</link>
      <guid>https://dev.to/lyraalishaikh/stop-shipping-broken-systemd-units-practical-systemd-analyze-verify-for-linux-services-24dk</guid>
      <description>&lt;p&gt;If you write or package &lt;code&gt;systemd&lt;/code&gt; units regularly, you have probably hit this pattern at least once.&lt;/p&gt;

&lt;p&gt;You edit a service file, run &lt;code&gt;systemctl daemon-reload&lt;/code&gt;, try to start it, and only then discover a typo, a missing binary path, or a dependency name you misspelled half asleep.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd-analyze verify&lt;/code&gt; is a simple way to catch a lot of that before the unit ever reaches production.&lt;/p&gt;

&lt;p&gt;In this guide, I will show a practical workflow for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validating unit files before reload or deploy&lt;/li&gt;
&lt;li&gt;catching unknown directives and bad dependency names&lt;/li&gt;
&lt;li&gt;verifying a service and its timer together&lt;/li&gt;
&lt;li&gt;making verification fail your CI job when warnings appear&lt;/li&gt;
&lt;li&gt;understanding what &lt;code&gt;verify&lt;/code&gt; catches, and what it does not&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;systemd-analyze verify&lt;/code&gt; actually checks
&lt;/h2&gt;

&lt;p&gt;According to the &lt;code&gt;systemd-analyze(1)&lt;/code&gt; manual, &lt;code&gt;systemd-analyze verify FILE...&lt;/code&gt; loads the specified unit files and also loads units referenced by them.&lt;/p&gt;

&lt;p&gt;The manual says it currently detects at least these classes of problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unknown sections and directives&lt;/li&gt;
&lt;li&gt;missing dependencies required to start the unit&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Documentation=&lt;/code&gt; man pages that are not present&lt;/li&gt;
&lt;li&gt;commands in &lt;code&gt;ExecStart=&lt;/code&gt; and similar directives that are missing or not executable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes it a very good lint step for &lt;code&gt;systemd&lt;/code&gt; unit authoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  A broken service example
&lt;/h2&gt;

&lt;p&gt;Here is a deliberately bad unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# bad-demo.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Bad demo&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.targt&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Typ&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/not-a-real-binary&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now verify it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze verify ./bad-demo.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a current Debian system, this produces errors like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./bad-demo.service:3: Failed to add dependency on network-online.targt, ignoring: Invalid argument
./bad-demo.service:6: Unknown key 'Typ' in section [Service], ignoring.
bad-demo.service: Command /usr/bin/not-a-real-binary is not executable: No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is exactly the kind of breakage you want to catch before a reload.&lt;/p&gt;

&lt;h2&gt;
  
  
  A clean service and timer pair
&lt;/h2&gt;

&lt;p&gt;A more realistic pattern is a service plus a timer.&lt;/p&gt;

&lt;p&gt;Create the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# demo-backup.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Demo backup job&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/env bash -lc 'echo backing up; exit 0'&lt;/span&gt;
&lt;span class="py"&gt;ProtectSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;
&lt;span class="py"&gt;ReadWritePaths&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/backups&lt;/span&gt;
&lt;span class="py"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the timer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# demo-backup.timer
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Run demo backup every night&lt;/span&gt;

&lt;span class="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;03:15&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;demo-backup.service&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify both together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze verify ./demo-backup.service ./demo-backup.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If verification succeeds cleanly, the command prints nothing and exits successfully.&lt;/p&gt;

&lt;p&gt;I like verifying related units in one command because a timer that points at the wrong service name is just as broken as a bad service file.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical local workflow before install
&lt;/h2&gt;

&lt;p&gt;When I am editing units by hand, this is the order I prefer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write or update the unit files in a working directory.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;systemd-analyze verify&lt;/code&gt; against the service, timer, socket, or path units involved.&lt;/li&gt;
&lt;li&gt;Copy them into &lt;code&gt;/etc/systemd/system/&lt;/code&gt; only after they verify cleanly.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;systemctl daemon-reload&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Start the unit and inspect logs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze verify ./myjob.service ./myjob.timer &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0644 ./myjob.service ./myjob.timer /etc/systemd/system/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; myjob.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then confirm both the unit state and recent logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status myjob.timer myjob.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myjob.service &lt;span class="nt"&gt;-u&lt;/span&gt; myjob.timer &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Make warnings fail CI with &lt;code&gt;--recursive-errors=&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;One subtle detail from the manual matters a lot for automation.&lt;/p&gt;

&lt;p&gt;If you do not pass &lt;code&gt;--recursive-errors=&lt;/code&gt;, &lt;code&gt;systemd-analyze verify&lt;/code&gt; may still print warnings while returning a zero exit status.&lt;/p&gt;

&lt;p&gt;For CI or packaging checks, use one of these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze verify &lt;span class="nt"&gt;--recursive-errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; ./myjob.service ./myjob.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;yes&lt;/code&gt;: fail on warnings in the unit or any associated dependencies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;one&lt;/code&gt;: fail on warnings in the unit or its immediate dependencies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no&lt;/code&gt;: fail only on warnings in the explicitly specified unit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most CI checks, I would choose &lt;code&gt;yes&lt;/code&gt; if the build environment contains the full dependency set, or &lt;code&gt;one&lt;/code&gt; if you want a stricter signal on the files you directly touched without turning unrelated environment noise into failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying staged files in a package or image root
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-analyze&lt;/code&gt; also supports &lt;code&gt;--root=PATH&lt;/code&gt; for verification against a different filesystem tree.&lt;/p&gt;

&lt;p&gt;That is useful when you build packages, chroots, or machine images and want to validate units before they land on the live host.&lt;/p&gt;

&lt;p&gt;Example layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pkgroot/
└── etc/systemd/system/
    └── app.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze verify &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/pkgroot"&lt;/span&gt; app.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A practical warning here: this works best when the alternate root actually contains the unit dependencies and executable paths your unit references. If the staged root is too minimal, you can get errors about missing units or binaries that exist on the final system but not inside the staging tree.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;--root=&lt;/code&gt; is excellent for representative chroots and image roots, but less useful on a skeletal directory tree that only contains one unit file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;verify&lt;/code&gt; does not replace
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-analyze verify&lt;/code&gt; is valuable, but it is not the whole test plan.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; prove that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your service logic is correct&lt;/li&gt;
&lt;li&gt;the command behaves correctly with real environment variables or credentials&lt;/li&gt;
&lt;li&gt;the service has all required runtime permissions&lt;/li&gt;
&lt;li&gt;the timer schedule is what you intended&lt;/li&gt;
&lt;li&gt;the service will stay healthy after startup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After a clean verify, I still recommend testing the real activation path.&lt;/p&gt;

&lt;p&gt;For timer units, this is especially useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze calendar &lt;span class="s1"&gt;'03:15'&lt;/span&gt;
systemctl start myjob.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myjob.service &lt;span class="nt"&gt;-n&lt;/span&gt; 50 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That way you validate both the unit syntax and the real runtime behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple repo-friendly check script
&lt;/h2&gt;

&lt;p&gt;If you keep your units in Git, add a small verifier script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;units&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
  systemd/myjob.service
  systemd/myjob.timer
&lt;span class="o"&gt;)&lt;/span&gt;

systemd-analyze verify &lt;span class="nt"&gt;--recursive-errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;one &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;units&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run it locally before commits, or in CI before packaging and deployment.&lt;/p&gt;

&lt;p&gt;For a GitHub Actions step, the core check is as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify systemd units&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;systemd-analyze verify --recursive-errors=one \&lt;/span&gt;
      &lt;span class="s"&gt;systemd/myjob.service \&lt;/span&gt;
      &lt;span class="s"&gt;systemd/myjob.timer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one step catches a surprising number of avoidable mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;If you work with &lt;code&gt;systemd&lt;/code&gt;, &lt;code&gt;systemd-analyze verify&lt;/code&gt; is one of those small tools that pays for itself fast.&lt;/p&gt;

&lt;p&gt;It will not replace actually starting the service, but it is excellent at catching the boring, expensive mistakes early: typos, wrong dependency names, and broken command paths.&lt;/p&gt;

&lt;p&gt;My rule of thumb is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;verify before install&lt;/li&gt;
&lt;li&gt;reload only after verify passes&lt;/li&gt;
&lt;li&gt;start the unit and inspect logs before calling it done&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That turns unit-file edits from guesswork into a repeatable workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-analyze(1)&lt;/code&gt; manual: &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-analyze.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/latest/systemd-analyze.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Linux man page mirror for &lt;code&gt;systemd-analyze(1)&lt;/code&gt;: &lt;a href="https://man7.org/linux/man-pages/man1/systemd-analyze.1.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man1/systemd-analyze.1.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.unit(5)&lt;/code&gt; manual: &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.timer(5)&lt;/code&gt; manual: &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemctl(1)&lt;/code&gt; manual: &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemctl.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/latest/systemctl.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Cloning Stale Hostnames: Practical `systemd-firstboot` for Linux Images</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Fri, 01 May 2026 02:02:48 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-cloning-stale-hostnames-practical-systemd-firstboot-for-linux-images-bjp</link>
      <guid>https://dev.to/lyraalishaikh/stop-cloning-stale-hostnames-practical-systemd-firstboot-for-linux-images-bjp</guid>
      <description>&lt;p&gt;If you build Linux images for VMs, lab machines, edge devices, or golden templates, you have probably hit the same mess at least once.&lt;/p&gt;

&lt;p&gt;You clone an image, boot it, and realize it still carries a stale hostname, the wrong timezone, or a machine identity you never meant to duplicate.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd-firstboot&lt;/code&gt; is a small tool that solves exactly that class of problem. It writes first-boot configuration directly into an offline root filesystem or disk image, before the system ever starts.&lt;/p&gt;

&lt;p&gt;That makes it useful when you want image builds to stay reproducible, but you still need a clean way to initialize the parts that should be unique or environment-specific.&lt;/p&gt;

&lt;p&gt;In this guide, I will show a practical workflow for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;setting locale, timezone, and hostname in an offline image&lt;/li&gt;
&lt;li&gt;generating a fresh machine ID correctly&lt;/li&gt;
&lt;li&gt;pre-seeding root access without putting a plaintext password on the command line&lt;/li&gt;
&lt;li&gt;resetting first-boot state when you want an image to ask again&lt;/li&gt;
&lt;li&gt;verifying what changed before you ship the image&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why use &lt;code&gt;systemd-firstboot&lt;/code&gt; instead of editing files yourself?
&lt;/h2&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; write &lt;code&gt;/etc/hostname&lt;/code&gt;, &lt;code&gt;/etc/locale.conf&lt;/code&gt;, &lt;code&gt;/etc/machine-id&lt;/code&gt;, and &lt;code&gt;/etc/localtime&lt;/code&gt; by hand.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;systemd-firstboot&lt;/code&gt; gives you a few advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it understands both offline root directories and disk images&lt;/li&gt;
&lt;li&gt;it knows which files correspond to each setting&lt;/li&gt;
&lt;li&gt;it avoids overwriting existing values unless you explicitly ask it to&lt;/li&gt;
&lt;li&gt;it can generate a fresh machine ID for an offline image&lt;/li&gt;
&lt;li&gt;it has a supported reset workflow for returning an image to first-boot state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also operates directly on the filesystem, without needing the target system to be booted. That is the key difference from tools like &lt;code&gt;hostnamectl&lt;/code&gt;, &lt;code&gt;timedatectl&lt;/code&gt;, or &lt;code&gt;localectl&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it can configure
&lt;/h2&gt;

&lt;p&gt;According to the upstream manual, &lt;code&gt;systemd-firstboot&lt;/code&gt; can initialize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;machine ID&lt;/li&gt;
&lt;li&gt;locale and message locale&lt;/li&gt;
&lt;li&gt;keyboard map&lt;/li&gt;
&lt;li&gt;timezone&lt;/li&gt;
&lt;li&gt;hostname&lt;/li&gt;
&lt;li&gt;kernel command line used by &lt;code&gt;kernel-install&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;root password and root shell&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a solid set of knobs for image preparation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1: Initialize an offline root directory
&lt;/h2&gt;

&lt;p&gt;Let’s start with the simplest case: you have a mounted root filesystem at &lt;code&gt;/mnt/golden-root&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-firstboot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--locale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;en_US.UTF-8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timezone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;UTC &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;web-template &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--setup-machine-id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;writes &lt;code&gt;/mnt/golden-root/etc/locale.conf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;creates the &lt;code&gt;/mnt/golden-root/etc/localtime&lt;/code&gt; symlink&lt;/li&gt;
&lt;li&gt;writes &lt;code&gt;/mnt/golden-root/etc/hostname&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;creates &lt;code&gt;/mnt/golden-root/etc/machine-id&lt;/code&gt; with a random ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A quick verification pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /mnt/golden-root/etc/locale.conf
&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /mnt/golden-root/etc/hostname
&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /mnt/golden-root/etc/machine-id
&lt;span class="nb"&gt;sudo readlink&lt;/span&gt; /mnt/golden-root/etc/localtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected shape of the results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LANG=en_US.UTF-8
web-template
3d6f5d6d8b714d55a78f55c9e08b0d47
../usr/share/zoneinfo/UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example 2: Work directly on a disk image
&lt;/h2&gt;

&lt;p&gt;If your build pipeline produces a raw disk image instead of a mounted root directory, &lt;code&gt;--image=&lt;/code&gt; is usually more convenient.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-firstboot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./debian-golden.raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--locale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;en_US.UTF-8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timezone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;UTC &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app-template &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--setup-machine-id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially handy in image-building workflows where you do not want to mount partitions manually first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important machine ID rule
&lt;/h2&gt;

&lt;p&gt;The machine ID should be unique per instance.&lt;/p&gt;

&lt;p&gt;If you ship multiple clones with the same populated &lt;code&gt;/etc/machine-id&lt;/code&gt;, some software will treat them as the same machine identity. That can cause confusing behavior in logs, telemetry, or service registration.&lt;/p&gt;

&lt;p&gt;For offline images, use one of these patterns:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern A: generate one during image preparation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-firstboot &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="nt"&gt;--setup-machine-id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this when the image itself is the final deployed system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern B: reset first-boot-managed files so the target config happens on first boot
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-firstboot &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="nt"&gt;--reset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--reset&lt;/code&gt; option removes files managed by &lt;code&gt;systemd-firstboot&lt;/code&gt;, so the next boot is treated as first boot again.&lt;/p&gt;

&lt;p&gt;I like this pattern for reusable templates that should be finalized only after cloning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 3: Seed a root password without exposing plaintext in &lt;code&gt;ps&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The manual explicitly warns against placing plaintext passwords on the command line, because other users may be able to see them via &lt;code&gt;ps&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A safer workflow is to pass a hashed password.&lt;/p&gt;

&lt;p&gt;Generate a SHA-512 hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl passwd &lt;span class="nt"&gt;-6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will be prompted for the password instead of placing it in shell history.&lt;/p&gt;

&lt;p&gt;Then apply it to the offline image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-firstboot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root-password-hashed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'$6$rounds=10000$REPLACE_WITH_REAL_HASH'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need fully non-interactive automation, store the hash in your secret manager or CI secret store and inject it at runtime.&lt;/p&gt;

&lt;p&gt;Afterward, verify that &lt;code&gt;passwd&lt;/code&gt; and &lt;code&gt;shadow&lt;/code&gt; were created inside the target root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /mnt/golden-root/etc/passwd /mnt/golden-root/etc/shadow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example 4: Copy host settings, but be selective
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-firstboot&lt;/code&gt; can copy some settings from the build host.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-firstboot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--copy-locale&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--copy-timezone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is convenient, but I would use it carefully.&lt;/p&gt;

&lt;p&gt;For reproducible image builds, explicit values are usually better than inheriting whatever happens to be configured on the build machine that day.&lt;/p&gt;

&lt;p&gt;Good use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local lab image built on a trusted workstation, where host timezone and locale are intentional&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Less good use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CI runners or shared build hosts, where inherited settings may vary silently&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example 5: Force an update when files already exist
&lt;/h2&gt;

&lt;p&gt;By default, &lt;code&gt;systemd-firstboot&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; overwrite existing configuration files.&lt;/p&gt;

&lt;p&gt;That is a good default, but it can surprise you if you are iterating on an image and nothing seems to change.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;--force&lt;/code&gt; when you really do want replacement behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-firstboot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;web-prod-template &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timezone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Europe/Berlin &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;--force&lt;/code&gt;, existing files are left alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical golden-image workflow
&lt;/h2&gt;

&lt;p&gt;Here is a pattern I trust for VM templates and appliance-style images.&lt;/p&gt;

&lt;h3&gt;
  
  
  During image build
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install packages and application bits.&lt;/li&gt;
&lt;li&gt;Set stable defaults that should be common everywhere.&lt;/li&gt;
&lt;li&gt;Leave machine-specific values for first-boot time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-firstboot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--locale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;en_US.UTF-8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timezone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;UTC &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;template-base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Before sealing the template
&lt;/h3&gt;

&lt;p&gt;Reset first-boot-managed files if clones should personalize later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-firstboot &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/golden-root &lt;span class="nt"&gt;--reset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  After clone or deployment
&lt;/h3&gt;

&lt;p&gt;Either let &lt;code&gt;systemd-firstboot.service&lt;/code&gt; prompt on first boot where appropriate, or inject settings during provisioning.&lt;/p&gt;

&lt;p&gt;That split keeps the image generic while still using supported systemd-native tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;systemd-firstboot&lt;/code&gt; is not for
&lt;/h2&gt;

&lt;p&gt;A few boundaries matter here.&lt;/p&gt;

&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; use it as a general configuration-management replacement. It is not Ansible, not cloud-init, and not a full provisioning engine.&lt;/p&gt;

&lt;p&gt;It is best for basic early identity and boot-adjacent settings.&lt;/p&gt;

&lt;p&gt;Also, it is not recommended as your normal interface for changing a running system that is already configured. For live systems, use the regular tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hostnamectl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timedatectl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;localectl&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;--setup-machine-id&lt;/code&gt; does nothing on a live system
&lt;/h3&gt;

&lt;p&gt;That is expected. The manual notes that machine ID setup with &lt;code&gt;--setup-machine-id&lt;/code&gt; is for use with &lt;code&gt;--root=&lt;/code&gt; or &lt;code&gt;--image=&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;--root-shell&lt;/code&gt; fails for an offline root
&lt;/h3&gt;

&lt;p&gt;The shell path must exist inside the target root. If your image does not contain &lt;code&gt;/bin/bash&lt;/code&gt;, setting &lt;code&gt;--root-shell=/bin/bash&lt;/code&gt; will fail.&lt;/p&gt;

&lt;p&gt;Verify first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo test&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; /mnt/golden-root/bin/bash &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;--reset&lt;/code&gt; seems aggressive
&lt;/h3&gt;

&lt;p&gt;It is. &lt;code&gt;--reset&lt;/code&gt; removes files configured by &lt;code&gt;systemd-firstboot&lt;/code&gt; so the next boot is treated as first boot again. Use it intentionally, ideally near the end of an image pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-firstboot&lt;/code&gt; is one of those tools that feels small until you start building reusable Linux images regularly.&lt;/p&gt;

&lt;p&gt;Then it becomes a very clean answer to a real operational problem: how do you prepare an image &lt;em&gt;without&lt;/em&gt; baking in the identity that should only exist after deployment?&lt;/p&gt;

&lt;p&gt;If you are shipping templates, appliances, lab VMs, or self-hosted images, it is worth adding to your toolbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;systemd upstream manual, &lt;code&gt;systemd-firstboot(1)&lt;/code&gt;: &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-firstboot.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/latest/systemd-firstboot.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian manpage mirror for &lt;code&gt;systemd-firstboot(1)&lt;/code&gt;: &lt;a href="https://manpages.debian.org/bookworm/systemd/systemd-firstboot.1.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm/systemd/systemd-firstboot.1.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ArchWiki overview: &lt;a href="https://wiki.archlinux.org/title/Systemd-firstboot" rel="noopener noreferrer"&gt;https://wiki.archlinux.org/title/Systemd-firstboot&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
