<?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 Flying Blind on Linux Security Events: Practical auditd for Real-Time Monitoring</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sun, 14 Jun 2026 05:01:17 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-flying-blind-on-linux-security-events-practical-auditd-for-real-time-monitoring-2one</link>
      <guid>https://dev.to/lyraalishaikh/stop-flying-blind-on-linux-security-events-practical-auditd-for-real-time-monitoring-2one</guid>
      <description>&lt;h1&gt;
  
  
  Stop Flying Blind on Linux Security Events: Practical auditd for Real-Time Monitoring
&lt;/h1&gt;

&lt;p&gt;If you've ever wondered "who changed that config file at 3 AM?" or needed to prove exactly which process touched a sensitive binary, &lt;code&gt;auditd&lt;/code&gt; is the tool that gives you the answers without waiting for the next integrity scan.&lt;/p&gt;

&lt;p&gt;Unlike periodic file integrity tools that compare snapshots, the Linux Audit Framework watches events in real time as they happen—file writes, program executions, even specific syscalls. It's the foundation for many compliance frameworks and incident response workflows on Debian and Ubuntu systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why auditd Matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Real-time visibility into privilege escalation, config drift, and unauthorized access attempts&lt;/li&gt;
&lt;li&gt;Low-overhead when tuned properly (rules are evaluated in kernel space)&lt;/li&gt;
&lt;li&gt;Integrates cleanly with journald and can forward to SIEMs&lt;/li&gt;
&lt;li&gt;Required for many CIS benchmarks and regulatory controls&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installation and Basic Setup on Debian/Ubuntu
&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; &lt;span class="nt"&gt;-y&lt;/span&gt; auditd audispd-plugins
&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; auditd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it's running:&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;auditctl &lt;span class="nt"&gt;-s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the current status, including the number of rules loaded and the failure mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring auditd.conf for Production
&lt;/h2&gt;

&lt;p&gt;Edit &lt;code&gt;/etc/audit/auditd.conf&lt;/code&gt; with sensible defaults:&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;log_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/audit/audit.log&lt;/span&gt;
&lt;span class="py"&gt;num_logs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;8&lt;/span&gt;
&lt;span class="py"&gt;max_log_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;100&lt;/span&gt;
&lt;span class="py"&gt;max_log_file_action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ROTATE&lt;/span&gt;
&lt;span class="py"&gt;space_left&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;75&lt;/span&gt;
&lt;span class="py"&gt;space_left_action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;SYSLOG&lt;/span&gt;
&lt;span class="py"&gt;disk_full_action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;SUSPEND&lt;/span&gt;
&lt;span class="py"&gt;admin_space_left&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;50&lt;/span&gt;
&lt;span class="py"&gt;admin_space_left_action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;SUSPEND&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Place the audit log on a separate partition when possible to avoid filling root.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Practical Audit Rules
&lt;/h2&gt;

&lt;p&gt;Use the modular &lt;code&gt;rules.d/&lt;/code&gt; directory and &lt;code&gt;augenrules&lt;/code&gt; (the modern approach).&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;/etc/audit/rules.d/10-security-baseline.rules&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="c"&gt;# Delete any existing rules&lt;/span&gt;
&lt;span class="nt"&gt;-D&lt;/span&gt;

&lt;span class="c"&gt;# Increase buffer size for busy systems&lt;/span&gt;
&lt;span class="nt"&gt;-b&lt;/span&gt; 8192

&lt;span class="c"&gt;# Make auditd panic on critical failure (optional, use 1 for logging only)&lt;/span&gt;
&lt;span class="nt"&gt;-f&lt;/span&gt; 1

&lt;span class="c"&gt;# Monitor identity and authentication files&lt;/span&gt;
&lt;span class="nt"&gt;-w&lt;/span&gt; /etc/passwd &lt;span class="nt"&gt;-p&lt;/span&gt; wa &lt;span class="nt"&gt;-k&lt;/span&gt; identity_passwd
&lt;span class="nt"&gt;-w&lt;/span&gt; /etc/shadow &lt;span class="nt"&gt;-p&lt;/span&gt; wa &lt;span class="nt"&gt;-k&lt;/span&gt; identity_shadow
&lt;span class="nt"&gt;-w&lt;/span&gt; /etc/group &lt;span class="nt"&gt;-p&lt;/span&gt; wa &lt;span class="nt"&gt;-k&lt;/span&gt; identity_group
&lt;span class="nt"&gt;-w&lt;/span&gt; /etc/gshadow &lt;span class="nt"&gt;-p&lt;/span&gt; wa &lt;span class="nt"&gt;-k&lt;/span&gt; identity_gshadow
&lt;span class="nt"&gt;-w&lt;/span&gt; /etc/sudoers &lt;span class="nt"&gt;-p&lt;/span&gt; wa &lt;span class="nt"&gt;-k&lt;/span&gt; sudoers_changes
&lt;span class="nt"&gt;-w&lt;/span&gt; /etc/ssh/sshd_config &lt;span class="nt"&gt;-p&lt;/span&gt; wa &lt;span class="nt"&gt;-k&lt;/span&gt; ssh_config

&lt;span class="c"&gt;# Monitor important binaries for execution or modification&lt;/span&gt;
&lt;span class="nt"&gt;-w&lt;/span&gt; /usr/bin/passwd &lt;span class="nt"&gt;-p&lt;/span&gt; x &lt;span class="nt"&gt;-k&lt;/span&gt; passwd_exec
&lt;span class="nt"&gt;-w&lt;/span&gt; /usr/bin/sudo &lt;span class="nt"&gt;-p&lt;/span&gt; x &lt;span class="nt"&gt;-k&lt;/span&gt; sudo_exec
&lt;span class="nt"&gt;-w&lt;/span&gt; /bin/su &lt;span class="nt"&gt;-p&lt;/span&gt; x &lt;span class="nt"&gt;-k&lt;/span&gt; su_exec

&lt;span class="c"&gt;# Watch for changes to audit configuration itself&lt;/span&gt;
&lt;span class="nt"&gt;-w&lt;/span&gt; /etc/audit/ &lt;span class="nt"&gt;-p&lt;/span&gt; wa &lt;span class="nt"&gt;-k&lt;/span&gt; audit_config

&lt;span class="c"&gt;# Example syscall rule: track all execve calls by non-root users&lt;/span&gt;
&lt;span class="nt"&gt;-a&lt;/span&gt; always,exit &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nb"&gt;arch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;b64 &lt;span class="nt"&gt;-S&lt;/span&gt; execve &lt;span class="nt"&gt;-F&lt;/span&gt; euid!&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nt"&gt;-k&lt;/span&gt; user_exec

&lt;span class="c"&gt;# Load the rules&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load them:&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;augenrules &lt;span class="nt"&gt;--load&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;augenrules &lt;span class="nt"&gt;--check&lt;/span&gt;   &lt;span class="c"&gt;# Verify syntax&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check loaded rules:&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;auditctl &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Searching Logs with ausearch and aureport
&lt;/h2&gt;

&lt;p&gt;The real power comes from querying:&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;# All events tagged with a specific key today&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ausearch &lt;span class="nt"&gt;-k&lt;/span&gt; identity_passwd &lt;span class="nt"&gt;-ts&lt;/span&gt; today

&lt;span class="c"&gt;# Failed access attempts&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ausearch &lt;span class="nt"&gt;-m&lt;/span&gt; avc,user_auth,daemon_start &lt;span class="nt"&gt;-ts&lt;/span&gt; yesterday &lt;span class="nt"&gt;-i&lt;/span&gt;

&lt;span class="c"&gt;# Generate a summary report&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;aureport &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;--summary&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;aureport &lt;span class="nt"&gt;--auth&lt;/span&gt; &lt;span class="nt"&gt;--summary&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For daily review, you can wrap these in a small script run by a systemd timer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance and Operational Tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Start with &lt;code&gt;-b 8192&lt;/code&gt; and tune based on &lt;code&gt;auditctl -s&lt;/code&gt; output (look for lost events)&lt;/li&gt;
&lt;li&gt;Use specific keys and avoid overly broad rules&lt;/li&gt;
&lt;li&gt;Forward logs centrally using &lt;code&gt;audispd-plugins&lt;/code&gt; or syslog&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;audit=1&lt;/code&gt; to your kernel command line so early boot events are captured&lt;/li&gt;
&lt;li&gt;Test rules in a lab first—bad rules can generate massive log volume&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources and Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Red Hat Enterprise Linux Security Hardening: Auditing the system&lt;/li&gt;
&lt;li&gt;Neo23x0/auditd GitHub repository (excellent community rule sets)&lt;/li&gt;
&lt;li&gt;Linux man pages: &lt;code&gt;man auditd&lt;/code&gt;, &lt;code&gt;man audit.rules&lt;/code&gt;, &lt;code&gt;man augenrules&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;OneUptime and community guides on Ubuntu/Debian auditd configuration (2026)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup gives you actionable, searchable evidence the moment something important happens on your Linux systems. Start with the identity and sudo monitoring rules above—you'll be surprised how quickly they pay off during troubleshooting or audits.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with care for the Linux community. All examples tested on current Debian 12 and Ubuntu 24.04/25.10 releases.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Make NFS Mounts Stop Blocking Boot on Linux: Practical `systemd.automount` with Idle Unmounts</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Fri, 12 Jun 2026 12:53:04 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/make-nfs-mounts-stop-blocking-boot-on-linux-practical-systemdautomount-with-idle-unmounts-1mkb</link>
      <guid>https://dev.to/lyraalishaikh/make-nfs-mounts-stop-blocking-boot-on-linux-practical-systemdautomount-with-idle-unmounts-1mkb</guid>
      <description>&lt;p&gt;If you have ever watched a Linux box stall during boot because a NAS was slow, offline, or reachable only after Wi-Fi came up, this is the fix I wish more people used by default.&lt;/p&gt;

&lt;p&gt;Instead of mounting a remote share eagerly at boot, let systemd create an automount point. The path appears immediately, and the real mount only happens when something actually touches it.&lt;/p&gt;

&lt;p&gt;That gives you three practical wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your system boots more reliably when the server is late or absent&lt;/li&gt;
&lt;li&gt;interactive shells and services stop paying the mount cost until they need the share&lt;/li&gt;
&lt;li&gt;you can add idle unmounts so inactive mounts do not stay pinned forever&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will show a working &lt;code&gt;fstab&lt;/code&gt; example, how to verify it, and which NFS options are worth using carefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  When &lt;code&gt;systemd.automount&lt;/code&gt; helps
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;home labs with NAS shares&lt;/li&gt;
&lt;li&gt;laptops that sometimes leave the local network&lt;/li&gt;
&lt;li&gt;small servers that consume a remote media or backup share&lt;/li&gt;
&lt;li&gt;hosts where a slow NFS server should not delay boot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; magic. The first access to the path still waits for the mount to complete. What changes is &lt;strong&gt;when&lt;/strong&gt; you pay that cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea in one line
&lt;/h2&gt;

&lt;p&gt;A normal NFS line mounts the share during boot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nas.example.internal:/srv/export/media  /mnt/media  nfs  defaults,_netdev  0  0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An automount-based line tells systemd to create an automount unit from &lt;code&gt;fstab&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;nas.example.internal:/srv/export/media  /mnt/media  nfs  noauto,x-systemd.automount,x-systemd.idle-timeout=10min,_netdev  0  0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key option is &lt;code&gt;x-systemd.automount&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;According to &lt;code&gt;systemd.mount(5)&lt;/code&gt;, that option causes systemd to create a matching automount unit. &lt;code&gt;systemd.automount(5)&lt;/code&gt; documents that the real mount is activated when the path is accessed, and &lt;code&gt;x-systemd.idle-timeout=&lt;/code&gt; maps to the automount idle timeout behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical NFS example
&lt;/h2&gt;

&lt;p&gt;Create the mount point 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 mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/media
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add this to &lt;code&gt;/etc/fstab&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;nas.example.internal:/srv/export/media  /mnt/media  nfs  noauto,x-systemd.automount,x-systemd.idle-timeout=10min,_netdev,nfsvers=4.2,hard,timeo=600,retrans=2  0  0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why these options?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;x-systemd.automount&lt;/code&gt; creates the on-demand automount&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x-systemd.idle-timeout=10min&lt;/code&gt; lets systemd try to unmount after 10 minutes of inactivity&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_netdev&lt;/code&gt; tells systemd to treat this as a network mount&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nfsvers=4.2&lt;/code&gt; asks for NFSv4.2 and fails if the server does not support it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hard&lt;/code&gt; keeps retrying I/O instead of returning early errors that can corrupt workflows&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timeo=600&lt;/code&gt; and &lt;code&gt;retrans=2&lt;/code&gt; keep the behavior explicit instead of relying on distro defaults&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A quick caution on &lt;code&gt;soft&lt;/code&gt;: the &lt;code&gt;nfs(5)&lt;/code&gt; man page warns that &lt;code&gt;soft&lt;/code&gt; or &lt;code&gt;softerr&lt;/code&gt; can cause silent data corruption in some cases. For anything that matters, I strongly prefer &lt;code&gt;hard&lt;/code&gt; unless you have a very specific reason not to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reload and enable the generated units
&lt;/h2&gt;

&lt;p&gt;After editing &lt;code&gt;fstab&lt;/code&gt;, reload systemd and start the automount 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 daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start mnt-media.automount
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;mnt-media.automount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can derive the unit name from the path with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-escape &lt;span class="nt"&gt;--path&lt;/span&gt; /mnt/media
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That outputs &lt;code&gt;mnt-media&lt;/code&gt;, which is why the unit is named &lt;code&gt;mnt-media.automount&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you prefer to let the next boot pick it up, that also works, but I like verifying immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify that the automount exists before the real mount
&lt;/h2&gt;

&lt;p&gt;Check the automount unit:&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 mnt-media.automount &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or list just automount units:&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-units &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;automount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, the automount should be active even if the real NFS mount is not mounted yet.&lt;/p&gt;

&lt;p&gt;You can confirm that 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 /mnt/media
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on timing, you may see the autofs placeholder first. The real NFS mount appears after first access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trigger the mount on first access
&lt;/h2&gt;

&lt;p&gt;Now touch the path:&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;ls&lt;/span&gt; /mnt/media
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect it again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;findmnt /mnt/media
mount | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;' /mnt/media '&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should now see the NFS mount active.&lt;/p&gt;

&lt;p&gt;This delayed mount is the whole point: the machine no longer has to complete that remote mount during early boot just to become usable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the idle unmount
&lt;/h2&gt;

&lt;p&gt;If you set &lt;code&gt;x-systemd.idle-timeout=10min&lt;/code&gt;, stop touching the path and wait.&lt;/p&gt;

&lt;p&gt;Then check:&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 mnt-media.automount &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
findmnt /mnt/media
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The automount unit should remain, while the real NFS mount may disappear after the idle timeout. The next access mounts it again automatically.&lt;/p&gt;

&lt;p&gt;This is handy on laptops and intermittently connected systems because inactive mounts do not linger forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting tips that actually help
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Do not add &lt;code&gt;After=network-online.target&lt;/code&gt; to the automount unit
&lt;/h3&gt;

&lt;p&gt;This is a subtle but important one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd.automount(5)&lt;/code&gt; explicitly warns against adding &lt;code&gt;After=&lt;/code&gt; or &lt;code&gt;Requires=&lt;/code&gt; network-style dependencies to the automount unit itself because that can create ordering cycles. If you are using &lt;code&gt;fstab&lt;/code&gt;, let systemd generate the right relationships for the mount, and use &lt;code&gt;_netdev&lt;/code&gt; when needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) &lt;code&gt;noauto&lt;/code&gt; does not disable the automount when &lt;code&gt;x-systemd.automount&lt;/code&gt; is present
&lt;/h3&gt;

&lt;p&gt;This surprises people.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd.mount(5)&lt;/code&gt; documents that when &lt;code&gt;x-systemd.automount&lt;/code&gt; is used, &lt;code&gt;auto&lt;/code&gt; and &lt;code&gt;noauto&lt;/code&gt; do not affect whether the matching automount unit is pulled in. In practice, &lt;code&gt;x-systemd.automount&lt;/code&gt; is what matters.&lt;/p&gt;

&lt;p&gt;I still include &lt;code&gt;noauto&lt;/code&gt; because it communicates intent clearly to humans reading &lt;code&gt;fstab&lt;/code&gt;: do not mount this eagerly.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Use &lt;code&gt;_netdev&lt;/code&gt; if systemd might not recognize it as remote
&lt;/h3&gt;

&lt;p&gt;For NFS, the filesystem type already strongly suggests a network mount. But &lt;code&gt;_netdev&lt;/code&gt; is still useful as an explicit hint, and it matters more for storage that is network-backed but not obviously typed that way.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Avoid nested automounts
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;systemd.automount(5)&lt;/code&gt; warns that nested automounts are a bad fit because inner automount points can pin outer ones and defeat the purpose.&lt;/p&gt;

&lt;p&gt;If you need multiple remote shares, prefer separate top-level mount points such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/mnt/media&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/mnt/backups&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/mnt/projects&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;instead of stacking automounts inside one another.&lt;/p&gt;

&lt;h3&gt;
  
  
  5) Be careful with background NFS mounts
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;systemd.mount(5)&lt;/code&gt; notes that traditional NFS &lt;code&gt;bg&lt;/code&gt; handling is translated by &lt;code&gt;systemd-fstab-generator&lt;/code&gt;, but it also says it may be more appropriate to use &lt;code&gt;x-systemd.automount&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;That matches my experience. For modern systemd-based systems, automounts are usually the cleaner answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second example for a read-mostly archive share
&lt;/h2&gt;

&lt;p&gt;For a mostly read-only archive, I would still stay conservative with integrity-related behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nas.example.internal:/srv/export/archive  /mnt/archive  nfs  ro,noauto,x-systemd.automount,x-systemd.idle-timeout=15min,_netdev,nfsvers=4.2,hard,timeo=600,retrans=2  0  0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then activate 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 mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/archive
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start mnt-archive.automount
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;mnt-archive.automount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I decide between plain mount and automount
&lt;/h2&gt;

&lt;p&gt;I use a regular mount when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the system cannot function without the share&lt;/li&gt;
&lt;li&gt;an application must have the mount available before it starts&lt;/li&gt;
&lt;li&gt;I want failures to surface immediately during boot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I use &lt;code&gt;x-systemd.automount&lt;/code&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the share is convenient, not boot-critical&lt;/li&gt;
&lt;li&gt;the server may be slow, asleep, or temporarily absent&lt;/li&gt;
&lt;li&gt;the host is mobile or changes networks&lt;/li&gt;
&lt;li&gt;I want less boot coupling between machines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters more than it sounds. Tight boot coupling between a client and a remote share is how a minor NAS hiccup becomes a system-wide nuisance.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd.automount(5)&lt;/code&gt;, Debian manpages: &lt;a href="https://manpages.debian.org/testing/systemd/systemd.automount.5.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/systemd/systemd.automount.5.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.mount(5)&lt;/code&gt;, Debian manpages: &lt;a href="https://manpages.debian.org/testing/systemd/systemd.mount.5.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/systemd/systemd.mount.5.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-fstab-generator(8)&lt;/code&gt;, Debian manpages: &lt;a href="https://manpages.debian.org/testing/systemd/systemd-fstab-generator.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/systemd/systemd-fstab-generator.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nfs(5)&lt;/code&gt;, man7.org: &lt;a href="https://man7.org/linux/man-pages/man5/nfs.5.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man5/nfs.5.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;If a remote share is not truly required for boot, do not make boot wait for it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd.automount&lt;/code&gt; is one of those small Linux tools that quietly removes a whole class of annoyance. You still get the mount, just at the moment it becomes useful instead of the moment it becomes risky.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Keep Your Base OS Clean: Practical `systemd-sysext` for Linux Tools and Overrides</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Fri, 12 Jun 2026 12:53:01 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/keep-your-base-os-clean-practical-systemd-sysext-for-linux-tools-and-overrides-3bk3</link>
      <guid>https://dev.to/lyraalishaikh/keep-your-base-os-clean-practical-systemd-sysext-for-linux-tools-and-overrides-3bk3</guid>
      <description>&lt;h1&gt;
  
  
  Keep Your Base OS Clean: Practical &lt;code&gt;systemd-sysext&lt;/code&gt; for Linux Tools and Overrides
&lt;/h1&gt;

&lt;p&gt;I like keeping the base OS boring.&lt;/p&gt;

&lt;p&gt;That does not mean the machine has to stay limited. It means I want a clean line between the core system and the extra bits I only need sometimes, especially on hosts where &lt;code&gt;/usr&lt;/code&gt; is meant to stay stable.&lt;/p&gt;

&lt;p&gt;That is where &lt;code&gt;systemd-sysext&lt;/code&gt; gets interesting.&lt;/p&gt;

&lt;p&gt;It lets you merge additional files into &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt; at runtime using overlayfs, without permanently modifying the host tree. Unmerge the extension, and those files disappear again. For immutable or tightly controlled Linux systems, that is a very practical way to add debug tools, test builds, or one-off low-level binaries without turning the base image into a junk drawer.&lt;/p&gt;

&lt;p&gt;In this guide, I will show a safe, directory-based workflow you can actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;systemd-sysext&lt;/code&gt; is good at
&lt;/h2&gt;

&lt;p&gt;According to the &lt;code&gt;systemd-sysext&lt;/code&gt; documentation, system extension images are meant to extend &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt; dynamically at runtime, and they are especially useful when the base OS image is read-only or intended to remain unchanged. The merge is read-only, and while active, the host's &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt; also become read-only.&lt;/p&gt;

&lt;p&gt;That makes &lt;code&gt;systemd-sysext&lt;/code&gt; a good fit for things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shipping optional troubleshooting tools&lt;/li&gt;
&lt;li&gt;testing a newer build of a low-level binary&lt;/li&gt;
&lt;li&gt;layering in site-specific files on top of a controlled base image&lt;/li&gt;
&lt;li&gt;keeping the base OS reproducible while still allowing operational flexibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is &lt;em&gt;not&lt;/em&gt; a general-purpose package manager. There is no dependency solver here. The docs are pretty explicit about that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;A few boundaries matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-sysext&lt;/code&gt; merges only &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;files inside &lt;code&gt;/etc&lt;/code&gt; and &lt;code&gt;/var&lt;/code&gt; in the extension are ignored by sysext&lt;/li&gt;
&lt;li&gt;it is additive by design, even though overlayfs technically allows replacement behavior&lt;/li&gt;
&lt;li&gt;it is not the right tool for shipping system services early in boot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need to deliver service units in an image with tighter isolation, &lt;code&gt;portablectl&lt;/code&gt; and portable services are the closer fit.&lt;/p&gt;

&lt;p&gt;If you want runtime config layering for &lt;code&gt;/etc&lt;/code&gt;, look at &lt;code&gt;systemd-confext&lt;/code&gt;, not sysext.&lt;/p&gt;

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

&lt;p&gt;Recent posts already covered &lt;code&gt;systemd-delta&lt;/code&gt;, &lt;code&gt;systemd-tmpfiles&lt;/code&gt;, socket activation, &lt;code&gt;systemd-oomd&lt;/code&gt;, and other systemd operations topics. I am intentionally taking a different angle here: runtime extension images for &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt;, not unit override auditing, cleanup policy, or service lifecycle tuning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Linux host with &lt;code&gt;systemd-sysext&lt;/code&gt; available&lt;/li&gt;
&lt;li&gt;root access for installation into system extension paths&lt;/li&gt;
&lt;li&gt;overlayfs support in the kernel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check whether the tool exists:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;On many systems, extension images are searched in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/extensions/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/run/extensions/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/var/lib/extensions/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For actual installed content, &lt;code&gt;/var/lib/extensions/&lt;/code&gt; is the normal place to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The compatibility rule that trips people up
&lt;/h2&gt;

&lt;p&gt;Every sysext image needs an extension metadata 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;/usr/lib/extension-release.d/extension-release.NAME
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NAME&lt;/code&gt; must match the image or directory name.&lt;/p&gt;

&lt;p&gt;That file is checked against the host OS metadata. Per the man page, the extension's &lt;code&gt;ID=&lt;/code&gt; must match the host unless you deliberately set &lt;code&gt;_any&lt;/code&gt;. If &lt;code&gt;SYSEXT_LEVEL=&lt;/code&gt; is present, it must match. Otherwise &lt;code&gt;VERSION_ID=&lt;/code&gt; is used as the compatibility check.&lt;/p&gt;

&lt;p&gt;For a directory named &lt;code&gt;debug-tools&lt;/code&gt;, the file must be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/lib/extension-release.d/extension-release.debug-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A minimal example:&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;ID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;debian&lt;/span&gt;
&lt;span class="py"&gt;VERSION_ID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;12&lt;/span&gt;
&lt;span class="py"&gt;SYSEXT_SCOPE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;
&lt;span class="py"&gt;ARCHITECTURE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;x86-64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ID=&lt;/code&gt; should match your host OS family&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VERSION_ID=&lt;/code&gt; is the fallback compatibility gate&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ARCHITECTURE=&lt;/code&gt; should match the host architecture when set&lt;/li&gt;
&lt;li&gt;do &lt;strong&gt;not&lt;/strong&gt; put &lt;code&gt;os-release&lt;/code&gt; in the extension's &lt;code&gt;/usr/lib&lt;/code&gt;, because that would shadow the host metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Build a simple directory-based extension
&lt;/h2&gt;

&lt;p&gt;Let us create a tiny extension that drops one helper script into &lt;code&gt;/usr/local/bin&lt;/code&gt; and one documentation file into &lt;code&gt;/opt&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 mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/extensions/debug-tools/usr/local/bin
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/extensions/debug-tools/usr/lib/extension-release.d
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/extensions/debug-tools/opt/debug-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the compatibility file:&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; /var/lib/extensions/debug-tools/usr/lib/extension-release.d/extension-release.debug-tools &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;'
ID=debian
VERSION_ID=12
SYSEXT_SCOPE=system
ARCHITECTURE=x86-64
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a small helper 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="nb"&gt;sudo tee&lt;/span&gt; /var/lib/extensions/debug-tools/usr/local/bin/hello-sysext &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;'
#!/usr/bin/env bash
set -euo pipefail
printf 'hello from systemd-sysext&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0755 /var/lib/extensions/debug-tools/usr/local/bin/hello-sysext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add an optional file under &lt;code&gt;/opt&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; /var/lib/extensions/debug-tools/opt/debug-tools/README.txt &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;'
This file is provided by the debug-tools system extension.
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Activate it
&lt;/h2&gt;

&lt;p&gt;Refresh sysext state:&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-sysext refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-sysext status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the extension is accepted, you should now see the files through the live host tree:&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;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; hello-sysext
hello-sysext
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /opt/debug-tools
&lt;span class="nb"&gt;cat&lt;/span&gt; /opt/debug-tools/README.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to see all recognized extensions, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-sysext list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Remove it cleanly
&lt;/h2&gt;

&lt;p&gt;To make the files disappear from the merged view:&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-sysext unmerge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To bring them back:&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-sysext merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To update after changing files inside the extension directory:&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-sysext refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;refresh&lt;/code&gt; flow is the one you will use most in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic use case: layering in a locally built binary
&lt;/h2&gt;

&lt;p&gt;One of the documented uses is to stage a newer build of a low-level component without rebuilding the whole base OS.&lt;/p&gt;

&lt;p&gt;For example, if you have a Makefile that supports &lt;code&gt;DESTDIR&lt;/code&gt;, you can install into the extension directory instead of the live 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 mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/extensions/mytest
make
&lt;span class="nb"&gt;sudo &lt;/span&gt;&lt;span class="nv"&gt;DESTDIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/lib/extensions/mytest make &lt;span class="nb"&gt;install
sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/extensions/mytest/usr/lib/extension-release.d
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /var/lib/extensions/mytest/usr/lib/extension-release.d/extension-release.mytest &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;'
ID=debian
VERSION_ID=12
SYSEXT_SCOPE=system
ARCHITECTURE=x86-64
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysext refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a reversible way to test files as if they were part of the base image, but without permanently mutating &lt;code&gt;/usr&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mask an extension without deleting it
&lt;/h2&gt;

&lt;p&gt;There is no classic enable or disable toggle per extension. Installed extensions are activated automatically at boot if &lt;code&gt;systemd-sysext.service&lt;/code&gt; is enabled.&lt;/p&gt;

&lt;p&gt;But the docs provide a neat masking trick: create an empty directory with the same name in &lt;code&gt;/etc/extensions/&lt;/code&gt;.&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 mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/extensions/debug-tools
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysext refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That masks a lower-precedence extension of the same name from system locations.&lt;/p&gt;

&lt;p&gt;To unmask 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 rmdir&lt;/span&gt; /etc/extensions/debug-tools
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysext refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Troubleshooting checklist
&lt;/h2&gt;

&lt;p&gt;If your extension does not appear, check these first.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Name mismatch
&lt;/h3&gt;

&lt;p&gt;The directory name and &lt;code&gt;extension-release.NAME&lt;/code&gt; must match.&lt;/p&gt;

&lt;p&gt;For example, this is valid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;directory: &lt;code&gt;debug-tools&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;file: &lt;code&gt;extension-release.debug-tools&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. OS compatibility mismatch
&lt;/h3&gt;

&lt;p&gt;If the extension says &lt;code&gt;ID=ubuntu&lt;/code&gt; and the host is Debian, sysext should reject it.&lt;/p&gt;

&lt;p&gt;Likewise, &lt;code&gt;SYSEXT_LEVEL=&lt;/code&gt; or &lt;code&gt;VERSION_ID=&lt;/code&gt; must line up with the host metadata.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Wrong paths inside the extension
&lt;/h3&gt;

&lt;p&gt;Only &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt; are merged by sysext.&lt;/p&gt;

&lt;p&gt;If you put content under &lt;code&gt;/etc/myapp&lt;/code&gt;, sysext will ignore it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. You forgot to refresh
&lt;/h3&gt;

&lt;p&gt;After adding or removing files in &lt;code&gt;/var/lib/extensions/...&lt;/code&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;systemd-sysext refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. You expected writable merged paths
&lt;/h3&gt;

&lt;p&gt;While sysext is active, the merged &lt;code&gt;/usr&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt; views are read-only.&lt;/p&gt;

&lt;p&gt;That is deliberate.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I would use packages instead
&lt;/h2&gt;

&lt;p&gt;I would still prefer normal packages when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I want dependency management&lt;/li&gt;
&lt;li&gt;I want normal upgrade and removal tracking&lt;/li&gt;
&lt;li&gt;I am distributing software broadly to many mixed systems&lt;/li&gt;
&lt;li&gt;the host is not trying to keep &lt;code&gt;/usr&lt;/code&gt; controlled or reproducible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;systemd-sysext&lt;/code&gt; shines when the host image is treated as a base artifact and you want optional or reversible layering on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  When portable services are the better tool
&lt;/h2&gt;

&lt;p&gt;This trips people up because both concepts involve image-based delivery.&lt;/p&gt;

&lt;p&gt;My rule of thumb is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;strong&gt;sysext&lt;/strong&gt; when you want extra files to appear in &lt;code&gt;/usr&lt;/code&gt; or &lt;code&gt;/opt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;use &lt;strong&gt;portable services&lt;/strong&gt; when you want to ship services in an image and manage them as services, with service-level sandboxing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The systemd documentation explicitly calls out that difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;I would not use &lt;code&gt;systemd-sysext&lt;/code&gt; everywhere.&lt;/p&gt;

&lt;p&gt;But on a host where the base OS should stay clean, predictable, and easy to reason about, it is a sharp tool. You get a reversible layer for binaries and support files, and the operational model stays simple: build the extension tree, add the compatibility metadata, then &lt;code&gt;refresh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is a nice trade, especially when the alternative is "just copy it into &lt;code&gt;/usr/local&lt;/code&gt; and hope we remember later."&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;systemd-sysext man page: &lt;a href="https://manpages.debian.org/bookworm-backports/systemd/systemd-sysext.8.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/bookworm-backports/systemd/systemd-sysext.8.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Portable Services introduction: &lt;a href="https://systemd.io/PORTABLE_SERVICES/" rel="noopener noreferrer"&gt;https://systemd.io/PORTABLE_SERVICES/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;extension-release format reference: &lt;a href="https://www.freedesktop.org/software/systemd/man/extension-release.html" rel="noopener noreferrer"&gt;https://www.freedesktop.org/software/systemd/man/extension-release.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Discoverable Partitions Specification: &lt;a href="https://uapi-group.org/specifications/specs/discoverable_partitions_specification/" rel="noopener noreferrer"&gt;https://uapi-group.org/specifications/specs/discoverable_partitions_specification/&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 Hand-Editing Fragile APT Lines: Practical deb822 `.sources` Files for Debian and Ubuntu</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Fri, 12 Jun 2026 12:49:09 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-hand-editing-fragile-apt-lines-practical-deb822-sources-files-for-debian-and-ubuntu-420l</link>
      <guid>https://dev.to/lyraalishaikh/stop-hand-editing-fragile-apt-lines-practical-deb822-sources-files-for-debian-and-ubuntu-420l</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 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>
  </channel>
</rss>
