<?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.us-east-2.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 Fighting cron: Practical systemd.timer Units on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Fri, 09 Oct 2026 05:02:13 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-fighting-cron-practical-systemdtimer-units-on-linux-1d5m</link>
      <guid>https://dev.to/lyraalishaikh/stop-fighting-cron-practical-systemdtimer-units-on-linux-1d5m</guid>
      <description>&lt;p&gt;You still have a crontab that 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="c"&gt;# m h  dom mon dow  command&lt;/span&gt;
0 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/local/bin/backup.sh &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /var/log/backup.log 2&amp;gt;&amp;amp;1
&lt;span class="k"&gt;*&lt;/span&gt;/15 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/local/bin/health-check.sh
@reboot /usr/local/bin/warmup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works until the host is powered off at 02:00, until two jobs collide at the top of the hour, until you need cgroup memory caps, until you want journald instead of a hand-rolled log file, or until someone asks “did last night’s job actually run?”&lt;/p&gt;

&lt;p&gt;systemd already has a first-class answer: &lt;strong&gt;&lt;code&gt;.timer&lt;/code&gt; units&lt;/strong&gt;. A timer owns &lt;em&gt;when&lt;/em&gt;. A matching service owns &lt;em&gt;what&lt;/em&gt;. You get the same journal, sandbox, credentials, and resource knobs you already use for daemons — plus calendar expressions that are easier to test than five-field cron strings.&lt;/p&gt;

&lt;p&gt;This guide is operational. Behavior and defaults come from &lt;code&gt;systemd.timer(5)&lt;/code&gt;, &lt;code&gt;systemd.time(7)&lt;/code&gt;, &lt;code&gt;systemd.service(5)&lt;/code&gt;, &lt;code&gt;systemd.exec(5)&lt;/code&gt;, &lt;code&gt;systemctl(1)&lt;/code&gt;, and stock units on &lt;strong&gt;systemd 257&lt;/strong&gt; (verified on Debian 13 / &lt;code&gt;257.13&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What a timer unit is (and is not)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.timer&lt;/code&gt; unit&lt;/td&gt;
&lt;td&gt;Schedule activation (calendar wall-clock and/or monotonic spans)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matching &lt;code&gt;.service&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Do the work (usually &lt;code&gt;Type=oneshot&lt;/code&gt; for jobs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OnCalendar=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Realtime / wall-clock calendar events (&lt;code&gt;systemd.time(7)&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;OnBootSec=&lt;/code&gt; / &lt;code&gt;OnUnitActiveSec=&lt;/code&gt; / …&lt;/td&gt;
&lt;td&gt;Monotonic timers relative to boot, unit activity, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Filesystem path activation (&lt;code&gt;inotify&lt;/code&gt;), not time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.socket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Connection / datagram / FIFO activation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;classic &lt;code&gt;cron&lt;/code&gt; / &lt;code&gt;anacron&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;External schedulers; still fine, but outside systemd’s unit graph&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A timer unit does &lt;strong&gt;not&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;restart an already-active service when it elapses (it leaves the running unit alone),&lt;/li&gt;
&lt;li&gt;invent “cron mail” by itself (pipe output to your notifier if you need that),&lt;/li&gt;
&lt;li&gt;guarantee sub-millisecond wall-clock precision by default (&lt;code&gt;AccuracySec=&lt;/code&gt; defaults to &lt;code&gt;1min&lt;/code&gt; for power coalescing).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It &lt;strong&gt;does&lt;/strong&gt; replace most “run this script every night / every N minutes / once after boot” entries, with better observability and dependency ordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mental model
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;You install &lt;code&gt;foo.timer&lt;/code&gt; and &lt;code&gt;foo.service&lt;/code&gt; (same basename by default).&lt;/li&gt;
&lt;li&gt;You &lt;strong&gt;enable and start the timer unit&lt;/strong&gt;, not the service.&lt;/li&gt;
&lt;li&gt;When the timer elapses, systemd starts &lt;code&gt;foo.service&lt;/code&gt; (unless it is already active).&lt;/li&gt;
&lt;li&gt;Timer units automatically get a &lt;code&gt;Before=&lt;/code&gt; dependency on the service they activate.&lt;/li&gt;
&lt;li&gt;Calendar timers (&lt;code&gt;OnCalendar=&lt;/code&gt;) also order after &lt;code&gt;time-set.target&lt;/code&gt; and &lt;code&gt;time-sync.target&lt;/code&gt; so they do not fire before the clock is sane.&lt;/li&gt;
&lt;li&gt;Enable with &lt;code&gt;WantedBy=timers.target&lt;/code&gt; so they come up on boot.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the service uses &lt;code&gt;RemainAfterExit=yes&lt;/code&gt;, a repetitive timer is usually a bad fit: the service stays “active” after the first run, and later elapses do nothing. Prefer plain oneshots for recurring jobs, or set &lt;code&gt;StopWhenUnneeded=yes&lt;/code&gt; on target units you deliberately re-activate.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--version&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1
&lt;span class="c"&gt;# systemd 257 (… )&lt;/span&gt;

man systemd.timer
man systemd.time
systemctl list-timers &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a typical Debian/Ubuntu host you already have stock examples:&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
&lt;span class="c"&gt;# OnCalendar=weekly&lt;/span&gt;
&lt;span class="c"&gt;# AccuracySec=1h&lt;/span&gt;
&lt;span class="c"&gt;# Persistent=true&lt;/span&gt;
&lt;span class="c"&gt;# RandomizedDelaySec=100min&lt;/span&gt;

systemctl &lt;span class="nb"&gt;cat &lt;/span&gt;systemd-tmpfiles-clean.timer
&lt;span class="c"&gt;# OnBootSec=15min&lt;/span&gt;
&lt;span class="c"&gt;# OnUnitActiveSec=1d&lt;/span&gt;

systemctl &lt;span class="nb"&gt;cat &lt;/span&gt;apt-daily.timer
&lt;span class="c"&gt;# OnCalendar=*-*-* 6,18:00&lt;/span&gt;
&lt;span class="c"&gt;# RandomizedDelaySec=12h&lt;/span&gt;
&lt;span class="c"&gt;# Persistent=true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Calendar vs monotonic timers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;OnCalendar=&lt;/code&gt; (wall clock)
&lt;/h3&gt;

&lt;p&gt;Realtime timers use calendar event expressions from &lt;code&gt;systemd.time(7)&lt;/code&gt;. They care about the wall clock and timezone. If the machine was asleep when a calendar event elapsed, systemd catches up on resume (one activation even if the expression matched multiple times while sleeping).&lt;/p&gt;

&lt;p&gt;Shorthands you will actually use:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shorthand&lt;/th&gt;
&lt;th&gt;Normalized form&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;minutely&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*-*-* *:*:00&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hourly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*-*-* *:00:00&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;daily&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*-*-* 00:00:00&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;weekly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mon *-*-* 00:00:00&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;monthly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*-*-01 00:00:00&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;yearly&lt;/code&gt; / &lt;code&gt;annually&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*-01-01 00:00:00&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;quarterly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*-01,04,07,10-01 00:00:00&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;semiannually&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*-01,07-01 00:00:00&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Useful patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mon *-*-* 09:00:00          # every Monday 09:00 local time
*-*-* 02:30:00              # every day at 02:30
*-*-* 6,18:00               # 06:00 and 18:00 daily (apt-daily style)
Fri *-*-* 17:00:00 UTC      # fixed timezone
*-02~03                     # third-last day of February
Mon *-05~07/1               # last Monday in May
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Always validate&lt;/strong&gt; expressions before shipping them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze calendar &lt;span class="s1"&gt;'daily'&lt;/span&gt;
systemd-analyze calendar &lt;span class="s1"&gt;'Mon *-*-* 09:00:00'&lt;/span&gt;
systemd-analyze calendar &lt;span class="s1"&gt;'*-*-* 02:30:00'&lt;/span&gt;
systemd-analyze calendar &lt;span class="s1"&gt;'weekly'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output shape (times will differ on your host):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Original form: daily
Normalized form: *-*-* 00:00:00
    Next elapse: Sat 2026-10-10 00:00:00 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On systems without a battery-backed RTC, enable &lt;code&gt;systemd-time-wait-sync.service&lt;/code&gt; so calendar timers wait for a network time fix. Timer units with any &lt;code&gt;OnCalendar=&lt;/code&gt; are already ordered after &lt;code&gt;time-sync.target&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monotonic timers (relative spans)
&lt;/h3&gt;

&lt;p&gt;These ignore wall clock and timezone. Values are time spans (&lt;code&gt;systemd.time(7)&lt;/code&gt;): &lt;code&gt;50&lt;/code&gt;, &lt;code&gt;5h 30min&lt;/code&gt;, &lt;code&gt;1d&lt;/code&gt;, …&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Relative to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OnActiveSec=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When the &lt;strong&gt;timer unit itself&lt;/strong&gt; was activated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OnBootSec=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Machine boot (in containers for the system manager, mapped to &lt;code&gt;OnStartupSec=&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OnStartupSec=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When the service manager first started (important for &lt;strong&gt;user&lt;/strong&gt; timers after login)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OnUnitActiveSec=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When the &lt;strong&gt;triggered unit&lt;/strong&gt; was last activated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OnUnitInactiveSec=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When the &lt;strong&gt;triggered unit&lt;/strong&gt; was last deactivated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notes from &lt;code&gt;systemd.timer(5)&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You may combine multiple directives (and mix them with &lt;code&gt;OnCalendar=&lt;/code&gt;). The timer fires when &lt;strong&gt;any&lt;/strong&gt; expression elapses.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OnBootSec=&lt;/code&gt; / &lt;code&gt;OnStartupSec=&lt;/code&gt; that are already in the past when the timer activates fire &lt;strong&gt;immediately&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Monotonic clocks normally &lt;strong&gt;pause during suspend&lt;/strong&gt;. With &lt;code&gt;WakeSystem=yes&lt;/code&gt;, systemd uses a clock that keeps advancing while suspended (&lt;code&gt;CLOCK_BOOTTIME&lt;/code&gt;) so wake-from-sleep can work.&lt;/li&gt;
&lt;li&gt;Empty string assignment clears &lt;strong&gt;all&lt;/strong&gt; configured timers (monotonic and calendar).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Validate spans 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-analyze timespan &lt;span class="s1"&gt;'5h 30min'&lt;/span&gt;
systemd-analyze timespan &lt;span class="s1"&gt;'15min'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  AccuracySec= vs RandomizedDelaySec= (do not confuse them)
&lt;/h2&gt;

&lt;p&gt;These two knobs sound similar and do opposite jobs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AccuracySec=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1min&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Power saving: allow coalescing wakeups inside a window after the nominal elapse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RandomizedDelaySec=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Spread load: add a fresh random delay in &lt;code&gt;[0, value]&lt;/code&gt; before each firing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FixedRandomDelay=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;false&lt;/code&gt; (247+)&lt;/td&gt;
&lt;td&gt;If true with a non-zero random delay, pick a &lt;strong&gt;stable&lt;/strong&gt; delay from machine ID + timer name&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;How they compose (&lt;code&gt;systemd.timer(5)&lt;/code&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Apply &lt;code&gt;RandomizedDelaySec=&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;Then possibly shift further within &lt;code&gt;AccuracySec=&lt;/code&gt; so multiple timers can share a wakeup.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Practical recipes:&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;# Tight wall-clock job (monitoring scrape, market open, …)
&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;1us&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;0&lt;/span&gt;

&lt;span class="c"&gt;# Fleet-friendly maintenance (like fstrim / apt)
&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;RandomizedDelaySec&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="c"&gt;# Stretch a busy hour without coalescing back together
&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;1us&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;30min&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fstrim.timer&lt;/code&gt; is the textbook “be kind to the fleet” pattern: weekly calendar, hour accuracy, up to 100 minutes of random delay, and &lt;code&gt;Persistent=true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persistent= — catch up after downtime
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&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;daily&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;Persistent=true&lt;/code&gt; (calendar timers only), systemd stores the last trigger timestamp on disk. If the machine was off across a missed window, the service runs once on timer activation (still subject to &lt;code&gt;RandomizedDelaySec=&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Cleanup / uninstall:&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 clean &lt;span class="nt"&gt;--what&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;state your-job.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use that before removing a persistent timer so the timestamp file does not linger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other important timer knobs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Unit=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Activate a different unit than &lt;code&gt;basename.service&lt;/code&gt; (suffix must not be &lt;code&gt;.timer&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WakeSystem=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resume from suspend when the timer elapses (needs hardware support; system manager)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RemainAfterElapse=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default &lt;code&gt;true&lt;/code&gt;: keep the timer loaded after a one-shot schedule finishes. Set &lt;code&gt;false&lt;/code&gt; for many transient timers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;OnClockChange=&lt;/code&gt; / &lt;code&gt;OnTimezoneChange=&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Boolean triggers on realtime clock jumps or timezone changes (242+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DeferReactivation=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;257+&lt;/strong&gt;, calendar only: schedule the next elapse from when the service goes inactive, instead of immediately re-firing if the job outlasted the interval&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;DeferReactivation=yes&lt;/code&gt; is the fix for “my 5-minute timer stampeded because the job took 12 minutes.” Default behavior queues the next elapse from the previous trigger time; if that moment is already past when the service finishes, systemd starts it again immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 1 — Daily oneshot with Persistent catch-up
&lt;/h2&gt;

&lt;p&gt;A small homelab backup-style job. The service is intentionally boring so the timer behavior is obvious.&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/systemd/system/lyra-daily-ping.service &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;'
[Unit]
Description=Lyra daily timer demo (oneshot)

[Service]
Type=oneshot
ExecStart=/bin/bash -c 'echo "lyra-daily-ping ran at &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;--iso-8601&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;seconds&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;; trigger=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TRIGGER_UNIT&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;none&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt; realtime_usec=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TRIGGER_TIMER_REALTIME_USEC&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="p"&gt;/a&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;" | tee -a /var/log/lyra-daily-ping.log'
# Optional hardening — same knobs as any service:
# ProtectSystem=strict
# ProtectHome=true
# PrivateTmp=true
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/lyra-daily-ping.timer &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;'
[Unit]
Description=Run lyra-daily-ping once per day (catch up if the host was off)

[Timer]
OnCalendar=daily
Persistent=true
AccuracySec=1min
RandomizedDelaySec=15min
# Unit=lyra-daily-ping.service   # default by basename

[Install]
WantedBy=timers.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&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; lyra-daily-ping.timer
systemctl list-timers lyra-daily-ping.timer
systemctl status lyra-daily-ping.timer &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Force a run without waiting for midnight:&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 start lyra-daily-ping.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; lyra-daily-ping.service &lt;span class="nt"&gt;-n&lt;/span&gt; 20 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When systemd activates the service &lt;strong&gt;from the timer&lt;/strong&gt;, &lt;code&gt;systemd.exec(5)&lt;/code&gt; may provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;$TRIGGER_UNIT&lt;/code&gt; — the timer name&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$TRIGGER_TIMER_REALTIME_USEC&lt;/code&gt; / &lt;code&gt;$TRIGGER_TIMER_MONOTONIC_USEC&lt;/code&gt; — elapse timestamps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are best-effort (coalesced triggers can collapse). Still useful for logs and metrics labels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 2 — Boot + every-hour monotonic maintenance
&lt;/h2&gt;

&lt;p&gt;Mirror the &lt;code&gt;systemd-tmpfiles-clean.timer&lt;/code&gt; pattern: run soon after boot, then repeatedly based on last activation.&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/systemd/system/lyra-hourly-health.service &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;'
[Unit]
Description=Lyra hourly health snapshot

[Service]
Type=oneshot
ExecStart=/bin/bash -c 'printf "loadavg=%s disk=%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt; &lt;span class="nt"&gt;-f1-3&lt;/span&gt; /proc/loadavg&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; / | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s2"&gt;"END{print &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;5}"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;" &amp;gt;&amp;gt; /var/log/lyra-hourly-health.log'
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/lyra-hourly-health.timer &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;'
[Unit]
Description=Hourly health snapshot (boot + periodic)

[Timer]
OnBootSec=5min
OnUnitActiveSec=1h
AccuracySec=1min

[Install]
WantedBy=timers.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&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; lyra-hourly-health.timer
systemctl list-timers lyra-hourly-health.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why &lt;code&gt;OnUnitActiveSec=&lt;/code&gt; instead of only &lt;code&gt;OnCalendar=hourly&lt;/code&gt;?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is independent of wall-clock skew.&lt;/li&gt;
&lt;li&gt;Combined with &lt;code&gt;OnBootSec=&lt;/code&gt;, you get a guaranteed first run after boot without waiting for the next wall-clock hour.&lt;/li&gt;
&lt;li&gt;It measures from the service’s last activation, which is often what operators mean by “every hour of uptime.”&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lab 3 — User timer that survives login (with linger)
&lt;/h2&gt;

&lt;p&gt;User timers live with &lt;code&gt;systemd --user&lt;/code&gt;. They need a user manager that stays up — see lingering if the job must run without an interactive session.&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

&lt;span class="nb"&gt;tee&lt;/span&gt; ~/.config/systemd/user/lyra-user-mail-sync.service &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;'
[Unit]
Description=Lyra user mail sync demo

[Service]
Type=oneshot
ExecStart=/bin/bash -c 'echo "user sync at &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;--iso-8601&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;seconds&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;" &amp;gt;&amp;gt; %h/.cache/lyra-user-mail-sync.log'
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;tee&lt;/span&gt; ~/.config/systemd/user/lyra-user-mail-sync.timer &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;'
[Unit]
Description=Sync mail every 15 minutes while the user manager runs

[Timer]
OnStartupSec=2min
OnUnitActiveSec=15min
AccuracySec=1min

[Install]
WantedBy=timers.target
&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; &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; lyra-user-mail-sync.timer
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; list-timers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the job must run even when you are logged out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;loginctl enable-linger &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# user@.service now starts at boot; user timers can fire without a seat&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer &lt;code&gt;OnStartupSec=&lt;/code&gt; over &lt;code&gt;OnBootSec=&lt;/code&gt; in user units: the user manager often starts at first login (or at boot only with linger), not at kernel boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Long-running jobs and DeferReactivation= (257+)
&lt;/h2&gt;

&lt;p&gt;If a calendar timer interval is shorter than the job runtime, default scheduling can immediately re-trigger when the service exits (the “next” elapse is already in the past).&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;[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;*:0/5&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;1us&lt;/span&gt;
&lt;span class="py"&gt;DeferReactivation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;DeferReactivation=yes&lt;/code&gt;, systemd schedules the next elapse from when the trigger unit becomes inactive, then waits for the next realtime calendar match. That prevents stampede loops on slow backups and package metadata downloads.&lt;/p&gt;

&lt;p&gt;On releases before 257, mitigate by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;making the interval longer than worst-case runtime,&lt;/li&gt;
&lt;li&gt;using a non-&lt;code&gt;oneshot&lt;/code&gt; service with locking (&lt;code&gt;flock&lt;/code&gt;) so overlapping starts no-op,&lt;/li&gt;
&lt;li&gt;or using monotonic &lt;code&gt;OnUnitInactiveSec=&lt;/code&gt; (“N after the last run finished”).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Operations cheat sheet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Inventory&lt;/span&gt;
systemctl list-timers &lt;span class="nt"&gt;--all&lt;/span&gt;
systemctl list-timers &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt;

&lt;span class="c"&gt;# Inspect&lt;/span&gt;
systemctl &lt;span class="nb"&gt;cat &lt;/span&gt;fstrim.timer
systemctl show fstrim.timer &lt;span class="nt"&gt;-p&lt;/span&gt; NextElapseUSecRealtime &lt;span class="nt"&gt;-p&lt;/span&gt; LastTriggerUSec &lt;span class="nt"&gt;-p&lt;/span&gt; Triggers
systemctl status fstrim.timer &lt;span class="nt"&gt;--no-pager&lt;/span&gt;

&lt;span class="c"&gt;# Manual fire of the job (not the schedule)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start fstrim.service

&lt;span class="c"&gt;# Temporarily stop scheduling without deleting units&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl disable &lt;span class="nt"&gt;--now&lt;/span&gt; lyra-daily-ping.timer

&lt;span class="c"&gt;# Persistent= state cleanup&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl clean &lt;span class="nt"&gt;--what&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;state lyra-daily-ping.timer

&lt;span class="c"&gt;# Logs&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; lyra-daily-ping.service &lt;span class="nt"&gt;-u&lt;/span&gt; lyra-daily-ping.timer &lt;span class="nt"&gt;--since&lt;/span&gt; today
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;systemctl list-timers&lt;/code&gt; columns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NEXT&lt;/strong&gt; / &lt;strong&gt;LEFT&lt;/strong&gt; — next scheduled elapse&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LAST&lt;/strong&gt; / &lt;strong&gt;PASSED&lt;/strong&gt; — last trigger&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UNIT&lt;/strong&gt; — timer name&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ACTIVATES&lt;/strong&gt; — the unit that will be started&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Failure modes worth knowing
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Service already active&lt;/strong&gt; — elapse is ignored; no second instance. Do not use &lt;code&gt;RemainAfterExit=yes&lt;/code&gt; oneshots for repetitive timers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong basename / missing &lt;code&gt;Unit=&lt;/code&gt;&lt;/strong&gt; — timer elapses, nothing useful starts. Keep names paired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enabled the service instead of the timer&lt;/strong&gt; — job ran once (or at boot via &lt;code&gt;[Install]&lt;/code&gt;), schedule never armed. Enable &lt;code&gt;*.timer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calendar fire before clock sync&lt;/strong&gt; — missing RTC + no &lt;code&gt;time-wait-sync&lt;/code&gt; yields surprising “missed” or early runs. Order after &lt;code&gt;time-sync.target&lt;/code&gt; is automatic for &lt;code&gt;OnCalendar=&lt;/code&gt;, but the sync service must exist and run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timezone surprises&lt;/strong&gt; — bare &lt;code&gt;09:00&lt;/code&gt; is local time. Pin &lt;code&gt;UTC&lt;/code&gt; in the expression when fleets span regions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suspend&lt;/strong&gt; — calendar events catch up on resume (once). Monotonic timers pause unless &lt;code&gt;WakeSystem=&lt;/code&gt; changes the clock basis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coalesced accuracy&lt;/strong&gt; — “why did it run at 00:37 instead of 00:00?” Because &lt;code&gt;AccuracySec=&lt;/code&gt; defaults to a minute and may be raised (for example &lt;code&gt;1h&lt;/code&gt; on &lt;code&gt;fstrim.timer&lt;/code&gt;). Tighten only when you need to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaving Persistent= state behind&lt;/strong&gt; — uninstall without &lt;code&gt;systemctl clean --what=state&lt;/code&gt; can surprise you if you later reinstall the same unit name.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How this differs from cron
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;cron&lt;/th&gt;
&lt;th&gt;systemd.timer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Schedule definition&lt;/td&gt;
&lt;td&gt;five fields / &lt;code&gt;@reboot&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;OnCalendar=&lt;/code&gt; + monotonic spans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missed runs while powered off&lt;/td&gt;
&lt;td&gt;needs anacron or custom logic&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Persistent=true&lt;/code&gt; on calendar timers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;free-for-all redirection&lt;/td&gt;
&lt;td&gt;journald unit logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource limits / sandbox&lt;/td&gt;
&lt;td&gt;external wrappers&lt;/td&gt;
&lt;td&gt;native cgroup + &lt;code&gt;Protect*=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;weak (&lt;code&gt;After&lt;/code&gt; via hacky scripts)&lt;/td&gt;
&lt;td&gt;real unit graph (&lt;code&gt;After=&lt;/code&gt;, &lt;code&gt;Requires=&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Randomized herd avoidance&lt;/td&gt;
&lt;td&gt;sleep $RANDOM hacks&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RandomizedDelaySec=&lt;/code&gt; / &lt;code&gt;FixedRandomDelay=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User session jobs&lt;/td&gt;
&lt;td&gt;crond + user crontab quirks&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemctl --user&lt;/code&gt; + optional linger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testability&lt;/td&gt;
&lt;td&gt;“wait and see”&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemd-analyze calendar&lt;/code&gt; / &lt;code&gt;timespan&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;cron is not evil. For a single workstation muscle-memory entry it is fine. The moment the job is part of a host’s service graph, timers are the cleaner tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundary vs related unit types
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Unit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time / calendar / boot-relative schedule&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;.timer&lt;/code&gt;&lt;/strong&gt; (this article)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directory / file events&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.path&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incoming connection / socket&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.socket&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block sleep/shutdown while a job runs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemd-inhibit&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create &lt;code&gt;/run&lt;/code&gt; trees the job needs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemd-tmpfiles&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-off ad-hoc schedule in a shell&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemd-run --on-calendar=…&lt;/code&gt; / &lt;code&gt;--on-active=…&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Transient example when you do not want unit files on disk:&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-run &lt;span class="nt"&gt;--on-calendar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*-*-* 23:30:00'&lt;/span&gt; &lt;span class="nt"&gt;--unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lyra-transient-ping &lt;span class="se"&gt;\&lt;/span&gt;
  /bin/echo &lt;span class="s1"&gt;'transient calendar fire'&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl list-timers lyra-transient-ping.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cleanup the labs
&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;systemctl disable &lt;span class="nt"&gt;--now&lt;/span&gt; lyra-daily-ping.timer lyra-hourly-health.timer 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
sudo &lt;/span&gt;systemctl clean &lt;span class="nt"&gt;--what&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;state lyra-daily-ping.timer 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/systemd/system/lyra-daily-ping.&lt;span class="o"&gt;{&lt;/span&gt;service,timer&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
           /etc/systemd/system/lyra-hourly-health.&lt;span class="o"&gt;{&lt;/span&gt;service,timer&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload

systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; disable &lt;span class="nt"&gt;--now&lt;/span&gt; lyra-user-mail-sync.timer 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.config/systemd/user/lyra-user-mail-sync.&lt;span class="o"&gt;{&lt;/span&gt;service,timer&lt;span class="o"&gt;}&lt;/span&gt;
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd.timer(5)&lt;/code&gt; — timer unit options (&lt;code&gt;OnCalendar=&lt;/code&gt;, monotonic timers, &lt;code&gt;Persistent=&lt;/code&gt;, &lt;code&gt;AccuracySec=&lt;/code&gt;, &lt;code&gt;RandomizedDelaySec=&lt;/code&gt;, &lt;code&gt;FixedRandomDelay=&lt;/code&gt;, &lt;code&gt;WakeSystem=&lt;/code&gt;, &lt;code&gt;DeferReactivation=&lt;/code&gt;, …)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.time(7)&lt;/code&gt; — time spans, timestamps, calendar event syntax and shorthands&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.service(5)&lt;/code&gt; — &lt;code&gt;Type=oneshot&lt;/code&gt;, &lt;code&gt;RemainAfterExit=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.exec(5)&lt;/code&gt; — &lt;code&gt;$TRIGGER_UNIT&lt;/code&gt;, &lt;code&gt;$TRIGGER_TIMER_REALTIME_USEC&lt;/code&gt;, &lt;code&gt;$TRIGGER_TIMER_MONOTONIC_USEC&lt;/code&gt;, &lt;code&gt;$INVOCATION_ID&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.special(7)&lt;/code&gt; — &lt;code&gt;timers.target&lt;/code&gt;, &lt;code&gt;time-set.target&lt;/code&gt;, &lt;code&gt;time-sync.target&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemctl(1)&lt;/code&gt; — &lt;code&gt;list-timers&lt;/code&gt;, &lt;code&gt;clean --what=state&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-analyze(1)&lt;/code&gt; — &lt;code&gt;calendar&lt;/code&gt;, &lt;code&gt;timespan&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-run(1)&lt;/code&gt; — transient timers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;loginctl(1)&lt;/code&gt; — &lt;code&gt;enable-linger&lt;/code&gt; for user timers without a seat&lt;/li&gt;
&lt;li&gt;Stock units worth reading: &lt;code&gt;fstrim.timer&lt;/code&gt;, &lt;code&gt;apt-daily.timer&lt;/code&gt;, &lt;code&gt;systemd-tmpfiles-clean.timer&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ship the schedule as a timer, ship the work as a service, and stop debugging &lt;code&gt;MAILTO&lt;/code&gt; and redirected log files at 02:15.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Booting Into a Dead Mount: Practical systemd.mount Units on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Thu, 08 Oct 2026 05:06:19 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-booting-into-a-dead-mount-practical-systemdmount-units-on-linux-2i8h</link>
      <guid>https://dev.to/lyraalishaikh/stop-booting-into-a-dead-mount-practical-systemdmount-units-on-linux-2i8h</guid>
      <description>&lt;p&gt;You add a secondary disk for &lt;code&gt;/data&lt;/code&gt;, drop a line in &lt;code&gt;/etc/fstab&lt;/code&gt;, and reboot. One of three things happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Everything mounts cleanly.&lt;/li&gt;
&lt;li&gt;Boot stalls for the default start timeout while systemd waits on a missing UUID.&lt;/li&gt;
&lt;li&gt;The machine comes up, but a critical path is empty and services write into the underlying directory instead of the volume you meant to attach.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;fstab&lt;/code&gt; is still the right human-facing format. Under systemd it is not “just mount(8) at boot” — &lt;code&gt;systemd-fstab-generator&lt;/code&gt; turns each entry into a native &lt;code&gt;.mount&lt;/code&gt; (and sometimes &lt;code&gt;.automount&lt;/code&gt; / &lt;code&gt;.swap&lt;/code&gt;) unit with real dependency edges, timeouts, and failure semantics.&lt;/p&gt;

&lt;p&gt;This guide is operational. Behavior comes from &lt;code&gt;systemd.mount(5)&lt;/code&gt;, &lt;code&gt;systemd-fstab-generator(8)&lt;/code&gt;, &lt;code&gt;systemd-mount(1)&lt;/code&gt;, &lt;code&gt;systemd-escape(1)&lt;/code&gt;, and stock units on &lt;strong&gt;systemd 257&lt;/strong&gt; (verified on Debian 13).&lt;/p&gt;

&lt;h2&gt;
  
  
  What a mount unit is (and is not)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.mount&lt;/code&gt; unit&lt;/td&gt;
&lt;td&gt;Mount one &lt;code&gt;What=&lt;/code&gt; source on one &lt;code&gt;Where=&lt;/code&gt; path under systemd job control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemd-fstab-generator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Translate &lt;code&gt;/etc/fstab&lt;/code&gt; → &lt;code&gt;.mount&lt;/code&gt; / &lt;code&gt;.swap&lt;/code&gt; early at boot and on reload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.automount&lt;/code&gt; unit&lt;/td&gt;
&lt;td&gt;Optional on-demand / idle-unmount wrapper that activates a matching &lt;code&gt;.mount&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemd-mount(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create &lt;strong&gt;transient&lt;/strong&gt; &lt;code&gt;.mount&lt;/code&gt; / &lt;code&gt;.automount&lt;/code&gt; units from the CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.path&lt;/code&gt; / &lt;code&gt;.socket&lt;/code&gt; / &lt;code&gt;.timer&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Other activation types — paths, listeners, schedules — not filesystem mounts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A mount unit does &lt;strong&gt;not&lt;/strong&gt; replace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LUKS unlock (&lt;code&gt;crypttab&lt;/code&gt; / &lt;code&gt;systemd-cryptenroll&lt;/code&gt;) or integrity layers (&lt;code&gt;dm-verity&lt;/code&gt;, &lt;code&gt;dm-integrity&lt;/code&gt;),&lt;/li&gt;
&lt;li&gt;volume managers deciding &lt;em&gt;which&lt;/em&gt; block device is the source,&lt;/li&gt;
&lt;li&gt;NFS-only “stop blocking boot with remote shares” workflows that live primarily on &lt;code&gt;.automount&lt;/code&gt; (covered separately).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It &lt;strong&gt;does&lt;/strong&gt; replace “hope &lt;code&gt;mount -a&lt;/code&gt; in a boot script is enough,” and it makes every mount visible to &lt;code&gt;systemctl&lt;/code&gt;, the journal, and ordering against &lt;code&gt;local-fs.target&lt;/code&gt; / &lt;code&gt;remote-fs.target&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mental model
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Unit &lt;strong&gt;name must match the mount point&lt;/strong&gt;, escaped: &lt;code&gt;/data/backup&lt;/code&gt; → &lt;code&gt;data-backup.mount&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[Mount]&lt;/code&gt; needs at least &lt;code&gt;What=&lt;/code&gt; and &lt;code&gt;Where=&lt;/code&gt;. Optional: &lt;code&gt;Type=&lt;/code&gt;, &lt;code&gt;Options=&lt;/code&gt;, &lt;code&gt;TimeoutSec=&lt;/code&gt;, &lt;code&gt;DirectoryMode=&lt;/code&gt;, …&lt;/li&gt;
&lt;li&gt;Local mounts order into &lt;code&gt;local-fs.target&lt;/code&gt;. Network mounts (by FS type or &lt;code&gt;_netdev&lt;/code&gt;) order into &lt;code&gt;remote-fs.target&lt;/code&gt; and pull &lt;code&gt;network-online.target&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nofail&lt;/code&gt; turns the dependency into a weak &lt;code&gt;Wants=&lt;/code&gt; and drops ordering before the fs target — boot continues if the volume is gone.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;noauto&lt;/code&gt; means nothing pulls the mount at boot unless another unit does.&lt;/li&gt;
&lt;li&gt;Parent mounts imply child requirements automatically (hierarchy edges).&lt;/li&gt;
&lt;li&gt;Runtime mounts (USB sticks, manual &lt;code&gt;mount&lt;/code&gt;) still show up as mount units via &lt;code&gt;/proc/self/mountinfo&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--version&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1
&lt;span class="c"&gt;# systemd 257 (… Debian 13)&lt;/span&gt;

man systemd.mount
man systemd-fstab-generator
man systemd-mount
man systemd-escape

systemctl list-units &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mount &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stock examples already on a typical host:&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;tmp.mount
&lt;span class="c"&gt;# [Mount]&lt;/span&gt;
&lt;span class="c"&gt;# What=tmpfs&lt;/span&gt;
&lt;span class="c"&gt;# Where=/tmp&lt;/span&gt;
&lt;span class="c"&gt;# Type=tmpfs&lt;/span&gt;
&lt;span class="c"&gt;# Options=mode=1777,strictatime,nosuid,nodev,size=50%%,nr_inodes=1m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the doubled &lt;code&gt;%%&lt;/code&gt; in unit files — percent is a specifier character; a literal &lt;code&gt;%&lt;/code&gt; must be written &lt;code&gt;%%&lt;/code&gt; (&lt;code&gt;systemd.mount(5)&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Naming: always use systemd-escape
&lt;/h2&gt;

&lt;p&gt;Hand-rolling unit names is how you get &lt;code&gt;not-found&lt;/code&gt; units and silent mismatches.&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;# Mount unit for a path&lt;/span&gt;
systemd-escape &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;--suffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mount &lt;span class="s2"&gt;"/data/backup"&lt;/span&gt;
&lt;span class="c"&gt;# data-backup.mount&lt;/span&gt;

systemd-escape &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;--suffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mount &lt;span class="s2"&gt;"/mnt/My Disk"&lt;/span&gt;
&lt;span class="c"&gt;# mnt-My\x20Disk.mount&lt;/span&gt;

&lt;span class="c"&gt;# Undo&lt;/span&gt;
systemd-escape &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="s1"&gt;'data-backup'&lt;/span&gt;
&lt;span class="c"&gt;# data/backup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules that matter in practice (&lt;code&gt;systemd.unit(5)&lt;/code&gt; / &lt;code&gt;systemd-escape(1)&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Leading &lt;code&gt;/&lt;/code&gt; is dropped; remaining &lt;code&gt;/&lt;/code&gt; become &lt;code&gt;-&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;-p&lt;/code&gt; / &lt;code&gt;--path&lt;/code&gt; so &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;//&lt;/code&gt;, and trailing slashes are normalized first.&lt;/li&gt;
&lt;li&gt;Mount units &lt;strong&gt;cannot be templated&lt;/strong&gt; and cannot gain extra names via symlinks.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Where=&lt;/code&gt; must be an absolute path and &lt;strong&gt;must not be a symlink&lt;/strong&gt; (the generator resolves fstab symlink targets for compatibility; native units refuse symlink destinations).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Anatomy of a .mount unit
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/data-lyra.mount
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Lyra data volume&lt;/span&gt;
&lt;span class="py"&gt;Documentation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;man:systemd.mount(5)&lt;/span&gt;
&lt;span class="c"&gt;# Optional explicit ordering beyond the defaults:
# After=local-fs-pre.target
# Before=local-fs.target
&lt;/span&gt;
&lt;span class="nn"&gt;[Mount]&lt;/span&gt;
&lt;span class="py"&gt;What&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/dev/disk/by-uuid/11111111-2222-3333-4444-555555555555&lt;/span&gt;
&lt;span class="py"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/data/lyra&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ext4&lt;/span&gt;
&lt;span class="py"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;defaults,noatime&lt;/span&gt;
&lt;span class="c"&gt;# DirectoryMode=0755
# TimeoutSec=30s
# ReadWriteOnly=no
# LazyUnmount=no
# ForceUnmount=no
# SloppyOptions=no
&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;local-fs.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;What=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Device node, UUID/LABEL-style source, bind path, or &lt;code&gt;tmpfs&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Where=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Absolute mount point; must match unit name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Type=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Filesystem type (&lt;code&gt;ext4&lt;/code&gt;, &lt;code&gt;xfs&lt;/code&gt;, &lt;code&gt;btrfs&lt;/code&gt;, &lt;code&gt;nfs&lt;/code&gt;, &lt;code&gt;tmpfs&lt;/code&gt;, …)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Options=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Comma-separated mount options (specifiers expanded)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TimeoutSec=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How long &lt;code&gt;mount&lt;/code&gt; may run before SIGTERM/SIGKILL failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DirectoryMode=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mode when systemd creates &lt;code&gt;Where=&lt;/code&gt; (default &lt;code&gt;0755&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ReadWriteOnly=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;If true, do &lt;strong&gt;not&lt;/strong&gt; fall back to read-only after RW failure (&lt;code&gt;x-systemd.rw-only&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LazyUnmount=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;umount -l&lt;/code&gt; semantics on stop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ForceUnmount=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;umount -f&lt;/code&gt; (classic unreachable-NFS hammer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SloppyOptions=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tolerate unknown options (&lt;code&gt;mount -s&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;User=&lt;/code&gt; / &lt;code&gt;Group=&lt;/code&gt; are not useful here: systemd invokes &lt;code&gt;mount(8)&lt;/code&gt; with &lt;code&gt;What&lt;/code&gt; and &lt;code&gt;Where&lt;/code&gt; as UID 0 and does not re-read fstab inside that call.&lt;/p&gt;

&lt;h2&gt;
  
  
  fstab is still fine — learn the x-systemd knobs
&lt;/h2&gt;

&lt;p&gt;Humans often prefer &lt;code&gt;/etc/fstab&lt;/code&gt;. Tooling and image builders often prefer unit files under &lt;code&gt;/etc&lt;/code&gt;. Precedence (&lt;code&gt;systemd.mount(5)&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unit in &lt;code&gt;/etc&lt;/code&gt; wins over fstab.&lt;/li&gt;
&lt;li&gt;fstab wins over units shipped under &lt;code&gt;/usr&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generator: &lt;code&gt;/usr/lib/systemd/system-generators/systemd-fstab-generator&lt;/code&gt; (also on &lt;code&gt;daemon-reload&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Options that change boot behavior
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;fstab option&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nofail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Wants=&lt;/code&gt; only; &lt;strong&gt;not&lt;/strong&gt; ordered before &lt;code&gt;local-fs&lt;/code&gt;/&lt;code&gt;remote-fs&lt;/code&gt;; boot continues if mount fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;noauto&lt;/code&gt; / &lt;code&gt;auto&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Do / do not pull from the fs target (&lt;code&gt;x-systemd.automount&lt;/code&gt; overrides this pair)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_netdev&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Force “network mount” ordering even if the type looks local (iSCSI, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x-systemd.device-timeout=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How long to wait for the backing device (fstab-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x-systemd.mount-timeout=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How long the mount command may run (fstab-only → &lt;code&gt;TimeoutSec=&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;x-systemd.requires=&lt;/code&gt; / &lt;code&gt;x-systemd.wants=&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Extra unit/path dependencies on the &lt;strong&gt;mount&lt;/strong&gt; unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;x-systemd.before=&lt;/code&gt; / &lt;code&gt;x-systemd.after=&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Extra ordering on the mount unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;x-systemd.wanted-by=&lt;/code&gt; / &lt;code&gt;x-systemd.required-by=&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Custom pull-in; &lt;strong&gt;suppresses&lt;/strong&gt; default fs-target deps (except &lt;code&gt;umount.target&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;x-systemd.requires-mounts-for=&lt;/code&gt; / &lt;code&gt;wants-mounts-for=&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Path-based mount dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x-systemd.device-bound=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt; → &lt;code&gt;BindsTo=&lt;/code&gt; device; &lt;code&gt;false&lt;/code&gt; → survive device vanish (volume managers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x-systemd.automount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create companion &lt;code&gt;.automount&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x-systemd.idle-timeout=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Automount idle unmount (&lt;code&gt;TimeoutIdleSec=&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x-systemd.makefs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Format if the device has no signature (&lt;code&gt;systemd-makefs@.service&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x-systemd.growfs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Grow FS to fill the block device after mount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x-systemd.pcrfs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Measure FS identity into PCR 15 after mount (253+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x-systemd.rw-only&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No RO fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x-initrd.mount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mount in initrd and keep through host lifetime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Example — secondary data disk that must &lt;strong&gt;not&lt;/strong&gt; stall boot if unplugged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UUID=11111111-2222-3333-4444-555555555555  /data/lyra  ext4  defaults,noatime,nofail,x-systemd.device-timeout=5s,x-systemd.mount-timeout=15s  0  2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example — image appliance partition that should format once and grow forever after repart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/dev/disk/by-partuuid/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee  /var/lib/myapp  ext4  defaults,x-systemd.makefs,x-systemd.growfs  0  2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;x-systemd.makefs&lt;/code&gt; / &lt;code&gt;growfs&lt;/code&gt; / &lt;code&gt;device-timeout&lt;/code&gt; / &lt;code&gt;mount-timeout&lt;/code&gt; / &lt;code&gt;pcrfs&lt;/code&gt; are &lt;strong&gt;fstab-only&lt;/strong&gt; — putting them in a unit file’s &lt;code&gt;Options=&lt;/code&gt; is ignored. In unit files, use the native settings (&lt;code&gt;TimeoutSec=&lt;/code&gt;, companion services, etc.).&lt;/p&gt;

&lt;h3&gt;
  
  
  Network mounts and the old NFS &lt;code&gt;bg&lt;/code&gt; flag
&lt;/h3&gt;

&lt;p&gt;Network FS types get &lt;code&gt;remote-fs-pre.target&lt;/code&gt; → &lt;code&gt;network-online.target&lt;/code&gt; → &lt;code&gt;remote-fs.target&lt;/code&gt; edges. &lt;code&gt;_netdev&lt;/code&gt; forces that path for “local-looking” network block devices.&lt;/p&gt;

&lt;p&gt;Classic NFS &lt;code&gt;bg&lt;/code&gt; is rewritten by the generator roughly as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prepend &lt;code&gt;x-systemd.mount-timeout=infinity,retry=10000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;append &lt;code&gt;fg,nofail&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For on-demand remote shares, &lt;code&gt;x-systemd.automount&lt;/code&gt; is usually clearer than &lt;code&gt;bg&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect what the generator actually built
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl daemon-reload

&lt;span class="c"&gt;# Generated unit text (when the unit is known)&lt;/span&gt;
systemctl &lt;span class="nb"&gt;cat &lt;/span&gt;data-lyra.mount

&lt;span class="c"&gt;# Or peek at generator output directly&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; /run/systemd/generator/&lt;span class="k"&gt;*&lt;/span&gt;.mount 2&amp;gt;/dev/null
systemctl show &lt;span class="nt"&gt;-p&lt;/span&gt; Where,What,Options,Type,TimeoutUSec,Requires,Wants,After,Before data-lyra.mount

findmnt /data/lyra
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; data-lyra.mount &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kernel cmdline escapes for one-off/extra mounts without editing fstab (&lt;code&gt;systemd-fstab-generator(8)&lt;/code&gt;, 254+):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemd.mount-extra=/dev/sda1:/mnt/extra:ext4:rw,noatime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Credential &lt;code&gt;fstab.extra&lt;/code&gt; can carry additional fstab-format lines (254+).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 1 — Local ext4 mount unit on a loop file
&lt;/h2&gt;

&lt;p&gt;Safe on any host with free space under &lt;code&gt;/var/tmp&lt;/code&gt;. No real disks required.&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/tmp/lyra-mount-lab /mnt/lyra-lab
&lt;span class="nb"&gt;sudo &lt;/span&gt;fallocate &lt;span class="nt"&gt;-l&lt;/span&gt; 512M /var/tmp/lyra-mount-lab/disk.img
&lt;span class="nb"&gt;sudo &lt;/span&gt;mkfs.ext4 &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; lyra-lab /var/tmp/lyra-mount-lab/disk.img

&lt;span class="c"&gt;# Stable What= via loop + label after first attach, or mount the file via Type=ext4:&lt;/span&gt;
&lt;span class="nv"&gt;UNIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;systemd-escape &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;--suffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mount /mnt/lyra-lab&lt;span class="si"&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;$UNIT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# mnt-lyra-lab.mount&lt;/span&gt;

&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/mnt-lyra-lab.mount &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;'
[Unit]
Description=Lyra lab ext4 mount (loop file)

[Mount]
What=/var/tmp/lyra-mount-lab/disk.img
Where=/mnt/lyra-lab
Type=ext4
Options=loop,defaults,noatime
TimeoutSec=20s

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&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; mnt-lyra-lab.mount
systemctl status mnt-lyra-lab.mount &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
findmnt /mnt/lyra-lab
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'hello from lyra mount lab'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /mnt/lyra-lab/hello.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop and prove the unit owns the lifecycle:&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 stop mnt-lyra-lab.mount
findmnt /mnt/lyra-lab &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'unmounted'&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start mnt-lyra-lab.mount
&lt;span class="nb"&gt;cat&lt;/span&gt; /mnt/lyra-lab/hello.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 2 — nofail vs required: don’t stall boot on optional media
&lt;/h2&gt;

&lt;p&gt;Simulate “device missing” without rebooting by pointing &lt;code&gt;What=&lt;/code&gt; at a nonexistent UUID:&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/systemd/system/mnt-lyra-optional.mount &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;'
[Unit]
Description=Lyra optional mount (nofail-style)
DefaultDependencies=no
Conflicts=umount.target
Before=local-fs.target umount.target
After=local-fs-pre.target
# Weak pull-in — same idea as fstab nofail:
Wants=local-fs.target
# Intentionally NOT: RequiredBy=local-fs.target / Before hard-require

[Mount]
What=/dev/disk/by-uuid/00000000-0000-0000-0000-000000000000
Where=/mnt/lyra-optional
Type=ext4
Options=defaults
TimeoutSec=5s

[Install]
WantedBy=local-fs.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/lyra-optional
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start mnt-lyra-optional.mount
&lt;span class="nb"&gt;echo exit&lt;/span&gt;:&lt;span class="nv"&gt;$?&lt;/span&gt;
systemctl status mnt-lyra-optional.mount &lt;span class="nt"&gt;--no-pager&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; mnt-lyra-optional.mount &lt;span class="nt"&gt;-n&lt;/span&gt; 20 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a &lt;strong&gt;failed&lt;/strong&gt; mount unit quickly (device timeout / mount failure), not a multi-minute hang. With fstab &lt;code&gt;nofail&lt;/code&gt;, &lt;code&gt;local-fs.target&lt;/code&gt; still reaches &lt;code&gt;active&lt;/code&gt; even when that mount fails; without &lt;code&gt;nofail&lt;/code&gt;, a missing required disk can hold the boot transaction until timeouts cascade.&lt;/p&gt;

&lt;p&gt;Equivalent fstab form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UUID=00000000-0000-0000-0000-000000000000  /mnt/lyra-optional  ext4  defaults,nofail,x-systemd.device-timeout=5s,x-systemd.mount-timeout=5s  0  0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 3 — Transient mounts with systemd-mount
&lt;/h2&gt;

&lt;p&gt;For USB sticks, one-off images, and scripts, skip unit files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List candidate filesystems&lt;/span&gt;
systemd-mount &lt;span class="nt"&gt;--list&lt;/span&gt;

&lt;span class="c"&gt;# Mount lab image to an explicit path (two-arg form: no probe required)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-mount &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ext4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;loop,noatime &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Lyra transient lab'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--collect&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /var/tmp/lyra-mount-lab/disk.img &lt;span class="se"&gt;\&lt;/span&gt;
  /mnt/lyra-transient

findmnt /mnt/lyra-transient
systemctl status &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;systemd-escape &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;--suffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mount /mnt/lyra-transient&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;

&lt;span class="c"&gt;# Automount + idle unmount (stays clean until first access)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-umount /mnt/lyra-transient
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-mount &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--timeout-idle-sec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30s &lt;span class="nt"&gt;--collect&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ext4 &lt;span class="nt"&gt;--options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;loop &lt;span class="se"&gt;\&lt;/span&gt;
  /var/tmp/lyra-mount-lab/disk.img &lt;span class="se"&gt;\&lt;/span&gt;
  /mnt/lyra-auto
&lt;span class="c"&gt;# First access triggers the real mount:&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; /mnt/lyra-auto
&lt;span class="c"&gt;# After idle, systemd detaches the backing FS again.&lt;/span&gt;

&lt;span class="c"&gt;# tmpfs helper (255+)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-mount &lt;span class="nt"&gt;--tmpfs&lt;/span&gt; lyra-tmp /mnt/lyra-tmpfs
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-umount /mnt/lyra-tmpfs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful flags from &lt;code&gt;systemd-mount(1)&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--discover&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Probe label/model; implied for single-arg form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;-A&lt;/code&gt; / &lt;code&gt;--automount=yes&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;On-demand mount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--timeout-idle-sec=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Idle unmount in automount mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`--fsck=yes\&lt;/td&gt;
&lt;td&gt;no`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--bind-device&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stop when backing device vanishes (removable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;-G&lt;/code&gt; / &lt;code&gt;--collect&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Unload failed transient units aggressively&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--owner=USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;uid=&lt;/code&gt;/&lt;code&gt;gid=&lt;/code&gt; where the FS supports it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--no-block&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enqueue without waiting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;udev pattern from the man page for auto USB mounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ACTION=="add", SUBSYSTEMS=="usb", SUBSYSTEM=="block", ENV{ID_FS_USAGE}=="filesystem", \
  RUN{program}+="/usr/bin/systemd-mount --no-block --automount=yes --collect $devnode"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Dependencies services should declare
&lt;/h2&gt;

&lt;p&gt;Prefer path-based deps over hard-coding generated unit names:&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;# in myapp.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;RequiresMountsFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/data/lyra&lt;/span&gt;
&lt;span class="c"&gt;# or softer:
# WantsMountsFor=/data/lyra
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That pulls the right &lt;code&gt;.mount&lt;/code&gt; units (and parents) without fragile string coupling. Hierarchy still adds implicit Requires/After between nested mounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes worth memorizing
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wrong unit name&lt;/strong&gt; — &lt;code&gt;Where=/data/lyra&lt;/code&gt; but file named &lt;code&gt;data.lyra.mount&lt;/code&gt; → unit never matches the path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symlink mount point&lt;/strong&gt; — rejected; generator tries to resolve fstab symlinks, native units do not accept symlink &lt;code&gt;Where=&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing disk without &lt;code&gt;nofail&lt;/code&gt;&lt;/strong&gt; — boot waits on device + mount timeouts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Services start against an empty under-mount&lt;/strong&gt; — declare &lt;code&gt;RequiresMountsFor=&lt;/code&gt; or order &lt;code&gt;After=&lt;/code&gt; the mount unit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;x-systemd.*&lt;/code&gt; in unit &lt;code&gt;Options=&lt;/code&gt;&lt;/strong&gt; — several fstab-only keys are silently ignored outside fstab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Literal &lt;code&gt;%&lt;/code&gt; in options&lt;/strong&gt; — write &lt;code&gt;size=50%%&lt;/code&gt; not &lt;code&gt;size=50%&lt;/code&gt; in unit files (&lt;code&gt;tmp.mount&lt;/code&gt; does this).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nested automounts&lt;/strong&gt; — unsupported; the inner pin defeats the outer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API filesystems&lt;/strong&gt; — some kernel API mounts cannot be disabled via mount units (&lt;code&gt;systemd.io/API_FILE_SYSTEMS&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume manager paths&lt;/strong&gt; — consider &lt;code&gt;x-systemd.device-bound=false&lt;/code&gt; so teardown does not race the VM stack.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Cleanup the labs
&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;systemctl disable &lt;span class="nt"&gt;--now&lt;/span&gt; mnt-lyra-lab.mount mnt-lyra-optional.mount 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/systemd/system/mnt-lyra-lab.mount /etc/systemd/system/mnt-lyra-optional.mount
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-umount /mnt/lyra-transient /mnt/lyra-auto /mnt/lyra-tmpfs 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
sudo &lt;/span&gt;umount /mnt/lyra-lab 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/tmp/lyra-mount-lab /mnt/lyra-lab /mnt/lyra-optional /mnt/lyra-transient /mnt/lyra-auto /mnt/lyra-tmpfs
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reset-failed &lt;span class="s1"&gt;'mnt-lyra-*'&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; a deep dive on NFS automount idle unmounts as the primary workflow (&lt;code&gt;.automount&lt;/code&gt; + remote shares).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; LUKS/TPM unlock, multipath, LVM thin, or Btrfs send/receive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; &lt;code&gt;systemd-gpt-auto-generator&lt;/code&gt; Discoverable Partitions policy (sibling of fstab generation).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; tmpfiles path &lt;em&gt;creation&lt;/em&gt; policy (&lt;code&gt;systemd-tmpfiles&lt;/code&gt;) — mounts attach filesystems; tmpfiles lays out directories inside them.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd.mount(5)&lt;/code&gt; — mount unit settings, fstab &lt;code&gt;x-systemd.*&lt;/code&gt; options, default dependencies
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.automount(5)&lt;/code&gt; — companion on-demand units, &lt;code&gt;TimeoutIdleSec=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-fstab-generator(8)&lt;/code&gt; — fstab → units, &lt;code&gt;systemd.mount-extra=&lt;/code&gt;, &lt;code&gt;fstab.extra&lt;/code&gt;, volatile modes
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-mount(1)&lt;/code&gt; / &lt;code&gt;systemd-umount&lt;/code&gt; — transient mount/automount helpers
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-escape(1)&lt;/code&gt; — path → unit name escaping
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.unit(5)&lt;/code&gt; — &lt;code&gt;RequiresMountsFor=&lt;/code&gt;, specifiers, unit naming
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fstab(5)&lt;/code&gt;, &lt;code&gt;mount(8)&lt;/code&gt;, &lt;code&gt;umount(8)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Stock &lt;code&gt;tmp.mount&lt;/code&gt; on systemd 257 (Debian 13)
&lt;/li&gt;
&lt;li&gt;API file systems notes: &lt;a href="https://systemd.io/API_FILE_SYSTEMS" rel="noopener noreferrer"&gt;https://systemd.io/API_FILE_SYSTEMS&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Treat mounts as first-class units: named, ordered, timed, and observable. Keep fstab for the simple cases, reach for native &lt;code&gt;.mount&lt;/code&gt; files and &lt;code&gt;systemd-mount&lt;/code&gt; when tooling or transient workflows need a sharper edge — and put &lt;code&gt;nofail&lt;/code&gt; + device timeouts on every volume that should not be allowed to hold the boot hostage.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>storage</category>
    </item>
    <item>
      <title>Stop Booting Daemons Just to Listen: Practical systemd.socket Activation on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Thu, 08 Oct 2026 05:02:22 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-booting-daemons-just-to-listen-practical-systemdsocket-activation-on-linux-2bcc</link>
      <guid>https://dev.to/lyraalishaikh/stop-booting-daemons-just-to-listen-practical-systemdsocket-activation-on-linux-2bcc</guid>
      <description>&lt;p&gt;You have a small internal API that gets hit a few times a day. Or a debug endpoint. Or a homelab tool that only matters when someone actually connects. The usual pattern is still:&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;# enable the service at boot so the port is open&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works. It also means the process, its language runtime, and its memory footprint sit warm forever — even at 3 a.m. when nobody is talking to it. Worse, if the daemon crashes hard enough to lose its listening socket, clients get &lt;code&gt;Connection refused&lt;/code&gt; until something restarts it.&lt;/p&gt;

&lt;p&gt;systemd already solved this. A &lt;code&gt;.socket&lt;/code&gt; unit owns the listening file descriptor. The matching &lt;code&gt;.service&lt;/code&gt; starts when traffic arrives (or in parallel at boot, sharing the pre-opened sockets). Same journal, cgroup, and sandbox knobs you already use for services.&lt;/p&gt;

&lt;p&gt;This guide is operational. Behavior and defaults come from &lt;code&gt;systemd.socket(5)&lt;/code&gt;, &lt;code&gt;systemd.service(5)&lt;/code&gt;, &lt;code&gt;sd_listen_fds(3)&lt;/code&gt;, &lt;code&gt;systemd-socket-proxyd(8)&lt;/code&gt;, and stock units on &lt;strong&gt;systemd 257&lt;/strong&gt; (verified on Debian 13).&lt;/p&gt;

&lt;h2&gt;
  
  
  What a socket unit is (and is not)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.socket&lt;/code&gt; unit&lt;/td&gt;
&lt;td&gt;Create and listen on TCP/UDP/UNIX/FIFO/… FDs; start a service on traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matching &lt;code&gt;.service&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Accept and handle work (&lt;code&gt;Accept=no&lt;/code&gt;) or one connection (&lt;code&gt;Accept=yes&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sd_listen_fds(3)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Native protocol: FDs start at 3, counted by &lt;code&gt;$LISTEN_FDS&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StandardInput=socket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;inetd-style: one accepted connection on stdin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Filesystem path activation (inotify), not network connections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.timer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Time-based activation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A socket unit does &lt;strong&gt;not&lt;/strong&gt; replace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;services that must do heavy work before they can usefully accept clients (unless you also start them explicitly),&lt;/li&gt;
&lt;li&gt;full reverse proxies / L7 load balancers,&lt;/li&gt;
&lt;li&gt;apps that insist on binding and owning the port themselves with no FD-passing support (use &lt;code&gt;systemd-socket-proxyd&lt;/code&gt; or fix the app).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It &lt;strong&gt;does&lt;/strong&gt; replace most “always-on listener for rarely used ports” and pairs naturally with on-demand tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mental model
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;You install &lt;code&gt;foo.socket&lt;/code&gt; and &lt;code&gt;foo.service&lt;/code&gt; (same basename by default).&lt;/li&gt;
&lt;li&gt;You &lt;strong&gt;enable and start the socket unit&lt;/strong&gt;, not necessarily the service.&lt;/li&gt;
&lt;li&gt;systemd binds and listens &lt;strong&gt;before&lt;/strong&gt; (or without) the service process.&lt;/li&gt;
&lt;li&gt;On incoming traffic, systemd starts the service and passes listening FDs (or one accepted connection).&lt;/li&gt;
&lt;li&gt;If the service exits and the socket unit stays up, the port keeps listening. The next connection can start the service again.&lt;/li&gt;
&lt;li&gt;There is &lt;strong&gt;no&lt;/strong&gt; implicit &lt;code&gt;WantedBy=&lt;/code&gt; from socket → service. The service can still be started alone; if you need “service requires its socket”, add an explicit &lt;code&gt;Requires=&lt;/code&gt; / &lt;code&gt;After=&lt;/code&gt; on the service (or use &lt;code&gt;Sockets=&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last point is why &lt;code&gt;ssh.socket&lt;/code&gt; + &lt;code&gt;ssh.service&lt;/code&gt; style pairs are common: the socket is the always-on edge; the daemon is optional until needed (or started in parallel sharing the FD).&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--version&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1
&lt;span class="c"&gt;# systemd 257 (socket activation is long-standing; details below note version gates)&lt;/span&gt;

man systemd.socket
man sd_listen_fds
systemctl list-units &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;socket &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a typical host you already have stock examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ssh.socket&lt;/code&gt; — &lt;code&gt;ListenStream=22&lt;/code&gt;, &lt;code&gt;Accept=no&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dbus.socket&lt;/code&gt; — &lt;code&gt;ListenStream=/run/dbus/system_bus_socket&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-hostnamed.socket&lt;/code&gt; — Varlink UNIX socket + &lt;code&gt;FileDescriptorName=varlink&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-journald.socket&lt;/code&gt; — multiple Listen* stanzas, &lt;code&gt;PassCredentials=yes&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&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;ssh.socket
&lt;span class="c"&gt;# [Socket]&lt;/span&gt;
&lt;span class="c"&gt;# ListenStream=22&lt;/span&gt;
&lt;span class="c"&gt;# Accept=no&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Accept=no vs Accept=yes
&lt;/h2&gt;

&lt;p&gt;This is the fork in the road. Get it wrong and either naming or FD semantics will surprise you.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;Accept=no&lt;/code&gt; (default)&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Accept=yes&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What is passed&lt;/td&gt;
&lt;td&gt;The &lt;strong&gt;listening&lt;/strong&gt; socket FD(s)&lt;/td&gt;
&lt;td&gt;One &lt;strong&gt;already-accepted&lt;/strong&gt; connection FD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service unit&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;foo.service&lt;/code&gt; (single instance)&lt;/td&gt;
&lt;td&gt;Template &lt;code&gt;foo@.service&lt;/code&gt; (one instance per connection)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App model&lt;/td&gt;
&lt;td&gt;Real daemon: &lt;code&gt;accept()&lt;/code&gt; loop, or use passed listeners&lt;/td&gt;
&lt;td&gt;inetd-style: handle one client then exit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;First connection pays start cost; later ones reuse process&lt;/td&gt;
&lt;td&gt;Every connection pays start cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolation&lt;/td&gt;
&lt;td&gt;One process sees all clients&lt;/td&gt;
&lt;td&gt;Each connection can be sandboxed separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Datagram / FIFO&lt;/td&gt;
&lt;td&gt;Always single-service style (&lt;code&gt;Accept=&lt;/code&gt; ignored)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Env extras&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$LISTEN_FDS&lt;/code&gt;, &lt;code&gt;$LISTEN_PID&lt;/code&gt;, …&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$REMOTE_ADDR&lt;/code&gt;, &lt;code&gt;$REMOTE_PORT&lt;/code&gt;, &lt;code&gt;$SO_COOKIE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;From &lt;code&gt;systemd.socket(5)&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance-sensitive services prefer &lt;strong&gt;&lt;code&gt;Accept=no&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Sporadic tools prefer &lt;strong&gt;&lt;code&gt;Accept=yes&lt;/code&gt;&lt;/strong&gt; (simpler code, stronger per-connection sandboxing).&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;Accept=yes&lt;/code&gt;, set &lt;code&gt;CollectMode=inactive-or-failed&lt;/code&gt; on the template so failed instances do not pile up in memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How FDs are passed (&lt;code&gt;Accept=no&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Native protocol (&lt;code&gt;sd_listen_fds(3)&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FDs start at &lt;strong&gt;&lt;code&gt;SD_LISTEN_FDS_START&lt;/code&gt; = 3&lt;/strong&gt; (after stdin/stdout/stderr).&lt;/li&gt;
&lt;li&gt;Count is in &lt;strong&gt;&lt;code&gt;$LISTEN_FDS&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$LISTEN_PID&lt;/code&gt;&lt;/strong&gt; must match the receiving process (since 259, &lt;code&gt;$LISTEN_PIDFDID&lt;/code&gt; is also checked when set).&lt;/li&gt;
&lt;li&gt;Optional &lt;strong&gt;&lt;code&gt;$LISTEN_FDNAMES&lt;/code&gt;&lt;/strong&gt; colon-separated names from &lt;code&gt;FileDescriptorName=&lt;/code&gt; or defaults.&lt;/li&gt;
&lt;li&gt;systemd sets &lt;code&gt;FD_CLOEXEC&lt;/code&gt; on the passed FDs.&lt;/li&gt;
&lt;li&gt;Do &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;shutdown(2)&lt;/code&gt; listening sockets you got with &lt;code&gt;Accept=no&lt;/code&gt; — the manager keeps a duplicate and you would break it.&lt;/li&gt;
&lt;li&gt;Do &lt;strong&gt;not&lt;/strong&gt; unlink AF_UNIX paths the manager owns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;inetd-style alternative: &lt;code&gt;StandardInput=socket&lt;/code&gt; in the service. Then &lt;code&gt;$LISTEN_FDS&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; set; the connection (or listener, depending on Accept) is stdin. That path is ideal for &lt;code&gt;Accept=yes&lt;/code&gt; helpers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 1 — Per-connection echo (&lt;code&gt;Accept=yes&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;inetd-mode is the fastest way to prove socket activation without linking &lt;code&gt;libsystemd&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/systemd/system/lyra-echo@.service &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;'
[Unit]
Description=Lyra per-connection echo server
CollectMode=inactive-or-failed

[Service]
Type=oneshot
ExecStart=/usr/bin/bash -c 'printf "lyra-echo ready&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sh"&gt;n"; cat'
StandardInput=socket
StandardOutput=socket
StandardError=journal
# Optional hardening for a toy service:
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/lyra-echo.socket &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;'
[Unit]
Description=Lyra echo socket (Accept=yes demo)

[Socket]
ListenStream=127.0.0.1:9099
Accept=yes
# Defaults: MaxConnections=64
# TriggerLimitIntervalSec=2s TriggerLimitBurst=200  (Accept=yes)

[Install]
WantedBy=sockets.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&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; lyra-echo.socket
systemctl status lyra-echo.socket &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test:&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;printf&lt;/span&gt; &lt;span class="s1"&gt;'hello\n'&lt;/span&gt; | nc &lt;span class="nt"&gt;-q&lt;/span&gt; 1 127.0.0.1 9099
&lt;span class="c"&gt;# lyra-echo ready&lt;/span&gt;
&lt;span class="c"&gt;# hello&lt;/span&gt;

systemctl list-units &lt;span class="s1"&gt;'lyra-echo@*'&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'lyra-echo@*'&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What just happened:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;lyra-echo.socket&lt;/code&gt; bound &lt;code&gt;127.0.0.1:9099&lt;/code&gt; and sat in &lt;code&gt;listening&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nc&lt;/code&gt; connected; systemd &lt;code&gt;accept()&lt;/code&gt;’d and spawned &lt;code&gt;lyra-echo@&amp;lt;conn&amp;gt;.service&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The service saw the client on stdin/stdout, echoed, exited.&lt;/li&gt;
&lt;li&gt;The socket unit kept listening for the next client.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;$REMOTE_ADDR&lt;/code&gt; / &lt;code&gt;$REMOTE_PORT&lt;/code&gt; are set for IPv4/IPv6 accepts (CGI-style; see RFC 3875). Useful for logging:&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;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/bin/bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'echo "peer=$REMOTE_ADDR:$REMOTE_PORT" &amp;gt;&amp;amp;2; cat'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 2 — Long-running listener (&lt;code&gt;Accept=no&lt;/code&gt; + &lt;code&gt;$LISTEN_FDS&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Now the production-shaped path: one service, listening FDs inherited.&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; /usr/local/bin/lyra-listen-demo &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 python3
"""Minimal Accept=no socket-activated server using &lt;/span&gt;&lt;span class="nv"&gt;$LISTEN_FDS&lt;/span&gt;&lt;span class="sh"&gt; only."""
import os, socket, sys

def listen_fds():
    if os.environ.get("LISTEN_PID") != str(os.getpid()):
        return []
    n = int(os.environ.get("LISTEN_FDS", "0"))
    # sd_listen_fds starts at FD 3
    return list(range(3, 3 + n))

fds = listen_fds()
if not fds:
    sys.stderr.write("no LISTEN_FDS; start via lyra-listen.socket&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;")
    sys.exit(1)

names = os.environ.get("LISTEN_FDNAMES", "").split(":")
sys.stderr.write(f"got {len(fds)} fd(s) names={names!r}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;")

listeners = []
for fd in fds:
    s = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
    # fromfd() dup's; close the passed fd number we no longer need
    os.close(fd)
    s.setblocking(True)
    listeners.append(s)

# Single-listener demo
lsock = listeners[0]
sys.stderr.write("listening via systemd-passed FD&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;")
while True:
    conn, addr = lsock.accept()
    with conn:
        conn.sendall(b"lyra-listen: " + repr(addr).encode() + b"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;")
        while True:
            data = conn.recv(4096)
            if not data:
                break
            conn.sendall(data)
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;755 /usr/local/bin/lyra-listen-demo

&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/lyra-listen.service &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;'
[Unit]
Description=Lyra Accept=no LISTEN_FDS demo
Requires=lyra-listen.socket
After=lyra-listen.socket

[Service]
ExecStart=/usr/local/bin/lyra-listen-demo
# Type=simple is fine: the socket already holds the listener
Restart=on-failure

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/lyra-listen.socket &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;'
[Unit]
Description=Lyra LISTEN_FDS demo socket

[Socket]
ListenStream=127.0.0.1:9100
Accept=no
FileDescriptorName=lyra-demo
# FreeBind=yes  # useful when binding a specific IP before the iface is up
# TriggerLimitIntervalSec=2s TriggerLimitBurst=20  (Accept=no defaults)

[Install]
WantedBy=sockets.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&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; lyra-listen.socket
&lt;span class="c"&gt;# Do NOT start the service yet — prove on-demand activation:&lt;/span&gt;
systemctl is-active lyra-listen.service &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;   &lt;span class="c"&gt;# inactive&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'ping\n'&lt;/span&gt; | nc &lt;span class="nt"&gt;-q&lt;/span&gt; 1 127.0.0.1 9100
systemctl is-active lyra-listen.service           &lt;span class="c"&gt;# active&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; lyra-listen.service &lt;span class="nt"&gt;-n&lt;/span&gt; 10 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes that matter in real daemons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefer &lt;code&gt;sd_listen_fds()&lt;/code&gt; / &lt;code&gt;sd_listen_fds_with_names()&lt;/code&gt; from &lt;code&gt;libsystemd&lt;/code&gt; over hand-parsing env when you can; they set &lt;code&gt;FD_CLOEXEC&lt;/code&gt; and validate pid/pidfd.&lt;/li&gt;
&lt;li&gt;With multiple &lt;code&gt;ListenStream=&lt;/code&gt; lines, all FDs are passed, ordered as configured in that unit.&lt;/li&gt;
&lt;li&gt;Multiple socket units can point at one service via &lt;code&gt;Service=&lt;/code&gt;; order &lt;strong&gt;across&lt;/strong&gt; units is undefined — use &lt;code&gt;FileDescriptorName=&lt;/code&gt; and &lt;code&gt;sd_listen_fds_with_names()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lab 3 — AF_UNIX with mode and ownership
&lt;/h2&gt;

&lt;p&gt;UNIX sockets are how most local desktop/system buses do on-demand activation.&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/systemd/system/lyra-unix.socket &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;'
[Unit]
Description=Lyra AF_UNIX demo socket

[Socket]
ListenStream=/run/lyra/demo.sock
SocketMode=0660
DirectoryMode=0755
# SocketUser=lyra SocketGroup=lyra   # if the user/group exist
Accept=yes
RemoveOnStop=yes

[Install]
WantedBy=sockets.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/lyra-unix@.service &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;'
[Unit]
Description=Lyra AF_UNIX connection
CollectMode=inactive-or-failed

[Service]
Type=oneshot
StandardInput=socket
StandardOutput=socket
ExecStart=/usr/bin/bash -c 'printf "unix peer=%s&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sh"&gt;n" "&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REMOTE_ADDR&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;unnamed&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"; cat'
PrivateTmp=yes
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&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; lyra-unix.socket
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'hi-unix\n'&lt;/span&gt; | nc &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; 1 /run/lyra/demo.sock
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /run/lyra/demo.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Abstract-namespace sockets use &lt;code&gt;@name&lt;/code&gt; instead of a filesystem path (&lt;code&gt;ListenStream=@lyra-demo&lt;/code&gt;). No inode on disk; good for private IPC, awkward for ad-hoc &lt;code&gt;nc&lt;/code&gt; debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Listen family cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directive&lt;/th&gt;
&lt;th&gt;Socket type&lt;/th&gt;
&lt;th&gt;Typical address forms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ListenStream=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SOCK_STREAM&lt;/code&gt; (TCP or UNIX stream)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;9090&lt;/code&gt;, &lt;code&gt;127.0.0.1:9090&lt;/code&gt;, &lt;code&gt;[::1]:9090&lt;/code&gt;, &lt;code&gt;/run/app.sock&lt;/code&gt;, &lt;code&gt;@abstract&lt;/code&gt;, &lt;code&gt;vsock:CID:port&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ListenDatagram=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SOCK_DGRAM&lt;/code&gt; (UDP or UNIX dgram)&lt;/td&gt;
&lt;td&gt;same address grammar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ListenSequentialPacket=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SOCK_SEQPACKET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AF_UNIX only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ListenFIFO=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FIFO path&lt;/td&gt;
&lt;td&gt;absolute path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ListenNetlink=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Netlink&lt;/td&gt;
&lt;td&gt;e.g. &lt;code&gt;kobject-uevent&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ListenMessageQueue=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POSIX mq&lt;/td&gt;
&lt;td&gt;name beginning with &lt;code&gt;/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SocketProtocol=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;udplite&lt;/code&gt; / &lt;code&gt;sctp&lt;/code&gt; / &lt;code&gt;mptcp&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pairs with Listen*&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Useful knobs when binding network listeners:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FreeBind=yes&lt;/code&gt;&lt;/strong&gt; — bind a specific IP before the address exists on an interface (&lt;code&gt;IP_FREEBIND&lt;/code&gt;). Recommended whenever you pin a non-wildcard address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BindToDevice=eth0&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;SO_BINDTODEVICE&lt;/code&gt;; adds device unit deps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BindIPv6Only=&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;default&lt;/code&gt; / &lt;code&gt;both&lt;/code&gt; / &lt;code&gt;ipv6-only&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Backlog=&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;listen(2)&lt;/code&gt; backlog; silently capped by &lt;code&gt;net.core.somaxconn&lt;/code&gt; (often 4096).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ReusePort=&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;NoDelay=&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;KeepAlive=&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;DeferAcceptSec=&lt;/code&gt;&lt;/strong&gt; — standard TCP tunables exposed as unit options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PassCredentials=&lt;/code&gt;&lt;/strong&gt; / &lt;strong&gt;&lt;code&gt;PassSecurity=&lt;/code&gt;&lt;/strong&gt; — AF_UNIX ancillary creds (journald uses these).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rate limits: trigger vs poll
&lt;/h2&gt;

&lt;p&gt;Same family of knobs as &lt;code&gt;.path&lt;/code&gt; units, with socket-specific defaults (&lt;code&gt;systemd.socket(5)&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;On limit hit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TriggerLimitIntervalSec=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TriggerLimitBurst=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;200&lt;/code&gt; if &lt;code&gt;Accept=yes&lt;/code&gt;, else &lt;code&gt;20&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Socket unit &lt;strong&gt;fails&lt;/strong&gt;; not connectible until restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PollLimitIntervalSec=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2s&lt;/code&gt; (255+)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PollLimitBurst=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;150&lt;/code&gt; if &lt;code&gt;Accept=yes&lt;/code&gt;, else &lt;code&gt;15&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Temporary&lt;/strong&gt; slowdown of polling (DoS soft brake)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;PollLimit*&lt;/code&gt; is the preferred flood brake. &lt;code&gt;TriggerLimit*&lt;/code&gt; is the hard tripwire. Service &lt;code&gt;StartLimitIntervalSec=&lt;/code&gt; / &lt;code&gt;StartLimitBurst=&lt;/code&gt; still apply independently.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MaxConnections=&lt;/code&gt; (default &lt;strong&gt;64&lt;/strong&gt;) and &lt;code&gt;MaxConnectionsPerSource=&lt;/code&gt; (default &lt;strong&gt;0&lt;/strong&gt; = off) only apply to &lt;code&gt;Accept=yes&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel start and crash resilience
&lt;/h2&gt;

&lt;p&gt;Lennart Poettering’s classic socket-activation write-up still holds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Listening sockets are created early; dependent daemons can start &lt;strong&gt;in parallel&lt;/strong&gt; and talk immediately (writes queue in the socket buffer until the server catches up).&lt;/li&gt;
&lt;li&gt;If the service crashes but the &lt;strong&gt;socket unit stays up&lt;/strong&gt;, clients do not get “connection refused” during the restart window — the backlog holds.&lt;/li&gt;
&lt;li&gt;Upgrades can restart the service process while keeping the same listening FDs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stock &lt;code&gt;systemd-journald.socket&lt;/code&gt; is the textbook multi-listener unit: datagram + stream, credential passing, dedicated &lt;code&gt;Service=&lt;/code&gt; name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bridging apps that cannot speak LISTEN_FDS
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-socket-proxyd(8)&lt;/code&gt; accepts the systemd-passed listener (&lt;code&gt;Accept=no&lt;/code&gt;), then bidirectional-proxies each client to a backend TCP or UNIX socket. That is how you give socket activation to a daemon that only knows &lt;code&gt;listen&lt;/code&gt; on its own path.&lt;/p&gt;

&lt;p&gt;Sketch from the man page (nginx behind a host port):&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;# proxy-to-app.socket
&lt;/span&gt;&lt;span class="nn"&gt;[Socket]&lt;/span&gt;
&lt;span class="py"&gt;ListenStream&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;80&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;sockets.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# proxy-to-app.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;app.service&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;app.service&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;proxy-to-app.socket&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;proxy-to-app.socket&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;notify&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-socket-proxyd /run/app.sock&lt;/span&gt;
&lt;span class="py"&gt;PrivateTmp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--connections-max=&lt;/code&gt; (default 256)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--exit-idle-time=&lt;/code&gt; so the proxy (and a &lt;code&gt;StopWhenUnneeded=&lt;/code&gt; backend) can idle down&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--proxy-protocol=v1&lt;/code&gt; (261+) when the backend should see the real client IP&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JoinsNamespaceOf=&lt;/code&gt; when the backend uses &lt;code&gt;PrivateNetwork=&lt;/code&gt; / &lt;code&gt;PrivateTmp=&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Caveat: side channels (&lt;code&gt;SCM_RIGHTS&lt;/code&gt;, &lt;code&gt;SO_PEERCRED&lt;/code&gt;, …) are &lt;strong&gt;not&lt;/strong&gt; forwarded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service wiring checklist
&lt;/h2&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;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp.socket&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;myapp.socket&lt;/span&gt;
&lt;span class="c"&gt;# optional: Sockets=myapp.socket   # pulls socket via Wants+After from the service side
&lt;/span&gt;
&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="c"&gt;# Accept=no native daemon:
&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/local/bin/myapp&lt;/span&gt;
&lt;span class="c"&gt;# OR inetd-style:
# StandardInput=socket
# StandardOutput=socket
&lt;/span&gt;
&lt;span class="c"&gt;# Accept=yes template extras:
# CollectMode=inactive-or-failed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install section belongs on the &lt;strong&gt;socket&lt;/strong&gt; for on-demand:&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;[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;sockets.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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; myapp.socket
&lt;span class="c"&gt;# multi-user pull-in of sockets.target starts your listeners at boot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verification and debugging
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status myapp.socket
systemctl show myapp.socket &lt;span class="nt"&gt;-p&lt;/span&gt; Listen &lt;span class="nt"&gt;-p&lt;/span&gt; Accept &lt;span class="nt"&gt;-p&lt;/span&gt; NAccepted &lt;span class="nt"&gt;-p&lt;/span&gt; NConnections
ss &lt;span class="nt"&gt;-ltnp&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;9100          &lt;span class="c"&gt;# or ss -xlp for UNIX&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.socket &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;

&lt;span class="c"&gt;# See what stock units look like when something already works:&lt;/span&gt;
systemctl &lt;span class="nb"&gt;cat &lt;/span&gt;dbus.socket systemd-hostnamed.socket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common failures:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service starts but cannot bind port&lt;/td&gt;
&lt;td&gt;App still binds itself &lt;strong&gt;and&lt;/strong&gt; ignores &lt;code&gt;LISTEN_FDS&lt;/code&gt; — double bind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;no LISTEN_FDS&lt;/code&gt; in app&lt;/td&gt;
&lt;td&gt;Started without socket; or &lt;code&gt;$LISTEN_PID&lt;/code&gt; mismatch after an unexpected fork&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Template name errors&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Accept=yes&lt;/code&gt; but service is not &lt;code&gt;name@.service&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Socket dead after flood&lt;/td&gt;
&lt;td&gt;Hit &lt;code&gt;TriggerLimit*&lt;/code&gt;; check &lt;code&gt;systemctl status&lt;/code&gt; and reset with &lt;code&gt;systemctl restart myapp.socket&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stale AF_UNIX node&lt;/td&gt;
&lt;td&gt;Left behind after crash with &lt;code&gt;RemoveOnStop=no&lt;/code&gt; (default); stop unit or remove carefully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works on IPv6 only / unexpected dual-stack&lt;/td&gt;
&lt;td&gt;Review &lt;code&gt;BindIPv6Only=&lt;/code&gt; and whether you passed a bare port vs &lt;code&gt;127.0.0.1:&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Cleanup the labs
&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;systemctl disable &lt;span class="nt"&gt;--now&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  lyra-echo.socket lyra-listen.socket lyra-unix.socket 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
sudo &lt;/span&gt;systemctl stop lyra-listen.service 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /etc/systemd/system/lyra-echo.socket &lt;span class="se"&gt;\&lt;/span&gt;
  /etc/systemd/system/lyra-echo@.service &lt;span class="se"&gt;\&lt;/span&gt;
  /etc/systemd/system/lyra-listen.socket &lt;span class="se"&gt;\&lt;/span&gt;
  /etc/systemd/system/lyra-listen.service &lt;span class="se"&gt;\&lt;/span&gt;
  /etc/systemd/system/lyra-unix.socket &lt;span class="se"&gt;\&lt;/span&gt;
  /etc/systemd/system/lyra-unix@.service &lt;span class="se"&gt;\&lt;/span&gt;
  /usr/local/bin/lyra-listen-demo
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Boundaries (what this post is not)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; &lt;code&gt;.path&lt;/code&gt; drop-directory activation — that is filesystem inotify, covered separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; &lt;code&gt;.timer&lt;/code&gt; schedules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; a deep dive on &lt;code&gt;busctl&lt;/code&gt; / Varlink method calls (those are APIs &lt;em&gt;over&lt;/em&gt; sockets).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; full SSH hardening or nginx design — only the activation edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; kqueue/launchd on macOS; same idea, different unit language.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to use which activation unit
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;Unit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First TCP/UNIX connection starts app&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.socket&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File appears / directory not empty&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.path&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calendar or monotonic schedule&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.timer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block device / pluggable hardware&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.device&lt;/code&gt; + bindings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual or dependency boot start only&lt;/td&gt;
&lt;td&gt;plain &lt;code&gt;.service&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enable the &lt;code&gt;.socket&lt;/code&gt;&lt;/strong&gt;, keep the service on-demand (or share FDs for parallel boot).&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;&lt;code&gt;Accept=no&lt;/code&gt;&lt;/strong&gt; for real daemons and &lt;strong&gt;&lt;code&gt;Accept=yes&lt;/code&gt;&lt;/strong&gt; for inetd-style one-shot connections.&lt;/li&gt;
&lt;li&gt;Teach apps &lt;strong&gt;&lt;code&gt;sd_listen_fds&lt;/code&gt; / &lt;code&gt;$LISTEN_FDS&lt;/code&gt;&lt;/strong&gt;, or put &lt;strong&gt;&lt;code&gt;StandardInput=socket&lt;/code&gt;&lt;/strong&gt; in front of simple helpers.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;&lt;code&gt;FreeBind=&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;FileDescriptorName=&lt;/code&gt;&lt;/strong&gt;, and &lt;strong&gt;&lt;code&gt;PollLimit*&lt;/code&gt;&lt;/strong&gt; deliberately on network edges.&lt;/li&gt;
&lt;li&gt;For stubborn legacy listeners, &lt;strong&gt;&lt;code&gt;systemd-socket-proxyd&lt;/code&gt;&lt;/strong&gt; is the supported bridge — not a home-grown &lt;code&gt;socat&lt;/code&gt; unit without notify/cgroup integration.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once the listening FD lives in systemd, “is the port up?” stops meaning “is the whole app up?” — and that is a much calmer way to run rarely used services on a Linux host.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd.socket(5)&lt;/code&gt; — socket unit options, Accept=, Listen*, limits (&lt;code&gt;man systemd.socket&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.service(5)&lt;/code&gt; — service templates, &lt;code&gt;Sockets=&lt;/code&gt;, activation ordering&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sd_listen_fds(3)&lt;/code&gt;, &lt;code&gt;sd_listen_fds_with_names(3)&lt;/code&gt; — FD passing protocol (&lt;code&gt;$LISTEN_FDS&lt;/code&gt;, FD 3+)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-socket-proxyd(8)&lt;/code&gt; — proxy helper for non-native socket activation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.unit(5)&lt;/code&gt;, &lt;code&gt;systemd.exec(5)&lt;/code&gt; — dependencies, sandboxing, &lt;code&gt;StandardInput=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Stock units: &lt;code&gt;ssh.socket&lt;/code&gt;, &lt;code&gt;dbus.socket&lt;/code&gt;, &lt;code&gt;systemd-hostnamed.socket&lt;/code&gt;, &lt;code&gt;systemd-journald.socket&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Lennart Poettering, &lt;em&gt;Socket Activation&lt;/em&gt; (systemd for Developers): &lt;a href="https://0pointer.net/blog/projects/socket-activation.html" rel="noopener noreferrer"&gt;https://0pointer.net/blog/projects/socket-activation.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debian manpages mirror: &lt;a href="https://manpages.debian.org/testing/systemd/systemd.socket.5.en.html" rel="noopener noreferrer"&gt;https://manpages.debian.org/testing/systemd/systemd.socket.5.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Arch man pages: &lt;a href="https://man.archlinux.org/man/systemd.socket.5" rel="noopener noreferrer"&gt;https://man.archlinux.org/man/systemd.socket.5&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Losing Work Mid-Job: Practical systemd-inhibit on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Wed, 07 Oct 2026 05:02:50 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-losing-work-mid-job-practical-systemd-inhibit-on-linux-32c2</link>
      <guid>https://dev.to/lyraalishaikh/stop-losing-work-mid-job-practical-systemd-inhibit-on-linux-32c2</guid>
      <description>&lt;p&gt;You start a multi-hour &lt;code&gt;rsync&lt;/code&gt;, walk away, and the laptop suspends halfway through. Or someone hits reboot while &lt;code&gt;apt full-upgrade&lt;/code&gt; is unpacking. Or a headless box with &lt;code&gt;IdleAction=suspend&lt;/code&gt; parks itself mid-backup because every session looked idle.&lt;/p&gt;

&lt;p&gt;Those are not application bugs. They are missing &lt;strong&gt;inhibitor locks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd-logind&lt;/code&gt; owns a small, deliberate API for this: applications (and operators) can temporarily block or delay sleep, shutdown, idle action, and even the low-level handling of power/lid keys. The command-line front end is &lt;code&gt;systemd-inhibit(1)&lt;/code&gt;. The protocol is documented in the upstream &lt;a href="https://systemd.io/INHIBITOR_LOCKS/" rel="noopener noreferrer"&gt;Inhibitor Locks&lt;/a&gt; page and in &lt;code&gt;org.freedesktop.login1(5)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This guide is operator-first. You will list active locks, wrap real jobs so they cannot be interrupted by idle/sleep/shutdown, put inhibitors on oneshot services, tune &lt;code&gt;logind.conf&lt;/code&gt; delay budgets, and know exactly when &lt;em&gt;not&lt;/em&gt; to take a blocking lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is (and is not)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;systemd-inhibit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CLI: run a command under an inhibitor lock, or list locks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;logind Inhibit()&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;D-Bus API that issues the lock as a file descriptor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IdleAction= / Handle*=&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What logind does when idle or when keys/lid fire — inhibitors gate these&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GNOME session inhibitors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Desktop-oriented (logout/screensaver); often forwarded into logind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;systemd.watchdog / Type=notify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hang detection and readiness — not sleep/shutdown gating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;loginctl linger&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keep &lt;code&gt;user@.service&lt;/code&gt; alive after logout — unrelated to sleep locks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Inhibitor locks answer one question: &lt;strong&gt;“Should this machine be allowed to sleep, shut down, go idle, or honor the lid/power key right now?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They do &lt;strong&gt;not&lt;/strong&gt; replace backups, fencing, or package-manager locks. They buy time and prevent &lt;em&gt;logind-mediated&lt;/em&gt; interruptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mental model
&lt;/h2&gt;

&lt;p&gt;Taking a lock is one D-Bus call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inhibit(what, who, why, mode) → file descriptor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;what&lt;/strong&gt; — colon-separated list of lock types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;who&lt;/strong&gt; — short human label (“Backup job”, “Package Manager”)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;why&lt;/strong&gt; — short human reason (“Nightly rsync in progress”)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mode&lt;/strong&gt; — &lt;code&gt;block&lt;/code&gt;, &lt;code&gt;delay&lt;/code&gt;, or &lt;code&gt;block-weak&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;return value&lt;/strong&gt; — an open FD; &lt;strong&gt;close the FD (or die) and the lock is gone&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the whole design. No stale lock database. No “forgot to unlock” forever. The kernel closes the FD when the process exits, so crashed jobs drop their inhibitors automatically.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd-inhibit&lt;/code&gt; is a thin wrapper: it takes the lock, execs your command, and holds the FD until the command finishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lock types (&lt;code&gt;--what=&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;systemd-inhibit(1)&lt;/code&gt; and the inhibitor-locks doc:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it inhibits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shutdown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;High-level power-off / reboot / halt / kexec / soft-reboot requested via logind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sleep&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Suspend / hibernate / hybrid-sleep / suspend-then-hibernate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;idle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Automatic idle handling (&lt;code&gt;IdleAction=&lt;/code&gt; path)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;handle-power-key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;logind’s own handling of the power key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;handle-reboot-key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;logind’s own handling of the reboot key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;handle-suspend-key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;logind’s own handling of the suspend key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;handle-hibernate-key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;logind’s own handling of the hibernate key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;handle-lid-switch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;logind’s own handling of the lid switch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Default when you omit &lt;code&gt;--what=&lt;/code&gt; is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idle:sleep:shutdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the right default for “do not interrupt this job.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important split:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;shutdown&lt;/code&gt; / &lt;code&gt;sleep&lt;/code&gt; / &lt;code&gt;idle&lt;/code&gt; are &lt;strong&gt;high-level&lt;/strong&gt; locks. They affect user-facing requests and idle policy.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;handle-*-key&lt;/code&gt; / &lt;code&gt;handle-lid-switch&lt;/code&gt; are &lt;strong&gt;low-level&lt;/strong&gt; locks. They only stop &lt;em&gt;logind’s built-in key/lid handlers&lt;/em&gt; so a desktop environment can take over. They do &lt;strong&gt;not&lt;/strong&gt; stop &lt;code&gt;systemctl suspend&lt;/code&gt; typed in a root shell.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also note the lid quirk in &lt;code&gt;logind.conf(5)&lt;/code&gt;: &lt;code&gt;LidSwitchIgnoreInhibited=&lt;/code&gt; defaults to &lt;strong&gt;yes&lt;/strong&gt;. So a high-level &lt;code&gt;sleep&lt;/code&gt; block may &lt;strong&gt;not&lt;/strong&gt; stop lid-triggered suspend unless you change that setting or take &lt;code&gt;handle-lid-switch&lt;/code&gt; (DE pattern). Laptops care about this; racks mostly do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modes (&lt;code&gt;--mode=&lt;/code&gt;)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;block&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;Operation fails until the lock is released. Privileged users may still override.&lt;/td&gt;
&lt;td&gt;Package upgrades, burns, backups, migrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delay&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Operation waits up to &lt;code&gt;InhibitDelayMaxSec=&lt;/code&gt; (default &lt;strong&gt;5s&lt;/strong&gt;), then proceeds anyway. Only valid for &lt;code&gt;sleep&lt;/code&gt; and &lt;code&gt;shutdown&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Save-state hooks, screen lock before suspend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;block-weak&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Like &lt;code&gt;block&lt;/code&gt;, but silently bypassed for root and for the lock owner’s own requests&lt;/td&gt;
&lt;td&gt;“Be nice, don’t hard-block admins”&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;delay&lt;/code&gt; is deliberately weak. It exists so apps can flush state on &lt;code&gt;PrepareForSleep(true)&lt;/code&gt; without being allowed to brick suspend forever. If you need a multi-hour backup to finish, you want &lt;strong&gt;&lt;code&gt;block&lt;/code&gt;&lt;/strong&gt;, not &lt;code&gt;delay&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 0 — See what is already holding locks
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-inhibit &lt;span class="nt"&gt;--list&lt;/span&gt;
&lt;span class="c"&gt;# JSON (newer systemd builds):&lt;/span&gt;
systemd-inhibit &lt;span class="nt"&gt;--list&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pretty 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical columns: what, who, why, mode, UID, PID.&lt;/p&gt;

&lt;p&gt;You can also ask logind over D-Bus (same data the CLI uses):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;busctl call org.freedesktop.login1 /org/freedesktop/login1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.login1.Manager ListInhibitors

busctl get-property org.freedesktop.login1 /org/freedesktop/login1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.login1.Manager BlockInhibited DelayInhibited InhibitDelayMaxUSec
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BlockInhibited&lt;/code&gt; / &lt;code&gt;DelayInhibited&lt;/code&gt; are colon-joined unions of active lock types — handy for monitoring probes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 1 — Protect a long job from idle + sleep + shutdown
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Hold idle:sleep:shutdown (the default) while the job runs&lt;/span&gt;
systemd-inhibit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--what&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;idle:sleep:shutdown &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--who&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"homelab-backup"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--why&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Nightly rsync of /srv"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;block &lt;span class="se"&gt;\&lt;/span&gt;
  rsync &lt;span class="nt"&gt;-aHAX&lt;/span&gt; &lt;span class="nt"&gt;--info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress2 /srv/ /mnt/backup/srv/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In another terminal while it runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-inhibit &lt;span class="nt"&gt;--list&lt;/span&gt;
&lt;span class="c"&gt;# Expect a row for who=homelab-backup, mode=block&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unprivileged &lt;code&gt;systemctl suspend&lt;/code&gt; / desktop “Suspend” should now be denied or deferred by policy while the lock is held. Root can still force actions; inhibitors are a cooperation protocol with privilege escape hatches, not a security boundary against &lt;code&gt;root&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Minimal smoke test without a real backup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-inhibit &lt;span class="nt"&gt;--who&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo &lt;span class="nt"&gt;--why&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"hold for 30s"&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;30 &amp;amp;
&lt;span class="nb"&gt;sleep &lt;/span&gt;1
systemd-inhibit &lt;span class="nt"&gt;--list&lt;/span&gt;
&lt;span class="nb"&gt;wait
&lt;/span&gt;systemd-inhibit &lt;span class="nt"&gt;--list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 2 — Narrow the lock (only what you need)
&lt;/h2&gt;

&lt;p&gt;Taking the widest lock forever is how laptops never sleep and batteries die. Scope 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="c"&gt;# Database dump: block shutdown + idle, but allow explicit admin suspend if you want&lt;/span&gt;
systemd-inhibit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--what&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;shutdown:idle &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--who&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pg-dump"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--why&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Logical dump of appdb"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;block &lt;span class="se"&gt;\&lt;/span&gt;
  pg_dump &lt;span class="nt"&gt;-Fc&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/backups/appdb.dump appdb

&lt;span class="c"&gt;# Optical / long write where suspend is the real enemy&lt;/span&gt;
systemd-inhibit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--what&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;sleep&lt;/span&gt;:idle &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--who&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"disk-image"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--why&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Writing appliance image"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;block &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;image.raw &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/sdX &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;16M &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress &lt;span class="nv"&gt;oflag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;direct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Man-page canonical example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-inhibit wodim foobar.iso
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 3 — Put an inhibitor on a systemd oneshot
&lt;/h2&gt;

&lt;p&gt;Wrapping &lt;code&gt;ExecStart=&lt;/code&gt; is the durable pattern for timers and overnight jobs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/etc/systemd/system/lyra-backup.service&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;Protected rsync backup (inhibits idle/sleep/shutdown)&lt;/span&gt;
&lt;span class="py"&gt;Documentation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;man:systemd-inhibit(1)&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="c"&gt;# Nice labels show up in systemd-inhibit --list and desktop UIs
&lt;/span&gt;&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/systemd-inhibit &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;--what=idle:sleep:shutdown &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;--mode=block &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;--who=lyra-backup.service &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;--why="Nightly /srv rsync" &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;/usr/local/sbin/lyra-backup.sh&lt;/span&gt;
&lt;span class="c"&gt;# Optional hardening — adjust paths to match your script
&lt;/span&gt;&lt;span class="py"&gt;ProtectSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;
&lt;span class="py"&gt;ProtectHome&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;read-only&lt;/span&gt;
&lt;span class="py"&gt;ReadWritePaths&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/mnt/backup /var/log&lt;/span&gt;
&lt;span class="py"&gt;PrivateTmp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;Nice&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="py"&gt;IOSchedulingClass&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;best-effort&lt;/span&gt;
&lt;span class="py"&gt;IOSchedulingPriority&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;7&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;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair with a timer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/lyra-backup.timer
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Nightly inhibited backup&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;*-*-* 02:30:00&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;yes&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;10m&lt;/span&gt;

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

&lt;/div&gt;





&lt;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 &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; lyra-backup.timer
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start lyra-backup.service   &lt;span class="c"&gt;# manual test&lt;/span&gt;
&lt;span class="c"&gt;# While running:&lt;/span&gt;
systemd-inhibit &lt;span class="nt"&gt;--list&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; lyra-backup.service &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For package upgrades on a workstation you actually use:&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-inhibit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--what&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;shutdown:sleep:idle &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--who&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"apt"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--why&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Unattended upgrade in progress"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  apt &lt;span class="nt"&gt;-y&lt;/span&gt; full-upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 4 — Delay locks and the 5-second budget
&lt;/h2&gt;

&lt;p&gt;Delay mode is for &lt;strong&gt;prepare-then-release&lt;/strong&gt;, not multi-hour work.&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;# Inspect the budget (property is in microseconds)&lt;/span&gt;
busctl get-property org.freedesktop.login1 /org/freedesktop/login1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.login1.Manager InhibitDelayMaxUSec
&lt;span class="c"&gt;# Default: 5000000  →  5 seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Raise it only if you have a real flush that needs more wall time (and still keep it bounded):&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;# /etc/systemd/logind.conf.d/10-inhibit-delay.conf&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;Login]
&lt;span class="nv"&gt;InhibitDelayMaxSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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 restart systemd-logind.service
&lt;span class="c"&gt;# Caution: restarting logind can disrupt sessions; do this in a maintenance window&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Application shape (from the upstream inhibitor-locks doc):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On start / document open → take &lt;code&gt;Inhibit("sleep", …, "delay")&lt;/code&gt;, keep the FD&lt;/li&gt;
&lt;li&gt;On &lt;code&gt;PrepareForSleep(true)&lt;/code&gt; → flush state &lt;strong&gt;fast&lt;/strong&gt;, then &lt;code&gt;close(fd)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;On &lt;code&gt;PrepareForSleep(false)&lt;/code&gt; (resume) → take the delay lock again&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Watching &lt;code&gt;PrepareForSleep(true)&lt;/code&gt; &lt;strong&gt;without&lt;/strong&gt; a delay lock is racy: suspend may complete before your handler finishes. The docs are explicit about this.&lt;/p&gt;

&lt;p&gt;CLI-only operators rarely need delay mode. Prefer &lt;code&gt;block&lt;/code&gt; around the job, or teach the app the D-Bus protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 5 — Key and lid handling (desktop / laptop)
&lt;/h2&gt;

&lt;p&gt;Desktop environments take &lt;code&gt;handle-power-key&lt;/code&gt;, &lt;code&gt;handle-suspend-key&lt;/code&gt;, &lt;code&gt;handle-hibernate-key&lt;/code&gt;, and &lt;code&gt;handle-lid-switch&lt;/code&gt; in &lt;strong&gt;block&lt;/strong&gt; mode so they can show UI or suppress lid-suspend on a docked laptop. Delay mode is &lt;strong&gt;not&lt;/strong&gt; supported for these types.&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;# Example: temporarily prevent logind from handling the lid itself&lt;/span&gt;
&lt;span class="c"&gt;# (a DE would normally own this for the whole session)&lt;/span&gt;
systemd-inhibit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--what&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;handle-lid-switch &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;block &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--who&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"dock-session"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--why&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"External displays attached"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;infinity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Related &lt;code&gt;logind.conf(5)&lt;/code&gt; knobs (defaults matter):&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;[Login]&lt;/span&gt;
&lt;span class="c"&gt;# What logind does with hardware events when no low-level handle-* lock is held
&lt;/span&gt;&lt;span class="py"&gt;HandlePowerKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;poweroff&lt;/span&gt;
&lt;span class="py"&gt;HandleSuspendKey&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;HandleHibernateKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;hibernate&lt;/span&gt;
&lt;span class="py"&gt;HandleLidSwitch&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;HandleLidSwitchDocked&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;
&lt;span class="c"&gt;# HandleLidSwitchExternalPower=   # ignored until set explicitly
&lt;/span&gt;
&lt;span class="c"&gt;# Do high-level inhibitors (sleep/shutdown/idle) affect key/lid actions?
&lt;/span&gt;&lt;span class="py"&gt;PowerKeyIgnoreInhibited&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;span class="py"&gt;SuspendKeyIgnoreInhibited&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;span class="py"&gt;HibernateKeyIgnoreInhibited&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;span class="py"&gt;RebootKeyIgnoreInhibited&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;span class="py"&gt;LidSwitchIgnoreInhibited&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes          # default: lid ignores high-level sleep blocks&lt;/span&gt;

&lt;span class="py"&gt;IdleAction&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ignore                     # or suspend / lock / …&lt;/span&gt;
&lt;span class="py"&gt;IdleActionSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30min&lt;/span&gt;
&lt;span class="py"&gt;InhibitDelayMaxSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your “backup blocked sleep” test still loses to closing the lid, check &lt;code&gt;LidSwitchIgnoreInhibited=&lt;/code&gt; before assuming inhibitors are broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polkit and who may inhibit
&lt;/h2&gt;

&lt;p&gt;Taking locks is privileged and gated by non-interactive polkit actions, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;org.freedesktop.login1.inhibit-block-shutdown&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;org.freedesktop.login1.inhibit-delay-shutdown&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;org.freedesktop.login1.inhibit-block-sleep&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;org.freedesktop.login1.inhibit-delay-sleep&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;org.freedesktop.login1.inhibit-block-idle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;org.freedesktop.login1.inhibit-handle-power-key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;org.freedesktop.login1.inhibit-handle-suspend-key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;org.freedesktop.login1.inhibit-handle-hibernate-key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;org.freedesktop.login1.inhibit-handle-lid-switch&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Upstream guidance: &lt;strong&gt;delay locks are easier to obtain than block locks&lt;/strong&gt; because their blast radius is smaller. If &lt;code&gt;Inhibit()&lt;/code&gt; is denied, treat it as “continue without protection,” not a hard error — especially for optional desktop features.&lt;/p&gt;

&lt;p&gt;System services running as root generally can take locks. User jobs may need linger + polkit rules depending on distro policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring snippet
&lt;/h2&gt;

&lt;p&gt;A tiny probe for dashboards or &lt;code&gt;ExecStartPost=&lt;/code&gt; health checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="c"&gt;# Exit 0 if a named inhibitor is present&lt;/span&gt;
&lt;span class="nv"&gt;who_re&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;lyra&lt;/span&gt;&lt;span class="p"&gt;-backup&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
systemd-inhibit &lt;span class="nt"&gt;--list&lt;/span&gt; &lt;span class="nt"&gt;--no-legend&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;re&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$who_re&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'tolower($0) ~ tolower(re) { found=1 } END { exit found?0:1 }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or watch the aggregate properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;busctl get-property org.freedesktop.login1 /org/freedesktop/login1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.login1.Manager BlockInhibited
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Failure modes and footguns
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;delay&lt;/code&gt; on a long job&lt;/strong&gt; — Suspend still happens after a few seconds. Use &lt;code&gt;block&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lid switch on laptops&lt;/strong&gt; — Default &lt;code&gt;LidSwitchIgnoreInhibited=yes&lt;/code&gt; bypasses high-level &lt;code&gt;sleep&lt;/code&gt; blocks for lid events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root override&lt;/strong&gt; — Inhibitors stop &lt;em&gt;unprivileged&lt;/em&gt; and policy-bound paths; they are not a mandatory access control against root.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idle inhibitors on mobiles&lt;/strong&gt; — Holding &lt;code&gt;idle&lt;/code&gt; forever destroys battery life. Scope duration to the job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restarting logind&lt;/strong&gt; — Needed after some conf changes; can disturb sessions. Prefer drop-ins and planned restarts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong layer&lt;/strong&gt; — &lt;code&gt;handle-suspend-key&lt;/code&gt; does not block &lt;code&gt;systemctl suspend&lt;/code&gt;. High-level &lt;code&gt;sleep&lt;/code&gt; does (subject to policy).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming DEs are enough&lt;/strong&gt; — GNOME inhibitors cover logout/screensaver cases and often forward into logind, but system services and other users’ locks only show up fully via logind/&lt;code&gt;systemd-inhibit --list&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No FD inheritance mistakes&lt;/strong&gt; — The lock is the FD. Backgrounding a job incorrectly so &lt;code&gt;systemd-inhibit&lt;/code&gt; exits early drops protection. Prefer &lt;code&gt;systemd-inhibit cmd…&lt;/code&gt; or a oneshot unit, not “inhibit &amp;amp;; cmd”.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When to use what
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One-off long shell job&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemd-inhibit … command&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nightly backup / upgrade oneshot&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ExecStart=systemd-inhibit …&lt;/code&gt; in a &lt;code&gt;.service&lt;/code&gt; + &lt;code&gt;.timer&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App must flush before suspend&lt;/td&gt;
&lt;td&gt;D-Bus &lt;code&gt;Inhibit(..., delay)&lt;/code&gt; + &lt;code&gt;PrepareForSleep&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DE owns power keys / lid UX&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;handle-*-key&lt;/code&gt; / &lt;code&gt;handle-lid-switch&lt;/code&gt; block locks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep user services after SSH logout&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;loginctl enable-linger&lt;/code&gt; (different problem)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detect frozen services&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;WatchdogSec=&lt;/code&gt; + &lt;code&gt;sd_notify&lt;/code&gt; (different problem)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crash evidence&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemd-coredump&lt;/code&gt; / &lt;code&gt;pstore&lt;/code&gt; / &lt;code&gt;kdump&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Quick reference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List&lt;/span&gt;
systemd-inhibit &lt;span class="nt"&gt;--list&lt;/span&gt;

&lt;span class="c"&gt;# Default protect (idle + sleep + shutdown), block mode&lt;/span&gt;
systemd-inhibit &lt;span class="nt"&gt;--who&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;JOB &lt;span class="nt"&gt;--why&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;REASON COMMAND

&lt;span class="c"&gt;# Explicit&lt;/span&gt;
systemd-inhibit &lt;span class="nt"&gt;--what&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;idle:sleep:shutdown &lt;span class="nt"&gt;--mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;block &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--who&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;JOB &lt;span class="nt"&gt;--why&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;REASON COMMAND

&lt;span class="c"&gt;# Delay only (short prepare window)&lt;/span&gt;
systemd-inhibit &lt;span class="nt"&gt;--what&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;delay &lt;span class="nt"&gt;--who&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;APP &lt;span class="nt"&gt;--why&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"flush"&lt;/span&gt; COMMAND

&lt;span class="c"&gt;# logind delay budget&lt;/span&gt;
&lt;span class="c"&gt;# InhibitDelayMaxSec= in logind.conf(.d)&lt;/span&gt;

&lt;span class="c"&gt;# D-Bus&lt;/span&gt;
&lt;span class="c"&gt;# org.freedesktop.login1.Manager.Inhibit / ListInhibitors&lt;/span&gt;
&lt;span class="c"&gt;# Signals: PrepareForSleep(b), PrepareForShutdown(b)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-inhibit(1)&lt;/code&gt; — CLI options, default &lt;code&gt;what=&lt;/code&gt;, modes, example&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://systemd.io/INHIBITOR_LOCKS/" rel="noopener noreferrer"&gt;Inhibitor Locks&lt;/a&gt; — lock types, block/delay schemes, &lt;code&gt;PrepareForSleep&lt;/code&gt; protocol, polkit action names&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;logind.conf(5)&lt;/code&gt; — &lt;code&gt;InhibitDelayMaxSec=&lt;/code&gt;, &lt;code&gt;IdleAction=&lt;/code&gt;, &lt;code&gt;Handle*=&lt;/code&gt;, &lt;code&gt;*IgnoreInhibited=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;org.freedesktop.login1(5)&lt;/code&gt; — &lt;code&gt;Inhibit&lt;/code&gt;, &lt;code&gt;ListInhibitors&lt;/code&gt;, inhibitor-related properties and signals&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-logind.service(8)&lt;/code&gt; — login manager responsibilities (idle, keys, inhibition)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;busctl(1)&lt;/code&gt; — calling and reading logind properties from scripts&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Idle suspend and surprise reboots are policy problems. Take a lock with a clear &lt;code&gt;who&lt;/code&gt;/&lt;code&gt;why&lt;/code&gt;, keep it only as long as the job, and prefer &lt;code&gt;block&lt;/code&gt; for real work and &lt;code&gt;delay&lt;/code&gt; for prepare-hooks. Your backups — and your future self — will notice.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Polling Drop Directories: Practical systemd.path Units on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Tue, 06 Oct 2026 05:02:33 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-polling-drop-directories-practical-systemdpath-units-on-linux-4807</link>
      <guid>https://dev.to/lyraalishaikh/stop-polling-drop-directories-practical-systemdpath-units-on-linux-4807</guid>
      <description>&lt;p&gt;You have a drop directory. A scanner dumps PDFs into &lt;code&gt;/var/spool/inbox&lt;/code&gt;, a camera drops JPEGs into &lt;code&gt;/var/lib/captures&lt;/code&gt;, a CI agent writes a flag file when a build is ready. The usual “solution” is a shell loop:&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="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;find /var/spool/inbox &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-print0&lt;/span&gt; | xargs &lt;span class="nt"&gt;-0&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; process-one
  &lt;span class="nb"&gt;sleep &lt;/span&gt;5
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works until it does not. You burn wakeups when the directory is empty, miss batches that land between polls, race with partial writes, and end up reinventing supervision, logging, and restart policy around a &lt;code&gt;sleep&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;systemd already has a unit type for this: &lt;strong&gt;&lt;code&gt;.path&lt;/code&gt; units&lt;/strong&gt;. They watch filesystem paths with &lt;code&gt;inotify(7)&lt;/code&gt; and start a matching service when a condition becomes true. No busy loop. No extra daemon. Same journal, cgroup, and sandbox knobs you already use for services.&lt;/p&gt;

&lt;p&gt;This guide is operational. Behavior and defaults come from &lt;code&gt;systemd.path(5)&lt;/code&gt;, &lt;code&gt;systemd.service(5)&lt;/code&gt;, &lt;code&gt;systemd.unit(5)&lt;/code&gt;, &lt;code&gt;systemd.exec(5)&lt;/code&gt;, and stock units shipped with &lt;strong&gt;systemd 257&lt;/strong&gt; (verified on Debian 13).&lt;/p&gt;

&lt;h2&gt;
  
  
  What a path unit is (and is not)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.path&lt;/code&gt; unit&lt;/td&gt;
&lt;td&gt;Watch one or more absolute paths; enqueue activation when a condition matches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matching &lt;code&gt;.service&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Do the work (usually &lt;code&gt;Type=oneshot&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inotify(7)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kernel API systemd uses under the hood&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.timer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Time-based activation (calendar / monotonic), not filesystem events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.socket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Connection / datagram / FIFO activation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemd-tmpfiles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create/clean path &lt;em&gt;layout&lt;/em&gt;; it does not run your app on change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A path unit does &lt;strong&gt;not&lt;/strong&gt; replace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;continuous daemons that must hold state across every file,&lt;/li&gt;
&lt;li&gt;recursive watches over huge trees with millions of events,&lt;/li&gt;
&lt;li&gt;reliable watches of files changed only on a remote NFS server (inotify does not see remote-side mutations).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It &lt;em&gt;does&lt;/em&gt; replace most “poll a spool every N seconds” scripts on local filesystems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mental model
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;You install &lt;code&gt;foo.path&lt;/code&gt; and &lt;code&gt;foo.service&lt;/code&gt; (same basename by default).&lt;/li&gt;
&lt;li&gt;You &lt;strong&gt;enable and start the path unit&lt;/strong&gt;, not the service.&lt;/li&gt;
&lt;li&gt;While &lt;code&gt;foo.path&lt;/code&gt; is active, systemd watches the configured paths.&lt;/li&gt;
&lt;li&gt;When a condition matches, systemd starts &lt;code&gt;foo.service&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When the service exits (success or failure), systemd &lt;strong&gt;re-checks the paths immediately&lt;/strong&gt;. If the condition is still true, it starts the service again — with rate limits so a hot directory cannot busy-loop the machine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That re-check behavior is the important bit for drop folders: leave files in the spool until processing succeeds and removes them, and the path unit will keep draining the queue.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--version&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1
&lt;span class="c"&gt;# systemd 257 (or any modern systemd; path units are long-standing)&lt;/span&gt;
&lt;span class="c"&gt;# TriggerLimitIntervalSec=/TriggerLimitBurst= added in systemd 250&lt;/span&gt;
&lt;span class="c"&gt;# $TRIGGER_UNIT / $TRIGGER_PATH added in systemd 252&lt;/span&gt;

man systemd.path
systemctl list-units &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;path &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a typical host you will already see stock watches such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;systemd-ask-password-console.path&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;systemd-ask-password-wall.path&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are the same mechanism you are about to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five path conditions
&lt;/h2&gt;

&lt;p&gt;All paths must be &lt;strong&gt;absolute&lt;/strong&gt;. Multiple directives may be combined; assigning the empty string to any of them resets that list.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directive&lt;/th&gt;
&lt;th&gt;Fires when&lt;/th&gt;
&lt;th&gt;Immediate on path-unit start if already true?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PathExists=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The path exists&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PathExistsGlob=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;At least one path matches the glob&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DirectoryNotEmpty=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Directory contains at least one entry&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PathChanged=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Watched file/dir changes; for files, on &lt;strong&gt;close after write&lt;/strong&gt;, not every write&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PathModified=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Like &lt;code&gt;PathChanged=&lt;/code&gt;, but also on simple writes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Details that bite people in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hidden names&lt;/strong&gt; (leading &lt;code&gt;.&lt;/code&gt;) are generally &lt;strong&gt;ignored&lt;/strong&gt; while monitoring.&lt;/li&gt;
&lt;li&gt;If a path (or a parent) is inaccessible, systemd watches for permission changes and activates once access is possible.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PathChanged=&lt;/code&gt; is usually the right choice for “config file rewritten by an editor or &lt;code&gt;mv&lt;/code&gt;”. Use &lt;code&gt;PathModified=&lt;/code&gt; only when you truly need mid-write notifications.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DirectoryNotEmpty=&lt;/code&gt; is the classic drop-folder condition: activate while work remains.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Helpers for directories
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;MakeDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;DirectoryMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0750&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When true, systemd creates the watched directories before arming the watch (&lt;code&gt;DirectoryMode=&lt;/code&gt; defaults to &lt;code&gt;0755&lt;/code&gt;). &lt;strong&gt;&lt;code&gt;MakeDirectory=&lt;/code&gt; is ignored for &lt;code&gt;PathExists=&lt;/code&gt;&lt;/strong&gt; — existence watches do not create the target for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optional target unit override
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;other-name.service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Defaults to the same basename with a &lt;code&gt;.service&lt;/code&gt; suffix. Keep names aligned unless you have a reason not to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trigger rate limit (systemd 250+)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;TriggerLimitIntervalSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2s&lt;/span&gt;
&lt;span class="py"&gt;TriggerLimitBurst&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These defaults limit how often the &lt;em&gt;path unit itself&lt;/em&gt; may enqueue activations. Hit the limit and the path unit fails and &lt;strong&gt;stops watching&lt;/strong&gt; until restarted. Set either value to &lt;code&gt;0&lt;/code&gt; to disable. This limit is enforced &lt;strong&gt;before&lt;/strong&gt; the service job is queued.&lt;/p&gt;

&lt;p&gt;Separately, the &lt;strong&gt;service&lt;/strong&gt; still has &lt;code&gt;StartLimitIntervalSec=&lt;/code&gt; / &lt;code&gt;StartLimitBurst=&lt;/code&gt; from &lt;code&gt;systemd.unit(5)&lt;/code&gt;. When a path-triggered service hits its start limit, that failure is &lt;strong&gt;propagated to the path unit&lt;/strong&gt;, which then fails too — ending a trigger/start loop deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 0 — Read a stock path unit
&lt;/h2&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;systemd-ask-password-console.path
systemctl status systemd-ask-password-console.path &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unit is small and instructive (abbreviated):&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;Dispatch Password Requests to Console Directory Watch&lt;/span&gt;
&lt;span class="py"&gt;DefaultDependencies&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;span class="py"&gt;Before&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;paths.target cryptsetup.target&lt;/span&gt;
&lt;span class="c"&gt;# ... early-boot Conflicts=/Before= omitted ...
&lt;/span&gt;
&lt;span class="nn"&gt;[Path]&lt;/span&gt;
&lt;span class="py"&gt;DirectoryNotEmpty&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/run/systemd/ask-password&lt;/span&gt;
&lt;span class="py"&gt;MakeDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever &lt;code&gt;/run/systemd/ask-password&lt;/code&gt; is non-empty, systemd starts &lt;code&gt;systemd-ask-password-console.service&lt;/code&gt;, which runs &lt;code&gt;systemd-tty-ask-password-agent&lt;/code&gt;. That is path activation in production: a directory becomes the control plane, not a sleep loop.&lt;/p&gt;

&lt;p&gt;Default dependency picture for normal (non-early-boot) path units:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;After=&lt;/code&gt; + &lt;code&gt;Requires=&lt;/code&gt; on &lt;code&gt;sysinit.target&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Before=&lt;/code&gt; on &lt;code&gt;paths.target&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Conflicts=&lt;/code&gt; + &lt;code&gt;Before=&lt;/code&gt; on &lt;code&gt;shutdown.target&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Implicit &lt;code&gt;Before=&lt;/code&gt; on the unit they activate&lt;/li&gt;
&lt;li&gt;Automatic mount requirement/ordering if the watched path sits under another mount unit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only early-boot / late-shutdown watches should set &lt;code&gt;DefaultDependencies=no&lt;/code&gt; the way the ask-password units do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 1 — Drop-folder spool (complete)
&lt;/h2&gt;

&lt;p&gt;Goal: files appear in &lt;code&gt;/var/spool/lyra-inbox&lt;/code&gt;, a oneshot service processes each batch, successful files move to a done directory, failures move to a quarantine directory, and the path unit keeps draining until empty.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Directories and a processor script
&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 mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/spool/lyra-inbox /var/spool/lyra-done /var/spool/lyra-failed /usr/local/libexec
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /usr/local/libexec/lyra-inbox-process &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;'
#!/bin/bash
set -euo pipefail

INBOX=/var/spool/lyra-inbox
DONE=/var/spool/lyra-done
FAILED=/var/spool/lyra-failed

echo "trigger unit=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TRIGGER_UNIT&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt; path=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TRIGGER_PATH&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt; invocation=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INVOCATION_ID&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"

shopt -s nullglob
files=("&lt;/span&gt;&lt;span class="nv"&gt;$INBOX&lt;/span&gt;&lt;span class="sh"&gt;"/*)
if ((&lt;/span&gt;&lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt; == 0)); then
  echo "inbox empty on entry; nothing to do"
  exit 0
fi

for f in "&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"; do
  base=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
  # skip incomplete uploads if writers use a temp suffix
  case "&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="sh"&gt;" in
    *.tmp|*.partial) continue ;;
  esac

  echo "processing &lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="sh"&gt;"
  if process-file "&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="sh"&gt;"; then
    mv -f -- "&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="nv"&gt;$DONE&lt;/span&gt;&lt;span class="sh"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="sh"&gt;"
  else
    mv -f -- "&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="nv"&gt;$FAILED&lt;/span&gt;&lt;span class="sh"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="sh"&gt;"
  fi
done
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Demo "processor": accept *.ok, reject anything else&lt;/span&gt;
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /usr/local/bin/process-file &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;'
#!/bin/bash
set -euo pipefail
case "&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;" in
  *.ok) echo "accepted &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;"; exit 0 ;;
  *)    echo "rejected &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;" &amp;gt;&amp;amp;2; exit 1 ;;
esac
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0755 /usr/local/libexec/lyra-inbox-process /usr/local/bin/process-file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Service unit
&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/systemd/system/lyra-inbox.service &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;'
[Unit]
Description=Process lyra drop-folder inbox
Documentation=man:systemd.path(5)

[Service]
Type=oneshot
ExecStart=/usr/local/libexec/lyra-inbox-process
# Generous enough for bursts; tight enough to stop runaway loops
StartLimitIntervalSec=30s
StartLimitBurst=15
# Optional hardening for a local spool processor:
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/spool/lyra-inbox /var/spool/lyra-done /var/spool/lyra-failed
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Type=oneshot&lt;/code&gt; is the usual match for path activation: run, finish, exit. Do &lt;strong&gt;not&lt;/strong&gt; set &lt;code&gt;RemainAfterExit=yes&lt;/code&gt; here if you want repeated drains — a oneshot that remains “active” after exit will not be started again while it still looks active.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Path unit
&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/systemd/system/lyra-inbox.path &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;'
[Unit]
Description=Watch lyra drop-folder inbox
Documentation=man:systemd.path(5)

[Path]
DirectoryNotEmpty=/var/spool/lyra-inbox
MakeDirectory=yes
DirectoryMode=0755
# Optional extra safety on pathological writers (250+):
# TriggerLimitIntervalSec=2s
# TriggerLimitBurst=200

[Install]
WantedBy=multi-user.target
&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. Enable the &lt;strong&gt;path&lt;/strong&gt;, not the service
&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;systemctl daemon-reload
&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; lyra-inbox.path
systemctl status lyra-inbox.path &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="c"&gt;# Active: active (waiting)  ← armed and idle&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;enable --now&lt;/code&gt; on the &lt;code&gt;.path&lt;/code&gt; unit is the whole enable story. The service is pulled in on demand.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Prove it
&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;# Successful item&lt;/span&gt;
&lt;span class="nb"&gt;echo &lt;/span&gt;payload | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /var/spool/lyra-inbox/job1.ok &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="nb"&gt;sleep &lt;/span&gt;1
systemctl status lyra-inbox.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; lyra-inbox.service &lt;span class="nt"&gt;-n&lt;/span&gt; 20 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /var/spool/lyra-done

&lt;span class="c"&gt;# Failing item still leaves the inbox empty (moved to failed/)&lt;/span&gt;
&lt;span class="nb"&gt;echo &lt;/span&gt;bad | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /var/spool/lyra-inbox/job2.bad &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /var/spool/lyra-failed

&lt;span class="c"&gt;# Burst: path unit re-checks after each oneshot exit&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in &lt;/span&gt;3 4 5&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;x | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="s2"&gt;"/var/spool/lyra-inbox/job&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.ok"&lt;/span&gt; &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;done
&lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;2
&lt;span class="nb"&gt;ls&lt;/span&gt; /var/spool/lyra-inbox /var/spool/lyra-done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful introspection:&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 lyra-inbox.path
systemctl show lyra-inbox.path &lt;span class="nt"&gt;-p&lt;/span&gt; FragmentPath &lt;span class="nt"&gt;-p&lt;/span&gt; ActiveState &lt;span class="nt"&gt;-p&lt;/span&gt; SubState &lt;span class="nt"&gt;-p&lt;/span&gt; TriggeredUnits
systemctl show lyra-inbox.service &lt;span class="nt"&gt;-p&lt;/span&gt; Result &lt;span class="nt"&gt;-p&lt;/span&gt; ExecMainStatus &lt;span class="nt"&gt;-p&lt;/span&gt; InactiveExitTimestamp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Safe writers (avoid partial-file races)
&lt;/h3&gt;

&lt;p&gt;Path units can wake your service while a writer still has a file open. Patterns that work:&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;# Write aside, then atomic rename into the inbox&lt;/span&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; /var/spool/lyra-inbox/.partial.XXXXXX&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'data\n'&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="nb"&gt;mv&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; /var/spool/lyra-inbox/job7.ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or write to another filesystem directory and &lt;code&gt;mv&lt;/code&gt; across only when the final name is ready (same-filesystem &lt;code&gt;rename(2)&lt;/code&gt; is atomic). Teach the processor to ignore &lt;code&gt;.*&lt;/code&gt;, &lt;code&gt;*.tmp&lt;/code&gt;, and &lt;code&gt;*.partial&lt;/code&gt; names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 2 — Config reload on close-after-write
&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 tee&lt;/span&gt; /etc/systemd/system/lyra-config-reload.path &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;'
[Unit]
Description=Reload lyra agent when config is replaced

[Path]
PathChanged=/etc/lyra/agent.conf

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/lyra-config-reload.service &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;'
[Unit]
Description=Apply lyra agent config
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/systemctl reload lyra-agent.service
# Or: ExecStart=/usr/local/bin/lyra-agent --validate-and-reload
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/lyra
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'mode=safe'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/lyra/agent.conf &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&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; lyra-config-reload.path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because this uses &lt;code&gt;PathChanged=&lt;/code&gt;, a careful writer should replace the file with &lt;code&gt;mv&lt;/code&gt; (close + rename), not append forever with a long-lived writer that never closes. Editors that write a tempfile and rename will trigger once per save; that is usually what you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 3 — Existence gate and globs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# Fire once the TLS material has been provisioned
&lt;/span&gt;&lt;span class="py"&gt;PathExists&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/lyra/tls/fullchain.pem&lt;/span&gt;
&lt;span class="py"&gt;PathExists&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/lyra/tls/privkey.pem&lt;/span&gt;

&lt;span class="c"&gt;# Or: any certificate appeared
&lt;/span&gt;&lt;span class="py"&gt;PathExistsGlob&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/lyra/tls/*.pem&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Existence conditions activate &lt;strong&gt;immediately&lt;/strong&gt; if already true when the path unit starts. That makes them ideal for “wait until bootstrap dropped the files, then start the real service” without a custom oneshot loop at boot.&lt;/p&gt;

&lt;p&gt;Example pattern:&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;# bootstrap-ready.path
&lt;/span&gt;&lt;span class="nn"&gt;[Path]&lt;/span&gt;
&lt;span class="py"&gt;PathExists&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/lib/lyra/BOOTSTRAPPED&lt;/span&gt;

&lt;span class="c"&gt;# bootstrap-ready.service
&lt;/span&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/bin/systemctl start lyra-agent.service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Environment variables on triggered runs
&lt;/h2&gt;

&lt;p&gt;For units activated by a path or timer, systemd may set (best-effort, systemd 252+):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$TRIGGER_UNIT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Name of the path/timer unit that triggered this run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$TRIGGER_PATH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Path-related trigger detail when applicable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;$TRIGGER_TIMER_REALTIME_USEC&lt;/code&gt; / &lt;code&gt;$TRIGGER_TIMER_MONOTONIC_USEC&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Timer trigger timestamps when applicable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$INVOCATION_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unique ID for this activation cycle&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Treat &lt;code&gt;$TRIGGER_*&lt;/code&gt; as &lt;strong&gt;debugging hints&lt;/strong&gt;. Multiple rapid triggers can coalesce; there is no guarantee which event is reported. Do not build exclusive locking or audit trails solely on these variables — derive work from directory state instead (as the spool lab does).&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and how to recover
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Path unit &lt;code&gt;active (waiting)&lt;/code&gt; but service never runs&lt;/td&gt;
&lt;td&gt;Condition false; hidden &lt;code&gt;.*&lt;/code&gt; files only; wrong absolute path&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ls -la&lt;/code&gt; the directory; remember dotfiles are ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service runs once, never again while files remain&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RemainAfterExit=yes&lt;/code&gt; or long-running &lt;code&gt;Type=simple&lt;/code&gt; still active&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;Type=oneshot&lt;/code&gt; without &lt;code&gt;RemainAfterExit=&lt;/code&gt; for drain loops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path unit failed, no longer watching&lt;/td&gt;
&lt;td&gt;Hit &lt;code&gt;TriggerLimit*&lt;/code&gt; or service &lt;code&gt;StartLimit*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl reset-failed lyra-inbox.service lyra-inbox.path &amp;amp;&amp;amp; systemctl start lyra-inbox.path&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missed remote uploads on NFS&lt;/td&gt;
&lt;td&gt;inotify does not see remote-side changes&lt;/td&gt;
&lt;td&gt;Watch on the writer host, or use a timer/&lt;code&gt;find&lt;/code&gt; hybrid for that mount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial files processed&lt;/td&gt;
&lt;td&gt;Writer still open, or non-atomic publish&lt;/td&gt;
&lt;td&gt;temp + &lt;code&gt;mv&lt;/code&gt;; ignore &lt;code&gt;*.tmp&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission denied on spool&lt;/td&gt;
&lt;td&gt;Service user cannot read/write paths&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;User=&lt;/code&gt; + ownership, or &lt;code&gt;ReadWritePaths=&lt;/code&gt; under hardening&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl reset-failed lyra-inbox.service lyra-inbox.path
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart lyra-inbox.path
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; lyra-inbox.path &lt;span class="nt"&gt;-u&lt;/span&gt; lyra-inbox.service &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Path vs timer vs socket vs tmpfiles
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Run when a directory has work or a file appears/changes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;.path&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run at 04:00 or every 15 minutes regardless of filesystem events&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;.timer&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run when a TCP/Unix client connects&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;.socket&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ensure &lt;code&gt;/run/myapp&lt;/code&gt; exists with mode 0750 each boot&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;systemd-tmpfiles&lt;/code&gt;&lt;/strong&gt; / &lt;code&gt;RuntimeDirectory=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continuously convert a stream with held state&lt;/td&gt;
&lt;td&gt;Long-running &lt;code&gt;.service&lt;/code&gt; (optionally &lt;em&gt;also&lt;/em&gt; path-activated for idle-start)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Composing them is normal: tmpfiles creates the spool, a path unit watches it, a hardened oneshot processes it, and a timer runs a daily “stuck file” report as a safety net.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&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;systemctl disable &lt;span class="nt"&gt;--now&lt;/span&gt; lyra-inbox.path lyra-config-reload.path 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/systemd/system/lyra-inbox.path /etc/systemd/system/lyra-inbox.service
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/systemd/system/lyra-config-reload.path /etc/systemd/system/lyra-config-reload.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/spool/lyra-inbox /var/spool/lyra-done /var/spool/lyra-failed
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /usr/local/libexec/lyra-inbox-process /usr/local/bin/process-file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Practical defaults I use
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DirectoryNotEmpty=&lt;/code&gt; + oneshot drain&lt;/strong&gt; for spools; delete or move work before exit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic publish&lt;/strong&gt; (&lt;code&gt;mv&lt;/code&gt;) from writers; ignore temporary suffixes in the processor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable the &lt;code&gt;.path&lt;/code&gt; unit&lt;/strong&gt;; let it own activation of the service.&lt;/li&gt;
&lt;li&gt;Keep &lt;strong&gt;&lt;code&gt;StartLimitBurst=&lt;/code&gt;&lt;/strong&gt; intentional on the service — silent infinite restart loops are worse than a failed path unit you can alert on.&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;&lt;code&gt;TriggerLimit*&lt;/code&gt;&lt;/strong&gt; only when writers are bursty or untrusted; defaults (2s / 200) are already a backstop on systemd 250+.&lt;/li&gt;
&lt;li&gt;Prefer &lt;strong&gt;&lt;code&gt;PathChanged=&lt;/code&gt;&lt;/strong&gt; over &lt;strong&gt;&lt;code&gt;PathModified=&lt;/code&gt;&lt;/strong&gt; for config files.&lt;/li&gt;
&lt;li&gt;Do not expect NFS/CIFS remote mutations to wake you; keep watches local to the writer where possible.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd.path(5)&lt;/code&gt; — path unit options, inotify basis, re-check-after-service-exit, &lt;code&gt;TriggerLimitIntervalSec=&lt;/code&gt; / &lt;code&gt;TriggerLimitBurst=&lt;/code&gt; (250+)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.service(5)&lt;/code&gt; — &lt;code&gt;Type=oneshot&lt;/code&gt;, service startup types&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.unit(5)&lt;/code&gt; — &lt;code&gt;StartLimitIntervalSec=&lt;/code&gt; / &lt;code&gt;StartLimitBurst=&lt;/code&gt;, unit search paths, dependencies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd.exec(5)&lt;/code&gt; — &lt;code&gt;$TRIGGER_UNIT&lt;/code&gt;, &lt;code&gt;$TRIGGER_PATH&lt;/code&gt;, &lt;code&gt;$INVOCATION_ID&lt;/code&gt; (trigger vars since 252)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inotify(7)&lt;/code&gt; — kernel watch API and limitations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemctl(1)&lt;/code&gt; — &lt;code&gt;enable --now&lt;/code&gt;, &lt;code&gt;list-units --type=path&lt;/code&gt;, &lt;code&gt;reset-failed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Stock units: &lt;code&gt;/lib/systemd/system/systemd-ask-password-console.path&lt;/code&gt;, &lt;code&gt;systemd-ask-password-wall.path&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Related reading on this blog: &lt;a href="https://dev.to/lyraalishaikh/stop-hand-crafting-run-and-tmp-trees-practical-systemd-tmpfiles-on-linux-3561"&gt;systemd-tmpfiles&lt;/a&gt; for directory layout, &lt;a href="https://dev.to/lyraalishaikh/stop-losing-user-services-on-logout-practical-loginctl-linger-on-linux-2m8o"&gt;loginctl linger&lt;/a&gt; for user-instance path units that must survive logout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Path units turn “something landed on disk” into the same control plane you already trust for services and timers. Once the drop folder owns a &lt;code&gt;.path&lt;/code&gt; + oneshot pair, the &lt;code&gt;sleep 5&lt;/code&gt; watcher can go in the scrap pile where it belongs.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Scraping systemctl Output: Practical busctl on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 05 Oct 2026 05:02:51 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-scraping-systemctl-output-practical-busctl-on-linux-56bp</link>
      <guid>https://dev.to/lyraalishaikh/stop-scraping-systemctl-output-practical-busctl-on-linux-56bp</guid>
      <description>&lt;p&gt;You need one stable answer from the service manager: is this unit active, what is its main PID, which links does networkd own, did NTP lock? The usual shortcut is to scrape &lt;code&gt;systemctl show&lt;/code&gt; or &lt;code&gt;systemctl status&lt;/code&gt; text. That works until a column moves, a locale changes the wording, or a script runs against a container whose &lt;code&gt;systemctl&lt;/code&gt; is a different generation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;busctl&lt;/code&gt; talks to the same D-Bus APIs those tools already use. You get typed method calls, property reads, object trees, live monitors, and JSON-friendly output without inventing a parser for human-oriented CLI text.&lt;/p&gt;

&lt;p&gt;This guide is operational. Commands and behavior come from &lt;code&gt;busctl(1)&lt;/code&gt;, the D-Bus type system, and live read-only labs against systemd &lt;strong&gt;257&lt;/strong&gt; on Debian 13. Mutating examples (&lt;code&gt;StartUnit&lt;/code&gt;, &lt;code&gt;set-property&lt;/code&gt;) are quoted from the man page so you can run them deliberately on a lab host.&lt;/p&gt;

&lt;h2&gt;
  
  
  What busctl is (and is not)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;D-Bus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Binary IPC message bus: system bus + per-user session buses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;busctl&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Operator CLI to list peers, introspect objects, call methods, read/write properties, monitor traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;systemctl&lt;/code&gt; / &lt;code&gt;hostnamectl&lt;/code&gt; / …&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Friendly wrappers over a subset of the same APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;varlinkctl&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Parallel stack for Varlink JSON services (resolved, many newer helpers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;dbus-send&lt;/code&gt; / &lt;code&gt;gdbus&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Older/general D-Bus clients; &lt;code&gt;busctl&lt;/code&gt; is the systemd-native ergonomic tool&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;busctl&lt;/code&gt; is not a replacement for every shell one-liner. Use it when you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stable machine-readable state (especially with &lt;code&gt;-j&lt;/code&gt; / &lt;code&gt;--json=&lt;/code&gt;),&lt;/li&gt;
&lt;li&gt;APIs that have no polished CLI yet,&lt;/li&gt;
&lt;li&gt;debugging of who owns a name and what objects they export,&lt;/li&gt;
&lt;li&gt;the same call path your production client will use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do &lt;strong&gt;not&lt;/strong&gt; need to abandon &lt;code&gt;varlinkctl&lt;/code&gt; where a service already speaks Varlink. D-Bus remains the primary control plane for &lt;code&gt;org.freedesktop.systemd1&lt;/code&gt;, &lt;code&gt;login1&lt;/code&gt;, &lt;code&gt;hostname1&lt;/code&gt;, &lt;code&gt;timedate1&lt;/code&gt;, &lt;code&gt;network1&lt;/code&gt;, and many third-party daemons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&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;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; busctl
busctl &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# busctl itself: systemd 209+&lt;/span&gt;
&lt;span class="c"&gt;# wait + --limit-messages=: 257+&lt;/span&gt;
&lt;span class="c"&gt;# --capsule=: 256+&lt;/span&gt;
&lt;span class="c"&gt;# --json= broadly available on modern systemd; behavior verified on 257&lt;/span&gt;

&lt;span class="c"&gt;# Default target is the system bus:&lt;/span&gt;
busctl list &lt;span class="nt"&gt;--acquired&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most read-only calls against systemd, hostname1, timedate1, and resolve1 work as a normal user. Start/stop/enable and some host mutations need root or a polkit rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mental model: service → object → interface → member
&lt;/h2&gt;

&lt;p&gt;Every call needs four coordinates (properties drop the method name and use a property name instead):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SERVICE     well-known or unique bus name   e.g. org.freedesktop.systemd1
OBJECT      object path                     e.g. /org/freedesktop/systemd1
INTERFACE   interface name                  e.g. org.freedesktop.systemd1.Manager
MEMBER      method, property, or signal     e.g. GetDefaultTarget / Version / UnitNew
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;D-Bus also has &lt;strong&gt;unique&lt;/strong&gt; names like &lt;code&gt;:1.4&lt;/code&gt; (connection IDs) and &lt;strong&gt;well-known&lt;/strong&gt; names like &lt;code&gt;org.freedesktop.systemd1&lt;/code&gt; (stable aliases services claim).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 1 — Inventory the bus
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Peers that currently hold well-known names&lt;/span&gt;
busctl list &lt;span class="nt"&gt;--acquired&lt;/span&gt;

&lt;span class="c"&gt;# Only unique connection IDs&lt;/span&gt;
busctl list &lt;span class="nt"&gt;--unique&lt;/span&gt;

&lt;span class="c"&gt;# Names that can be activated on demand but are not running yet&lt;/span&gt;
busctl list &lt;span class="nt"&gt;--activatable&lt;/span&gt;

&lt;span class="c"&gt;# Who owns the bus / a name / a PID&lt;/span&gt;
busctl status
busctl status org.freedesktop.resolve1
busctl status 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a typical host you will see names such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;org.freedesktop.systemd1&lt;/code&gt; — PID 1 service manager&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;org.freedesktop.login1&lt;/code&gt; — logind&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;org.freedesktop.hostname1&lt;/code&gt; / &lt;code&gt;timedate1&lt;/code&gt; / &lt;code&gt;locale1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;org.freedesktop.network1&lt;/code&gt; / &lt;code&gt;resolve1&lt;/code&gt; when networkd/resolved are enabled&lt;/li&gt;
&lt;li&gt;third-party names (&lt;code&gt;Avahi&lt;/code&gt;, desktop portals, containers, …)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;status&lt;/code&gt; prints process credentials (UID/GID, cgroup, unit, command line). That is the fastest way to answer “which unit owns this bus name?” without grepping &lt;code&gt;ps&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  User bus and containers
&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;# Calling user's session bus&lt;/span&gt;
busctl &lt;span class="nt"&gt;--user&lt;/span&gt; list &lt;span class="nt"&gt;--acquired&lt;/span&gt;

&lt;span class="c"&gt;# Another user's user bus on the host (needs privileges)&lt;/span&gt;
busctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;debian@.host list &lt;span class="nt"&gt;--acquired&lt;/span&gt;

&lt;span class="c"&gt;# System bus inside a local machine/container registered with machined&lt;/span&gt;
&lt;span class="c"&gt;# busctl --machine=myvm list --acquired&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--machine=&lt;/code&gt; and &lt;code&gt;--host=&lt;/code&gt; mirror the rest of the systemd tooling family. Capsules (256+) use &lt;code&gt;--capsule=&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 2 — Tree and introspect
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Object tree for the service manager&lt;/span&gt;
busctl tree org.freedesktop.systemd1 | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-40&lt;/span&gt;

&lt;span class="c"&gt;# Flat path list&lt;/span&gt;
busctl tree &lt;span class="nt"&gt;--list&lt;/span&gt; org.freedesktop.systemd1 | &lt;span class="nb"&gt;head&lt;/span&gt;

&lt;span class="c"&gt;# Manager object members&lt;/span&gt;
busctl introspect org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-60&lt;/span&gt;

&lt;span class="c"&gt;# Raw Introspectable XML (useful for codegen / deep dumps)&lt;/span&gt;
busctl introspect &lt;span class="nt"&gt;--xml-interface&lt;/span&gt; org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unit object paths are &lt;strong&gt;escaped&lt;/strong&gt;. Dots become &lt;code&gt;_2e&lt;/code&gt;, dashes &lt;code&gt;_2d&lt;/code&gt;, and so on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemd-journald.service  →  /org/freedesktop/systemd1/unit/systemd_2djournald_2eservice
cron.service              →  /org/freedesktop/systemd1/unit/cron_2eservice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not hand-build those paths in scripts. Call &lt;code&gt;GetUnit&lt;/code&gt; (below) and use the returned object path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 3 — Properties without text scraping
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Manager identity&lt;/span&gt;
busctl get-property org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager &lt;span class="se"&gt;\&lt;/span&gt;
  Version Architecture Virtualization

&lt;span class="c"&gt;# Terse vs verbose vs JSON&lt;/span&gt;
busctl get-property org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager Environment

busctl get-property &lt;span class="nt"&gt;--verbose&lt;/span&gt; org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager Environment

busctl get-property &lt;span class="nt"&gt;-j&lt;/span&gt; org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager Version
&lt;span class="c"&gt;# → {"type":"s","data":"257.13-1~deb13u1"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other high-signal hosts:&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;# hostname1&lt;/span&gt;
busctl get-property org.freedesktop.hostname1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/hostname1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.hostname1 &lt;span class="se"&gt;\&lt;/span&gt;
  Hostname KernelName KernelRelease OperatingSystemPrettyName Chassis

&lt;span class="c"&gt;# timedate1&lt;/span&gt;
busctl get-property org.freedesktop.timedate1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/timedate1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.timedate1 &lt;span class="se"&gt;\&lt;/span&gt;
  Timezone NTP NTPSynchronized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example outputs from a live Debian 13 KVM guest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s "openclaw-host1"
s "Linux"
s "6.12.111+deb13-amd64"
s "Debian GNU/Linux 13 (trixie)"
s "vm"

s "Etc/UTC"
b true
b true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terse mode prefixes every value with its D-Bus type code (&lt;code&gt;s&lt;/code&gt; string, &lt;code&gt;b&lt;/code&gt; boolean, &lt;code&gt;u&lt;/code&gt; uint32, &lt;code&gt;as&lt;/code&gt; array of strings, …). That is intentional: scripts can branch on type without guessing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resolve a unit, then read its state
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;UNIT_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;busctl call org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager GetUnit s systemd-journald.service &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;&lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

busctl get-property org.freedesktop.systemd1 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$UNIT_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Unit &lt;span class="se"&gt;\&lt;/span&gt;
  Id Description LoadState ActiveState SubState

busctl get-property org.freedesktop.systemd1 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$UNIT_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Service MainPID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s "systemd-journald.service"
s "Journal Service"
s "loaded"
s "active"
s "running"
u 331
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the stable equivalent of grepping &lt;code&gt;systemctl status&lt;/code&gt; for “Active:” and “Main PID:”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 4 — Method calls and signature syntax
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;call&lt;/code&gt; takes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;busctl call SERVICE OBJECT INTERFACE METHOD [SIGNATURE [ARGS...]]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature string is the D-Bus type signature for the &lt;strong&gt;arguments you pass&lt;/strong&gt;, not for the return value. Common codes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;busctl argument shape&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;one string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt;/&lt;code&gt;yes&lt;/code&gt;/&lt;code&gt;no&lt;/code&gt;/&lt;code&gt;1&lt;/code&gt;/&lt;code&gt;0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;y&lt;/code&gt; &lt;code&gt;n&lt;/code&gt; &lt;code&gt;q&lt;/code&gt; &lt;code&gt;i&lt;/code&gt; &lt;code&gt;u&lt;/code&gt; &lt;code&gt;x&lt;/code&gt; &lt;code&gt;t&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;integers&lt;/td&gt;
&lt;td&gt;decimal string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;float string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;o&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;object path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/org/...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;as&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;array of strings&lt;/td&gt;
&lt;td&gt;count, then N strings: &lt;code&gt;as 2 a b&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a{sv}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dict of string→variant&lt;/td&gt;
&lt;td&gt;count, then key type value…&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(…)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;struct&lt;/td&gt;
&lt;td&gt;fields inline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;variant&lt;/td&gt;
&lt;td&gt;inner signature + value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Examples from &lt;code&gt;busctl(1)&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;# string&lt;/span&gt;
s jawoll

&lt;span class="c"&gt;# string array with three entries&lt;/span&gt;
as 3 hello world foobar

&lt;span class="c"&gt;# dict string → variant&lt;/span&gt;
a&lt;span class="o"&gt;{&lt;/span&gt;sv&lt;span class="o"&gt;}&lt;/span&gt; 3 One s Eins Two u 2 Yes b &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Safe read-only manager calls
&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;# Default target&lt;/span&gt;
busctl call org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager GetDefaultTarget
&lt;span class="c"&gt;# s "graphical.target"   # or multi-user.target, etc.&lt;/span&gt;

&lt;span class="c"&gt;# Unit file state (enabled/disabled/static/…)&lt;/span&gt;
busctl call org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager GetUnitFileState s systemd-journald.service
&lt;span class="c"&gt;# s "static"&lt;/span&gt;

&lt;span class="c"&gt;# List matching unit files (patterns arrays: states, then names)&lt;/span&gt;
busctl call org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager ListUnitFilesByPatterns &lt;span class="se"&gt;\&lt;/span&gt;
  asas 0 1 &lt;span class="s1"&gt;'ssh*.service'&lt;/span&gt;

&lt;span class="c"&gt;# Failed units only (empty array if healthy)&lt;/span&gt;
busctl call org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager ListUnitsFiltered as 1 failed

&lt;span class="c"&gt;# networkd links&lt;/span&gt;
busctl call org.freedesktop.network1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/network1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.network1.Manager ListLinks

&lt;span class="c"&gt;# logind sessions&lt;/span&gt;
busctl call org.freedesktop.login1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/login1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.login1.Manager ListSessions

&lt;span class="c"&gt;# Who owns a well-known name right now?&lt;/span&gt;
busctl call org.freedesktop.DBus &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/DBus &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.DBus GetNameOwner s org.freedesktop.systemd1
&lt;span class="c"&gt;# s ":1.4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Controlled mutation (lab only)
&lt;/h3&gt;

&lt;p&gt;From the official man-page examples — run only where you intend to change 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="c"&gt;# Start a unit (job mode "replace"); returns a job object path&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;busctl call org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager StartUnit &lt;span class="se"&gt;\&lt;/span&gt;
  ss &lt;span class="s2"&gt;"cups.service"&lt;/span&gt; &lt;span class="s2"&gt;"replace"&lt;/span&gt;
&lt;span class="c"&gt;# o "/org/freedesktop/systemd1/job/42684"&lt;/span&gt;

&lt;span class="c"&gt;# Temporary manager log level (restores on daemon-reexec/reboot policy dependent)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;busctl set-property org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager LogLevel s debug

busctl get-property org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager LogLevel
&lt;span class="c"&gt;# s "debug"&lt;/span&gt;

&lt;span class="c"&gt;# Put it back&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;busctl set-property org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager LogLevel s info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful &lt;code&gt;call&lt;/code&gt; flags:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;-q&lt;/code&gt; / &lt;code&gt;--quiet&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Suppress reply payload; exit code still reports success/failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--expect-reply=no&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fire-and-forget (no exit status from the method result)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--auto-start=no&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Do not activate a stopped service just to talk to it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--allow-interactive-authorization=no&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fail instead of prompting polkit/agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--timeout=5s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cap wait (default 25s for &lt;code&gt;call&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;-j&lt;/code&gt; / &lt;code&gt;--json=&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Structured output for replies/properties&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Lab 5 — Monitor, capture, wait
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Everything (noisy — use on a quiet lab)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;busctl monitor

&lt;span class="c"&gt;# Only traffic involving the service manager&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;busctl monitor org.freedesktop.systemd1

&lt;span class="c"&gt;# Match rules (see sd_bus_add_match(3))&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;busctl monitor &lt;span class="nt"&gt;--match&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"type='signal',interface='org.freedesktop.systemd1.Manager'"&lt;/span&gt;

&lt;span class="c"&gt;# Exit after N messages (257+)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;busctl monitor &lt;span class="nt"&gt;-N&lt;/span&gt; 5 org.freedesktop.systemd1

&lt;span class="c"&gt;# pcapng for Wireshark&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;busctl capture org.freedesktop.systemd1 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/bus.pcapng
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Waiting for a single signal (257+):&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;# Block until the manager emits a signal (example shape)&lt;/span&gt;
busctl &lt;span class="nb"&gt;wait&lt;/span&gt; /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager UnitNew
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;wait&lt;/code&gt; can omit the service name to accept the signal from any sender. Combine with &lt;code&gt;--timeout=&lt;/code&gt; and &lt;code&gt;-N&lt;/code&gt; when you want bounded automation instead of an open-ended trap.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;emit&lt;/code&gt; (242+) synthesizes a signal — useful for testing clients, rarely needed in day-2 ops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;busctl emit /com/example/Obj com.example.Iface Ping
&lt;span class="c"&gt;# optional: --destination=org.example.Service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 6 — Small automation patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JSON health probe
&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;ver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;busctl get-property &lt;span class="nt"&gt;-j&lt;/span&gt; org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager Version | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .data&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;ntp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;busctl get-property &lt;span class="nt"&gt;-j&lt;/span&gt; org.freedesktop.timedate1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/timedate1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.timedate1 NTPSynchronized | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .data&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;failed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;busctl call &lt;span class="nt"&gt;-j&lt;/span&gt; org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager ListUnitsFiltered as 1 failed&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"systemd=&lt;/span&gt;&lt;span class="nv"&gt;$ver&lt;/span&gt;&lt;span class="s2"&gt; ntp_synced=&lt;/span&gt;&lt;span class="nv"&gt;$ntp&lt;/span&gt;&lt;span class="s2"&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;$failed&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-j&lt;/code&gt; embeds D-Bus type metadata (&lt;code&gt;{"type":"s","data":"..."}&lt;/code&gt;) so conversions stay lossless. Pipe through &lt;code&gt;jq&lt;/code&gt; only after you decide which fields matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Map PID → unit without fragile cgroup string cuts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?usage:&lt;span class="p"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt; &amp;lt;pid&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;PATH_OBJ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;busctl call org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager GetUnitByPID u &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="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;&lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

busctl get-property org.freedesktop.systemd1 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATH_OBJ&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Unit Id ActiveState
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Prefer GetUnit over hand-escaped paths
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;systemd-networkd.service
&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;busctl call org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager GetUnit s &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;&lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&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 → %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  busctl vs varlinkctl vs systemctl
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Reach for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Human operator UX, unit lifecycle day-to-day&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemctl&lt;/code&gt;, &lt;code&gt;journalctl&lt;/code&gt;, &lt;code&gt;loginctl&lt;/code&gt;, …&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stable D-Bus method/property access, scripts, unfamiliar services&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;busctl&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON Varlink sockets (&lt;code&gt;io.systemd.*&lt;/code&gt;, custom tools)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;varlinkctl&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-off debug of who is on the bus&lt;/td&gt;
&lt;td&gt;&lt;code&gt;busctl list/status/monitor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wireshark-level bus forensics&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;busctl capture&lt;/code&gt; → pcapng&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Many components expose &lt;strong&gt;both&lt;/strong&gt; stacks over time. Resolved is a good example: CLI/&lt;code&gt;resolvectl&lt;/code&gt; for humans, Varlink for JSON-native automation, and classic D-Bus still present on a lot of hosts. Pick the API the service documents as primary; use &lt;code&gt;busctl introspect&lt;/code&gt; / &lt;code&gt;varlinkctl introspect&lt;/code&gt; when docs lag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational caveats
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Policy still applies.&lt;/strong&gt; A successful &lt;code&gt;introspect&lt;/code&gt; does not mean every method is callable. polkit and D-Bus policy may require root or an interactive authorization agent (&lt;code&gt;--allow-interactive-authorization=&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activation side effects.&lt;/strong&gt; Default &lt;code&gt;--auto-start=yes&lt;/code&gt; can start a service just because you talked to its name. Use &lt;code&gt;--auto-start=no&lt;/code&gt; in careful probes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escaped paths.&lt;/strong&gt; Never concatenate unit names into object paths by hand for production scripts — call &lt;code&gt;GetUnit&lt;/code&gt; / &lt;code&gt;LoadUnit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output stability.&lt;/strong&gt; Terse type-prefixed text and &lt;code&gt;--json=&lt;/code&gt; are the machine interfaces. Do not scrape &lt;code&gt;busctl tree&lt;/code&gt; art or &lt;code&gt;status&lt;/code&gt; human columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Noise and privilege on monitor.&lt;/strong&gt; Full-bus &lt;code&gt;monitor&lt;/code&gt; is root-heavy and high volume; always filter with a service name, &lt;code&gt;--match=&lt;/code&gt;, or &lt;code&gt;-N&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version gates.&lt;/strong&gt; &lt;code&gt;wait&lt;/code&gt; and &lt;code&gt;--limit-messages=&lt;/code&gt; need systemd &lt;strong&gt;257+&lt;/strong&gt;. Confirm with &lt;code&gt;busctl --version&lt;/code&gt; before baking them into fleet scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User vs system bus.&lt;/strong&gt; Forgetting &lt;code&gt;--user&lt;/code&gt; is the most common “Name has no owner” footgun for session services.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Hands-on mini lab (read-only)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1) Who is on the system bus?&lt;/span&gt;
busctl list &lt;span class="nt"&gt;--acquired&lt;/span&gt;

&lt;span class="c"&gt;# 2) Confirm manager version over D-Bus (not shell scraping)&lt;/span&gt;
busctl get-property &lt;span class="nt"&gt;-j&lt;/span&gt; org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager Version

&lt;span class="c"&gt;# 3) Default target + journald state&lt;/span&gt;
busctl call org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager GetDefaultTarget

&lt;span class="nv"&gt;JP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;busctl call org.freedesktop.systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/systemd1 &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Manager GetUnit s systemd-journald.service &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;&lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
busctl get-property org.freedesktop.systemd1 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Unit ActiveState SubState
busctl get-property org.freedesktop.systemd1 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  org.freedesktop.systemd1.Service MainPID

&lt;span class="c"&gt;# 4) Host + time snapshot&lt;/span&gt;
busctl get-property org.freedesktop.hostname1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/hostname1 org.freedesktop.hostname1 &lt;span class="se"&gt;\&lt;/span&gt;
  Hostname OperatingSystemPrettyName Chassis
busctl get-property org.freedesktop.timedate1 &lt;span class="se"&gt;\&lt;/span&gt;
  /org/freedesktop/timedate1 org.freedesktop.timedate1 &lt;span class="se"&gt;\&lt;/span&gt;
  Timezone NTPSynchronized

&lt;span class="c"&gt;# 5) Optional: one filtered monitor sample (Ctrl+C or -N 3)&lt;/span&gt;
&lt;span class="c"&gt;# sudo busctl monitor -N 3 org.freedesktop.systemd1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those five blocks work, you can replace a surprising amount of brittle &lt;code&gt;systemctl | awk&lt;/code&gt; glue with explicit D-Bus calls.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;busctl(1)&lt;/code&gt; — commands, signature formatting, examples (&lt;code&gt;StartUnit&lt;/code&gt;, &lt;code&gt;LogLevel&lt;/code&gt;, verbose arrays)&lt;/li&gt;
&lt;li&gt;D-Bus Specification — &lt;a href="https://dbus.freedesktop.org/doc/dbus-specification.html#type-system" rel="noopener noreferrer"&gt;Type system&lt;/a&gt; (basic types, arrays, variants, dicts)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sd_bus_add_match(3)&lt;/code&gt; — match rule language used by &lt;code&gt;busctl monitor --match=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sd-bus(3)&lt;/code&gt; — C API behind the same stack&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;org.freedesktop.systemd1&lt;/code&gt; Manager interface — discovered live via &lt;code&gt;busctl introspect&lt;/code&gt; (methods such as &lt;code&gt;GetUnit&lt;/code&gt;, &lt;code&gt;ListUnitsFiltered&lt;/code&gt;, &lt;code&gt;StartUnit&lt;/code&gt;, &lt;code&gt;GetUnitByPID&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Companion tooling: &lt;code&gt;varlinkctl(1)&lt;/code&gt;, &lt;code&gt;systemctl(1)&lt;/code&gt;, &lt;code&gt;machinectl(1)&lt;/code&gt;, &lt;code&gt;wireshark(1)&lt;/code&gt; for pcapng captures&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;D-Bus is not going away just because Varlink exists. &lt;code&gt;busctl&lt;/code&gt; is the difference between hoping a text UI stays parseable and calling the same interface your language bindings will use in production — with types, object paths, and exit codes that mean something.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Hand-Rolling Service Accounts: Practical systemd-sysusers on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sun, 04 Oct 2026 05:01:55 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-hand-rolling-service-accounts-practical-systemd-sysusers-on-linux-o36</link>
      <guid>https://dev.to/lyraalishaikh/stop-hand-rolling-service-accounts-practical-systemd-sysusers-on-linux-o36</guid>
      <description>&lt;p&gt;Most Linux packages still ship shell that 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;getent group myapp &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; groupadd &lt;span class="nt"&gt;--system&lt;/span&gt; myapp
getent passwd myapp &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; useradd &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--gid&lt;/span&gt; myapp &lt;span class="nt"&gt;--home-dir&lt;/span&gt; /var/lib/myapp &lt;span class="nt"&gt;--shell&lt;/span&gt; /usr/sbin/nologin myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works until someone races the script, a container image rebuild forgets the accounts, an admin overrides &lt;code&gt;/etc/login.defs&lt;/code&gt;, or you need the same identity on a chroot, a DDI, and a live host without copy-pasting three different &lt;code&gt;useradd&lt;/code&gt; dialects.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd-sysusers&lt;/code&gt; is the declarative fix. Drop one line per account under &lt;code&gt;sysusers.d/&lt;/code&gt;, run it at package install or early boot, and the system users appear the same way every time — without inventing human logins, and without touching NIS/LDAP databases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is (and is not)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-sysusers&lt;/code&gt; reads &lt;code&gt;sysusers.d&lt;/code&gt; fragments and creates &lt;strong&gt;system&lt;/strong&gt; users and groups by writing &lt;code&gt;/etc/passwd&lt;/code&gt; and &lt;code&gt;/etc/group&lt;/code&gt; directly. That is intentional:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is for daemon identities and privilege separation, not for people.&lt;/li&gt;
&lt;li&gt;It bypasses complex remote user databases (NIS, LDAP, and friends).&lt;/li&gt;
&lt;li&gt;System users must resolve &lt;strong&gt;without the network&lt;/strong&gt;, including early boot and initrd paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need portable encrypted human homes, that is &lt;code&gt;systemd-homed&lt;/code&gt;. If you want a throwaway UID for one unit with no persistent name, that is &lt;code&gt;DynamicUser=&lt;/code&gt;. &lt;code&gt;sysusers&lt;/code&gt; sits in the middle: named, persistent, local system accounts that packages and images can declare.&lt;/p&gt;

&lt;p&gt;The boot unit is &lt;code&gt;systemd-sysusers.service&lt;/code&gt;. You can also invoke the tool by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Config locations and precedence
&lt;/h2&gt;

&lt;p&gt;Search paths (highest override wins on the same basename):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/etc/sysusers.d/*.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local administrator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/run/sysusers.d/*.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/usr/local/lib/sysusers.d/*.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local software installs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/usr/lib/sysusers.d/*.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Vendor / package&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rules from &lt;code&gt;sysusers.d(5)&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Packages ship files under &lt;code&gt;/usr/lib/sysusers.d/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Admins override by placing the &lt;strong&gt;same filename&lt;/strong&gt; under &lt;code&gt;/etc/sysusers.d/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;All files are sorted &lt;strong&gt;lexicographically by filename&lt;/strong&gt;, regardless of directory.&lt;/li&gt;
&lt;li&gt;If multiple files define the same user/group name, the &lt;strong&gt;lexicographically earliest&lt;/strong&gt; filename wins; later conflicts are logged as warnings.&lt;/li&gt;
&lt;li&gt;Disable a vendor snippet without deleting package files:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /dev/null /etc/sysusers.d/some-vendor.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Name files like &lt;code&gt;package.conf&lt;/code&gt; or &lt;code&gt;package-part.conf&lt;/code&gt; so one piece can be overridden cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Line format
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#Type Name     ID             GECOS                 Home directory Shell
u!    httpd    404            "HTTP User"
u!    _authd   /usr/bin/authd "Authorization user"
u!    postgres -              "Postgresql Database" /var/lib/pgsql /usr/libexec/postgresdb
g     input    -              -
m     _authd   input
u     root     0              "Superuser"           /root          /bin/zsh
r     -        500-900
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty lines and &lt;code&gt;#&lt;/code&gt; comments are ignored. Fields after &lt;code&gt;Name&lt;/code&gt; depend on the type.&lt;/p&gt;

&lt;h3&gt;
  
  
  Types you will actually use
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;u&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a system user &lt;strong&gt;and&lt;/strong&gt; a same-named group if missing. Account is created disabled (invalid password). Default primary group is the matching group unless &lt;code&gt;ID&lt;/code&gt; says otherwise.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;u!&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same as &lt;code&gt;u&lt;/code&gt;, but the account is &lt;strong&gt;fully locked&lt;/strong&gt;. Recommended for daemon users. The lock matters for non-password auth (SSH and similar), not only password logins. Added in systemd &lt;strong&gt;257&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;g&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a system group only. &lt;code&gt;u&lt;/code&gt; already creates a matching group, so use &lt;code&gt;g&lt;/code&gt; for shared groups.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add a user to a group. Missing user or group is created implicitly.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add a numeric UID/GID range to the allocation pool (or a single ID). UIDs and GIDs share the pool so same-named user/group pairs tend to get matching numbers.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Name rules
&lt;/h3&gt;

&lt;p&gt;Strict syntax from the man page and &lt;a href="https://systemd.io/USER_NAMES/" rel="noopener noreferrer"&gt;systemd.io/USER_NAMES&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allowed: &lt;code&gt;a-z&lt;/code&gt;, &lt;code&gt;A-Z&lt;/code&gt;, &lt;code&gt;0-9&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;First character: letter or &lt;code&gt;_&lt;/code&gt; (not a digit, not &lt;code&gt;-&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Length: 1–31 characters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Strong recommendation: prefix system names with &lt;code&gt;_&lt;/code&gt; (for example &lt;code&gt;_myapp&lt;/code&gt;) so they do not collide with admin-created people accounts.&lt;/p&gt;

&lt;h3&gt;
  
  
  ID field
&lt;/h3&gt;

&lt;p&gt;For &lt;code&gt;u&lt;/code&gt; / &lt;code&gt;g&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-&lt;/code&gt; — automatic allocation from the system UID/GID pool (&lt;strong&gt;preferred&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;numeric UID/GID — pin a specific ID when you truly must&lt;/li&gt;
&lt;li&gt;absolute path — take owner/group from an existing file (handy for SUID/SGID binaries already on disk)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uid:gid&lt;/code&gt; or &lt;code&gt;uid:groupname&lt;/code&gt; — set a specific primary group; the group must already exist or be created explicitly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-:groupname&lt;/code&gt; — automatic UID with an explicit primary group&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; use &lt;code&gt;65535&lt;/code&gt; or &lt;code&gt;4294967295&lt;/code&gt;. Both are reserved placeholder values on Linux (&lt;code&gt;(uid_t)-1&lt;/code&gt; in 16-bit and 32-bit eras).&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;m&lt;/code&gt;, the ID field is the &lt;strong&gt;group name&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
For &lt;code&gt;r&lt;/code&gt;, the ID field is &lt;code&gt;FROM-TO&lt;/code&gt; or a single number.&lt;/p&gt;
&lt;h3&gt;
  
  
  GECOS, home, shell
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GECOS&lt;/strong&gt; — short quoted description; no colons. Only for &lt;code&gt;u&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Home&lt;/strong&gt; — defaults to &lt;code&gt;/&lt;/code&gt; if omitted. &lt;code&gt;systemd-sysusers&lt;/code&gt; only writes the passwd home field; it does &lt;strong&gt;not&lt;/strong&gt; create the directory. Pair with a &lt;code&gt;tmpfiles.d&lt;/code&gt; &lt;code&gt;d&lt;/code&gt; line when the path must exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shell&lt;/strong&gt; — defaults to &lt;code&gt;/usr/sbin/nologin&lt;/code&gt; (or &lt;code&gt;/bin/sh&lt;/code&gt; for UID 0). Leave it alone unless software truly needs another shell.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Useful CLI flags
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apply all discovered sysusers.d snippets&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysusers

&lt;span class="c"&gt;# Dry-run: show what would be created (no writes)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysusers &lt;span class="nt"&gt;--dry-run&lt;/span&gt;

&lt;span class="c"&gt;# Inspect merged config&lt;/span&gt;
systemd-sysusers &lt;span class="nt"&gt;--cat-config&lt;/span&gt; | less
systemd-sysusers &lt;span class="nt"&gt;--tldr&lt;/span&gt;

&lt;span class="c"&gt;# One admin file&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysusers /etc/sysusers.d/myapp.conf

&lt;span class="c"&gt;# Inline lines without a file&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysusers &lt;span class="nt"&gt;--inline&lt;/span&gt; &lt;span class="s1"&gt;'u! _demo - "Demo service user"'&lt;/span&gt;

&lt;span class="c"&gt;# Operate on an alternate root or disk image (image builders / offline)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysusers &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/root
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysusers &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/lib/machines/lab.raw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;--dry-run&lt;/code&gt; landed in systemd 250 (&lt;code&gt;-n&lt;/code&gt; short option in 262+). &lt;code&gt;--inline&lt;/code&gt; and &lt;code&gt;--replace=&lt;/code&gt; arrived in 238.&lt;/p&gt;
&lt;h3&gt;
  
  
  Package install pattern: &lt;code&gt;--replace=&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;When a package script runs &lt;strong&gt;before&lt;/strong&gt; its own &lt;code&gt;/usr/lib/sysusers.d/foo.conf&lt;/code&gt; is on disk, feed the lines on stdin but keep admin override priority:&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;echo&lt;/span&gt; &lt;span class="s1"&gt;'u! radvd - "radvd daemon"'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysusers &lt;span class="nt"&gt;--replace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/lib/sysusers.d/radvd.conf -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That behaves as if &lt;code&gt;radvd.conf&lt;/code&gt; were already installed, while still honoring an earlier admin file such as &lt;code&gt;/etc/sysusers.d/radvd.conf&lt;/code&gt; or &lt;code&gt;/etc/sysusers.d/00-overrides.conf&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on lab: a locked service user + shared group + state dir
&lt;/h2&gt;

&lt;p&gt;Assume a small metrics agent that should run as &lt;code&gt;_metrics&lt;/code&gt;, share a &lt;code&gt;metrics-readers&lt;/code&gt; group with a log shipper, and own &lt;code&gt;/var/lib/metrics&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Declare accounts
&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/sysusers.d/metrics.conf &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;'
# Locked daemon user; automatic UID/GID from the system pool
u!  _metrics          -  "Metrics agent"

# Shared group for readers (no login user implied)
g   metrics-readers   -

# Membership
m   _metrics          metrics-readers
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On systemd &lt;strong&gt;older than 257&lt;/strong&gt;, use plain &lt;code&gt;u&lt;/code&gt; instead of &lt;code&gt;u!&lt;/code&gt; and lock separately if your policy requires it. Prefer &lt;code&gt;u!&lt;/code&gt; on current releases.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Declare the state directory (tmpfiles, not sysusers)
&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/tmpfiles.d/metrics.conf &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;'
d  /var/lib/metrics  0750  _metrics  _metrics  -  -
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Apply and verify
&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;# Preview first&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysusers &lt;span class="nt"&gt;--dry-run&lt;/span&gt; /etc/sysusers.d/metrics.conf

&lt;span class="c"&gt;# Create accounts&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysusers /etc/sysusers.d/metrics.conf

&lt;span class="c"&gt;# Create the directory tree&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-tmpfiles &lt;span class="nt"&gt;--create&lt;/span&gt; /etc/tmpfiles.d/metrics.conf

&lt;span class="c"&gt;# Verify&lt;/span&gt;
getent passwd _metrics
getent group _metrics metrics-readers
&lt;span class="nb"&gt;id &lt;/span&gt;_metrics
namei &lt;span class="nt"&gt;-l&lt;/span&gt; /var/lib/metrics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a system UID (typically in the distribution system range, commonly 1–999), shell &lt;code&gt;/usr/sbin/nologin&lt;/code&gt;, and membership in both &lt;code&gt;_metrics&lt;/code&gt; and &lt;code&gt;metrics-readers&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Wire a service
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/metrics-agent.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Example metrics agent&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;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;_metrics&lt;/span&gt;
&lt;span class="py"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;_metrics&lt;/span&gt;
&lt;span class="py"&gt;SupplementaryGroups&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;metrics-readers&lt;/span&gt;
&lt;span class="py"&gt;StateDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;metrics&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/local/bin/metrics-agent --data=%S/metrics&lt;/span&gt;
&lt;span class="c"&gt;# Harden lightly; expand with your real profile
&lt;/span&gt;&lt;span class="py"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;ProtectSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;
&lt;span class="py"&gt;ProtectHome&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;PrivateTmp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;ReadWritePaths&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/lib/metrics&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;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;StateDirectory=metrics&lt;/code&gt; manages &lt;code&gt;/var/lib/metrics&lt;/code&gt; for the unit lifetime under the service UID. Keeping the matching &lt;code&gt;tmpfiles.d&lt;/code&gt; line is still useful for boot-time ownership repair and non-unit tools that expect the path to exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Idempotence check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysusers /etc/sysusers.d/metrics.conf
&lt;span class="c"&gt;# Second run: no changes if the user/group/membership already exist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sysusers.d(5)&lt;/code&gt; is explicit: existing users, groups, and memberships are left alone. That is why re-running from package scripts is safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Cleanup (lab only)
&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;systemctl disable &lt;span class="nt"&gt;--now&lt;/span&gt; metrics-agent.service 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/systemd/system/metrics-agent.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload

&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/sysusers.d/metrics.conf /etc/tmpfiles.d/metrics.conf
&lt;span class="c"&gt;# Accounts are not auto-deleted by removing snippets — remove deliberately if this was only a lab:&lt;/span&gt;
&lt;span class="c"&gt;# sudo userdel _metrics&lt;/span&gt;
&lt;span class="c"&gt;# sudo groupdel metrics-readers&lt;/span&gt;
&lt;span class="c"&gt;# sudo rm -rf /var/lib/metrics&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  First-boot credentials (empty &lt;code&gt;/etc&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;systemd-sysusers&lt;/code&gt; understands service credentials (same family as &lt;code&gt;LoadCredential=&lt;/code&gt; / &lt;code&gt;SetCredential=&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Credential&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;passwd.hashed-password.USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set initial UNIX hash when creating &lt;code&gt;USER&lt;/code&gt; (often &lt;code&gt;root&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;passwd.plaintext-password.USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Plaintext alternative; hashed form wins if both set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;passwd.shell.USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shell to use at creation time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sysusers.extra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extra &lt;code&gt;sysusers.d&lt;/code&gt; lines, processed &lt;strong&gt;after&lt;/strong&gt; on-disk drop-ins&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These only apply when the account is &lt;strong&gt;created&lt;/strong&gt;. They do not rotate passwords on existing users. &lt;code&gt;systemd-sysusers.service&lt;/code&gt; inherits the root password/shell credentials and &lt;code&gt;sysusers.extra&lt;/code&gt; by default, which is why first-boot containers can seed root like this (hash abbreviated for readability):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-nspawn &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;... &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-credential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;passwd.hashed-password.root:&lt;span class="s1"&gt;'$y$j9T$...'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate hashes with &lt;code&gt;mkpasswd(1)&lt;/code&gt;. Prefer hashed credentials over plaintext outside throwaway labs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where UIDs come from
&lt;/h2&gt;

&lt;p&gt;From &lt;a href="https://systemd.io/UIDS-GIDS/" rel="noopener noreferrer"&gt;Users, Groups, UIDs and GIDs on systemd Systems&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt; — root&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1–999&lt;/strong&gt; — system users (default boundary; do not casually move it)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1000+&lt;/strong&gt; — regular/human users (and higher special ranges)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;65534&lt;/strong&gt; — &lt;code&gt;nobody&lt;/code&gt; / overflow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;65535&lt;/strong&gt; and &lt;strong&gt;4294967295&lt;/strong&gt; — unusable placeholders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;61184–65519&lt;/strong&gt; — &lt;code&gt;DynamicUser=&lt;/code&gt; allocations (synthesized via &lt;code&gt;nss-systemd&lt;/code&gt;, not &lt;code&gt;sysusers&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;60001–60513&lt;/strong&gt; — &lt;code&gt;systemd-homed&lt;/code&gt; homes&lt;/li&gt;
&lt;li&gt;Other ranges exist for greeters, nspawn user namespaces, and foreign OS images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;sysusers&lt;/code&gt; allocates named system accounts from the system pool (or your &lt;code&gt;r&lt;/code&gt; ranges). It does not claim the dynamic or homed ranges. Keep automatic &lt;code&gt;-&lt;/code&gt; IDs unless you have a documented reason to pin numbers (NFS export tables, hard-coded third-party packages, and similar).&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundaries: pick the right tool
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Named persistent system daemon user/group&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;systemd-sysusers&lt;/code&gt; / &lt;code&gt;sysusers.d&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create &lt;code&gt;/run&lt;/code&gt; and &lt;code&gt;/var/lib&lt;/code&gt; trees + ownership&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;systemd-tmpfiles&lt;/code&gt; / &lt;code&gt;tmpfiles.d&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-unit ephemeral UID, no long-lived name&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;DynamicUser=yes&lt;/code&gt;&lt;/strong&gt; in the unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human users with portable encrypted homes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;systemd-homed&lt;/code&gt; / &lt;code&gt;homectl&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets for services&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;systemd-creds&lt;/code&gt; / &lt;code&gt;LoadCredentialEncrypted=&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interactive people accounts on a workstation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;useradd&lt;/code&gt; / your IdM — not sysusers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Common anti-pattern: creating a system user with a real login shell and home under &lt;code&gt;/home&lt;/code&gt;. Prefer &lt;code&gt;u!&lt;/code&gt;, &lt;code&gt;/usr/sbin/nologin&lt;/code&gt;, and a data directory under &lt;code&gt;/var/lib&lt;/code&gt; managed by tmpfiles or &lt;code&gt;StateDirectory=&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational tips
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inspect before you write.&lt;/strong&gt; &lt;code&gt;systemd-sysusers --dry-run&lt;/code&gt; and &lt;code&gt;--cat-config&lt;/code&gt; / &lt;code&gt;--tldr&lt;/code&gt; show the merge order and planned accounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship package files in &lt;code&gt;/usr/lib&lt;/code&gt;.&lt;/strong&gt; Keep &lt;code&gt;/etc&lt;/code&gt; for site policy and disables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pair with tmpfiles.&lt;/strong&gt; Sysusers owns the identity; tmpfiles owns the directories that identity needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock daemon accounts.&lt;/strong&gt; Use &lt;code&gt;u!&lt;/code&gt; on systemd 257+ so SSH keys and other non-password methods cannot log in either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stay offline-resolvable.&lt;/strong&gt; System users belong in local files, not remote directories, or early boot and rescue environments break.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image builds.&lt;/strong&gt; Prefer &lt;code&gt;--root=&lt;/code&gt; / &lt;code&gt;--image=&lt;/code&gt; in builders so the guest passwd database is populated offline the same way the host would do at first boot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removing a &lt;code&gt;.conf&lt;/code&gt; does not delete the account.&lt;/strong&gt; That is a feature for running systems; reverse with explicit &lt;code&gt;userdel&lt;/code&gt;/&lt;code&gt;groupdel&lt;/code&gt; only when you mean it.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://man7.org/linux/man-pages/man5/sysusers.d.5.html" rel="noopener noreferrer"&gt;sysusers.d(5)&lt;/a&gt; — file format, types, precedence, idempotence&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man7.org/linux/man-pages/man8/systemd-sysusers.8.html" rel="noopener noreferrer"&gt;systemd-sysusers(8)&lt;/a&gt; — CLI, &lt;code&gt;--replace=&lt;/code&gt;, credentials, image/root modes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://systemd.io/UIDS-GIDS/" rel="noopener noreferrer"&gt;Users, Groups, UIDs and GIDs on systemd Systems&lt;/a&gt; — UID ranges and special IDs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://systemd.io/USER_NAMES/" rel="noopener noreferrer"&gt;User/Group Name Syntax&lt;/a&gt; — strict vs relaxed name rules&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man7.org/linux/man-pages/man5/tmpfiles.d.5.html" rel="noopener noreferrer"&gt;tmpfiles.d(5)&lt;/a&gt; — create the directories sysusers only records&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man7.org/linux/man-pages/man5/systemd.exec.5.html" rel="noopener noreferrer"&gt;systemd.exec(5)&lt;/a&gt; — &lt;code&gt;User=&lt;/code&gt;, &lt;code&gt;DynamicUser=&lt;/code&gt;, &lt;code&gt;StateDirectory=&lt;/code&gt;, credentials&lt;/li&gt;
&lt;li&gt;Debian man pages mirror: &lt;a href="https://manpages.debian.org/testing/systemd/sysusers.d.5.en.html" rel="noopener noreferrer"&gt;sysusers.d(5)&lt;/a&gt;, &lt;a href="https://manpages.debian.org/testing/systemd/systemd-sysusers.8.en.html" rel="noopener noreferrer"&gt;systemd-sysusers(8)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop scattering &lt;code&gt;useradd&lt;/code&gt; one-liners through packaging scripts. Declare the account once, apply it everywhere, and let tmpfiles own the directories that account needs.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Hand-Crafting /run and /tmp Trees: Practical systemd-tmpfiles on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Sat, 03 Oct 2026 05:02:03 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-hand-crafting-run-and-tmp-trees-practical-systemd-tmpfiles-on-linux-3561</link>
      <guid>https://dev.to/lyraalishaikh/stop-hand-crafting-run-and-tmp-trees-practical-systemd-tmpfiles-on-linux-3561</guid>
      <description>&lt;h1&gt;
  
  
  Stop Hand-Crafting /run and /tmp Trees: Practical systemd-tmpfiles on Linux
&lt;/h1&gt;

&lt;p&gt;Most Linux hosts already run &lt;code&gt;systemd-tmpfiles&lt;/code&gt; on every boot. Packages drop snippets under &lt;code&gt;/usr/lib/tmpfiles.d/&lt;/code&gt;, the early boot units create volatile trees under &lt;code&gt;/run&lt;/code&gt; and friends, and a timer later ages junk out of &lt;code&gt;/tmp&lt;/code&gt; and &lt;code&gt;/var/tmp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Operators still keep reinventing the same work with ad-hoc 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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/myapp/cache
&lt;span class="nb"&gt;chown &lt;/span&gt;myapp:myapp /var/lib/myapp/cache
&lt;span class="nb"&gt;chmod &lt;/span&gt;0750 /var/lib/myapp/cache
find /var/lib/myapp/cache &lt;span class="nt"&gt;-mtime&lt;/span&gt; +7 &lt;span class="nt"&gt;-delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works until the next image rebuild, the next package update that races your script, or the next reboot where &lt;code&gt;/run&lt;/code&gt; is empty again and nothing recreates the sockets directory.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tmpfiles.d&lt;/code&gt; is the declarative replacement: one line per path, applied the same way at boot, on package install hooks, and on a periodic clean timer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually runs
&lt;/h2&gt;

&lt;p&gt;Three system units do the work (names from &lt;code&gt;systemd-tmpfiles(8)&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Unit&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemd-tmpfiles-setup-dev.service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Early device-node /dev setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemd-tmpfiles-setup.service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create/adjust paths during boot (&lt;code&gt;--create&lt;/code&gt;, with &lt;code&gt;--boot&lt;/code&gt; lines)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;systemd-tmpfiles-clean.service&lt;/code&gt; + &lt;code&gt;.timer&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Age-based cleanup (&lt;code&gt;--clean&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Typical boot invocation (documented in the same man page):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-tmpfiles &lt;span class="nt"&gt;--remove&lt;/span&gt; &lt;span class="nt"&gt;--create&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful direct calls when you change config:&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;# Apply create/adjust rules now (safe runtime subset)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-tmpfiles &lt;span class="nt"&gt;--create&lt;/span&gt;

&lt;span class="c"&gt;# Run age cleanup now&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-tmpfiles &lt;span class="nt"&gt;--clean&lt;/span&gt;

&lt;span class="c"&gt;# See which snippets would be merged&lt;/span&gt;
systemd-tmpfiles &lt;span class="nt"&gt;--cat-config&lt;/span&gt; | less

&lt;span class="c"&gt;# Apply only one admin file&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-tmpfiles &lt;span class="nt"&gt;--create&lt;/span&gt; /etc/tmpfiles.d/myapp.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After editing drop-ins, restarting &lt;code&gt;systemd-tmpfiles-clean.service&lt;/code&gt; re-applies settings that are safe at runtime. Lines marked with &lt;code&gt;!&lt;/code&gt; only run when &lt;code&gt;--boot&lt;/code&gt; is passed — do not force those on a live multi-user system unless you understand the impact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Config locations and precedence
&lt;/h2&gt;

&lt;p&gt;Search paths (system instance):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;/etc/tmpfiles.d/*.conf&lt;/code&gt; — &lt;strong&gt;admin overrides&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/run/tmpfiles.d/*.conf&lt;/code&gt; — runtime&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/usr/lib/tmpfiles.d/*.conf&lt;/code&gt; — vendor/package (plus &lt;code&gt;/usr/local/lib/tmpfiles.d&lt;/code&gt; on newer releases)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same basename: &lt;code&gt;/etc&lt;/code&gt; wins over &lt;code&gt;/run&lt;/code&gt; wins over &lt;code&gt;/usr/lib&lt;/code&gt;. Files are then sorted &lt;strong&gt;lexicographically by filename&lt;/strong&gt;. If two different files claim the &lt;strong&gt;same path&lt;/strong&gt;, the &lt;strong&gt;lexicographically earliest filename&lt;/strong&gt; wins; later conflicts are logged as errors.&lt;/p&gt;

&lt;p&gt;Disable a vendor file without deleting package content:&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 ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /dev/null /etc/tmpfiles.d/some-vendor.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;User instance (separate from system cleanup) reads &lt;code&gt;~/.config/user-tmpfiles.d/&lt;/code&gt;, &lt;code&gt;$XDG_RUNTIME_DIR/user-tmpfiles.d/&lt;/code&gt;, and share paths under &lt;code&gt;~/.local/share/user-tmpfiles.d/&lt;/code&gt; and &lt;code&gt;/usr/share/user-tmpfiles.d/&lt;/code&gt;. Important nuance from the man page: &lt;strong&gt;system&lt;/strong&gt; age rules for &lt;code&gt;/tmp&lt;/code&gt; still apply to user files placed there, even if the user instance turns cleanup off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Line format
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#Type  Path                 Mode  User  Group  Age  Argument
d      /run/myapp           0755  myapp myapp  -    -
f      /run/myapp/ready     0644  myapp myapp  -    -
L      /run/myapp/log       -     -     -      -    /var/log/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type&lt;/strong&gt; — operation letter, optional modifiers (&lt;code&gt;+&lt;/code&gt;, &lt;code&gt;!&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;~&lt;/code&gt;, &lt;code&gt;^&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path&lt;/strong&gt; — absolute path (specifiers like &lt;code&gt;%h&lt;/code&gt;, &lt;code&gt;%C&lt;/code&gt; allowed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mode&lt;/strong&gt; — e.g. &lt;code&gt;0755&lt;/code&gt;; &lt;code&gt;-&lt;/code&gt; means default (&lt;code&gt;0755&lt;/code&gt; dirs, &lt;code&gt;0644&lt;/code&gt; files) or “don’t change” for &lt;code&gt;z&lt;/code&gt;/&lt;code&gt;Z&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User / Group&lt;/strong&gt; — name or numeric ID; use system accounts resolvable in early boot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Age&lt;/strong&gt; — cleanup threshold (&lt;code&gt;10d&lt;/code&gt;, &lt;code&gt;1h&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;, or &lt;code&gt;-&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Argument&lt;/strong&gt; — content, symlink target, device &lt;code&gt;major:minor&lt;/code&gt;, ACL text, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Types you will actually use
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create and own paths
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Directory; contents eligible for age cleanup if Age is set
d /run/myapp 0750 myapp myapp - -

# Like d, but --remove empties it (boot wipe of leftovers)
D /run/myapp/tmp 0750 myapp myapp 1d -

# Create file only if missing; optional content in Argument
f /var/lib/myapp/state.env 0640 myapp myapp - -

# Always truncate/write content
f+ /run/myapp/banner 0644 root root - Hello from tmpfiles

# Write into an existing file (globs OK); w+ appends
w /sys/module/dummy/parameters/foo - - - - 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leading parents for create types are auto-created as &lt;code&gt;root:root&lt;/code&gt; mode &lt;code&gt;0755&lt;/code&gt;. If you need different ownership on parents, add explicit &lt;code&gt;d&lt;/code&gt; lines first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clean without owning create
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Adjust mode/owner of existing dirs + age their contents (globs OK)
e /var/tmp/myapp 0750 myapp myapp 7d -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;e&lt;/code&gt; does nothing useful unless at least one of mode, user, group, or age is set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Symlinks, pipes, copy-from-factory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;L /etc/os-release - - - - ../usr/lib/os-release
L+ /run/service/current - - - - /opt/service/releases/1.2.3

p /run/myapp/cmd.fifo 0600 myapp myapp - -

# Copy from source if target missing/empty
C /var/lib/myapp/defaults - - - - /usr/share/myapp/factory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;L&lt;/code&gt;/&lt;code&gt;C&lt;/code&gt; with omitted argument pull same-named objects from &lt;code&gt;/usr/share/factory/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleanup exclusions and forced removal
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Keep this tree during age clean of a parent
x /var/tmp/myapp/keep - - - - -

# Remove empty path / recursive tree (usually with ! for boot-only)
r /run/myapp/stale-lock - - - - -
R! /run/myapp/scratch - - - - -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;x&lt;/code&gt; excludes a path (and below) from &lt;strong&gt;age&lt;/strong&gt; cleaning; it does &lt;strong&gt;not&lt;/strong&gt; block &lt;code&gt;r&lt;/code&gt;/&lt;code&gt;R&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix permissions after the fact
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;z /var/lib/myapp/data 0750 myapp myapp - -
Z /var/lib/myapp 0750 myapp myapp - -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;z&lt;/code&gt;/&lt;code&gt;Z&lt;/code&gt; adjust mode, owner, and restore SELinux context when policy is active. They accept globs.&lt;/p&gt;

&lt;h3&gt;
  
  
  ACLs and attributes (when you need them)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a /var/lib/shared 0750 root shared - u:alice:rwx,u:bob:r-x
h /var/log/immutable.log - - - - +i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Type modifiers that prevent outages
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Modifier&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;!&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only with &lt;code&gt;--boot&lt;/code&gt; (unsafe on a live system — e.g. wiping X11 locks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create failure is non-fatal (great for optional sysfs writes in containers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recreate/truncate/replace semantics for &lt;code&gt;f&lt;/code&gt;/&lt;code&gt;w&lt;/code&gt;/&lt;code&gt;L&lt;/code&gt;/&lt;code&gt;p&lt;/code&gt;/&lt;code&gt;c&lt;/code&gt;/&lt;code&gt;b&lt;/code&gt; (and force-copy for &lt;code&gt;C+&lt;/code&gt; on newer systemd)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;If an existing object has the wrong type, remove it first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;~&lt;/code&gt; on type&lt;/td&gt;
&lt;td&gt;Base64-decode Argument before write (&lt;code&gt;f&lt;/code&gt;/&lt;code&gt;w&lt;/code&gt; family)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;^&lt;/code&gt; on type&lt;/td&gt;
&lt;td&gt;Read Argument from a service credential name (&lt;code&gt;tmpfiles.extra&lt;/code&gt; / credentials docs)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Example from the manual — boot-only lock cleanup paired with a normal directory line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;d /tmp/.X11-unix 1777 root root 10d
r! /tmp/.X[0-9]*-lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Container-safe sysctl-style write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;w- /proc/sys/vm/swappiness - - - - 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Age cleanup without foot-guns
&lt;/h2&gt;

&lt;p&gt;Age applies to &lt;code&gt;d&lt;/code&gt;, &lt;code&gt;D&lt;/code&gt;, &lt;code&gt;e&lt;/code&gt;, &lt;code&gt;v&lt;/code&gt;, &lt;code&gt;q&lt;/code&gt;, &lt;code&gt;Q&lt;/code&gt;, &lt;code&gt;C&lt;/code&gt;, &lt;code&gt;x&lt;/code&gt;, and &lt;code&gt;X&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Formats: &lt;code&gt;10d&lt;/code&gt;, &lt;code&gt;12h&lt;/code&gt;, &lt;code&gt;30m&lt;/code&gt;, &lt;code&gt;1w&lt;/code&gt;, or combinations that sum. &lt;code&gt;0&lt;/code&gt; means delete matching contents every &lt;code&gt;--clean&lt;/code&gt; run.&lt;/p&gt;

&lt;p&gt;Default “is this still young?” check uses mtime, atime, and (for non-directories) ctime. Directory &lt;strong&gt;ctime&lt;/strong&gt; is ignored by default so the cleaner does not keep resurrecting directories it just touched.&lt;/p&gt;

&lt;p&gt;Narrow the clocks with &lt;code&gt;age-by&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;# Files by mtime/birth; directories by atime; threshold 1 hour
d /tmp/foo/bar - - - bmA:1h -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefix Age with &lt;code&gt;~&lt;/code&gt; to clean only &lt;strong&gt;one level down&lt;/strong&gt;, not the directory’s immediate children naming semantics described in &lt;code&gt;tmpfiles.d(5)&lt;/code&gt; — useful when the top-level entries must stay.&lt;/p&gt;

&lt;p&gt;Applications can take a shared BSD &lt;code&gt;flock&lt;/code&gt; on a directory to pause aging for that subtree while a job runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab: app runtime dir + aged cache
&lt;/h2&gt;

&lt;p&gt;Assume a system user &lt;code&gt;myapp&lt;/code&gt; already exists (create with &lt;code&gt;useradd --system&lt;/code&gt; / &lt;code&gt;systemd-sysusers&lt;/code&gt; as you prefer).&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/tmpfiles.d/myapp.conf &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;'
# Runtime (tmpfs /run) — recreated every boot
d /run/myapp           0750 myapp myapp - -
d /run/myapp/sockets   0750 myapp myapp - -
f /run/myapp/README    0640 myapp myapp - managed-by-tmpfiles

# On-disk cache with 7-day age cleanup of contents
d /var/cache/myapp     0750 myapp myapp 7d -

# Keep one subdirectory forever while parent ages
x /var/cache/myapp/pinned - - - - -

# Permission repair if something else chowns the tree
Z /var/cache/myapp 0750 myapp myapp - -
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-tmpfiles &lt;span class="nt"&gt;--create&lt;/span&gt; /etc/tmpfiles.d/myapp.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;namei &lt;span class="nt"&gt;-l&lt;/span&gt; /run/myapp/sockets
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /run/myapp /var/cache/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simulate cache files and clean:&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;-u&lt;/span&gt; myapp &lt;span class="nb"&gt;touch&lt;/span&gt; /var/cache/myapp/old.bin /var/cache/myapp/pinned/keep.bin
&lt;span class="c"&gt;# After aging window, or with Age 0 in a test copy of the snippet:&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-tmpfiles &lt;span class="nt"&gt;--clean&lt;/span&gt; /etc/tmpfiles.d/myapp.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a force-clean lab without waiting, use a separate test snippet with &lt;code&gt;e /tmp/myapp-cache-test 0755 myapp myapp 0 -&lt;/code&gt;, create files, then &lt;code&gt;--clean&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer unit directories when the lifetime matches the service
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;tmpfiles.d&lt;/code&gt; is for paths whose lifetime is &lt;strong&gt;independent&lt;/strong&gt; of one unit, or that need richer rules (globs, ACLs, factory copy, boot-only removes).&lt;/p&gt;

&lt;p&gt;For a single service, prefer &lt;code&gt;systemd.exec(5)&lt;/code&gt; directives so the directory dies with the stop:&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;# /etc/systemd/system/myapp.service
&lt;/span&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="py"&gt;RuntimeDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="py"&gt;RuntimeDirectoryMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0750&lt;/span&gt;
&lt;span class="py"&gt;StateDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="py"&gt;CacheDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="py"&gt;LogsDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&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/local/bin/myapp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That yields &lt;code&gt;/run/myapp&lt;/code&gt;, &lt;code&gt;/var/lib/myapp&lt;/code&gt;, &lt;code&gt;/var/cache/myapp&lt;/code&gt;, &lt;code&gt;/var/log/myapp&lt;/code&gt; with matching ownership, without a parallel tmpfiles snippet.&lt;/p&gt;

&lt;p&gt;Rule of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service-private&lt;/strong&gt; runtime/state/cache/log → &lt;code&gt;RuntimeDirectory=&lt;/code&gt; / &lt;code&gt;StateDirectory=&lt;/code&gt; / …&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared&lt;/strong&gt;, multi-service, boot-seeded, aged, or factory-copied paths → &lt;code&gt;tmpfiles.d&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets&lt;/strong&gt; → &lt;code&gt;LoadCredential=&lt;/code&gt; / &lt;code&gt;systemd-creds&lt;/code&gt;, not world-readable tmpfiles content&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Image builds and credentials
&lt;/h2&gt;

&lt;p&gt;Offline OS trees:&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-tmpfiles &lt;span class="nt"&gt;--root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/rootfs &lt;span class="nt"&gt;--create&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt;
&lt;span class="c"&gt;# -E skips /dev /proc /run /sys so you do not populate overmount points&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disk images: &lt;code&gt;--image=PATH&lt;/code&gt; (implies &lt;code&gt;-E&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;First-boot extras can arrive via the &lt;code&gt;tmpfiles.extra&lt;/code&gt; credential: extra tmpfiles lines processed after on-disk drop-ins (cannot override earlier same-path winners). Combine with &lt;code&gt;^&lt;/code&gt; type modifier to pull file bodies from named credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Put admin rules in &lt;code&gt;/etc/tmpfiles.d/name.conf&lt;/code&gt; — never edit vendor files in &lt;code&gt;/usr/lib&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;systemd-tmpfiles --create&lt;/code&gt; after edits; use &lt;code&gt;--boot&lt;/code&gt; only in early-boot context.&lt;/li&gt;
&lt;li&gt;Confirm the clean timer: &lt;code&gt;systemctl status systemd-tmpfiles-clean.timer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;x&lt;/code&gt;/&lt;code&gt;X&lt;/code&gt; before aggressive parent &lt;code&gt;Age=&lt;/code&gt; values.&lt;/li&gt;
&lt;li&gt;Mark destructive lines with &lt;code&gt;!&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keep User=/Group= resolvable without LDAP at early boot.&lt;/li&gt;
&lt;li&gt;Mask bad vendor snippets with &lt;code&gt;/etc/tmpfiles.d/foo.conf -&amp;gt; /dev/null&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Do not store secrets in &lt;code&gt;f&lt;/code&gt;/&lt;code&gt;w&lt;/code&gt; arguments; use credentials.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Boundaries (what this article is not)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Not &lt;code&gt;systemd-sysusers&lt;/code&gt; (account creation) or &lt;code&gt;systemd-firstboot&lt;/code&gt; (machine identity seeding)&lt;/li&gt;
&lt;li&gt;Not &lt;code&gt;RuntimeDirectory=&lt;/code&gt; deep-dive beyond the boundary above&lt;/li&gt;
&lt;li&gt;Not &lt;code&gt;systemd-creds&lt;/code&gt; secret injection (covered separately)&lt;/li&gt;
&lt;li&gt;Not Btrfs quota design beyond noting &lt;code&gt;v&lt;/code&gt;/&lt;code&gt;q&lt;/code&gt;/&lt;code&gt;Q&lt;/code&gt; exist for subvolumes&lt;/li&gt;
&lt;li&gt;Not a replacement for backups — age cleanup &lt;strong&gt;deletes&lt;/strong&gt; data on purpose&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/trixie/systemd/tmpfiles.d.5.en.html" rel="noopener noreferrer"&gt;tmpfiles.d(5) — Debian manpages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/bookworm/systemd/systemd-tmpfiles.8.en.html" rel="noopener noreferrer"&gt;systemd-tmpfiles(8) — Debian manpages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/trixie/systemd/systemd.exec.5.en.html" rel="noopener noreferrer"&gt;systemd.exec(5) — RuntimeDirectory= and friends&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Upstream overview: systemd project documentation for tmpfiles.d / systemd-tmpfiles&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Ship the snippet next to the unit, not a shell &lt;code&gt;mkdir&lt;/code&gt; in &lt;code&gt;ExecStartPre=&lt;/code&gt;. Your boot path stays boring — which is exactly what you want from &lt;code&gt;/run&lt;/code&gt; and &lt;code&gt;/tmp&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Typing LUKS Passphrases at Every Boot: Practical systemd-cryptenroll on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Fri, 02 Oct 2026 05:02:21 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-typing-luks-passphrases-at-every-boot-practical-systemd-cryptenroll-on-linux-49nm</link>
      <guid>https://dev.to/lyraalishaikh/stop-typing-luks-passphrases-at-every-boot-practical-systemd-cryptenroll-on-linux-49nm</guid>
      <description>&lt;p&gt;Full-disk encryption that still stops for a passphrase at every reboot is only half done. On a laptop that is fine. On a homelab node, a fleet box, or anything you expect to come back after power loss, interactive unlock becomes the outage.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemd-cryptenroll&lt;/code&gt; is the operator tool for binding &lt;strong&gt;LUKS2&lt;/strong&gt; volumes to hardware factors systemd already understands at boot: TPM2 chips, FIDO2 tokens (hmac-secret), PKCS#11 smartcards, plus computer-generated recovery keys. Unlock metadata lives in the LUKS2 JSON token area. At boot, &lt;code&gt;systemd-cryptsetup&lt;/code&gt; + &lt;code&gt;/etc/crypttab&lt;/code&gt; consume it.&lt;/p&gt;

&lt;p&gt;This guide is operational. Commands and semantics come from &lt;code&gt;systemd-cryptenroll(1)&lt;/code&gt;, &lt;code&gt;crypttab(5)&lt;/code&gt;, and &lt;code&gt;systemd-cryptsetup@.service(8)&lt;/code&gt; (Arch man pages / upstream systemd docs, covering features through systemd 262). Examples assume a LUKS2 volume you already unlock with a passphrase today. Do not wipe that passphrase until recovery and hardware unlock are proven.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is (and is not)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;systemd-cryptenroll&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enroll TPM2 / FIDO2 / PKCS#11 / passphrase / recovery into &lt;strong&gt;LUKS2&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;crypttab + systemd-cryptsetup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unlock at boot from those enrollments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Clevis + Tang&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Network-Bound Disk Encryption (NBDE) — unlock when a Tang server is reachable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;systemd-creds&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-&lt;strong&gt;service&lt;/strong&gt; secrets at activation, not disk unlock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;sbctl / Secure Boot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Boot chain PE signing — complementary, not a LUKS unlocker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cryptsetup luksAddKey&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Passphrase/keyfile slots only — no TPM2/FIDO2 token JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;cryptenroll supports &lt;strong&gt;LUKS2 only&lt;/strong&gt;. Older LUKS1 volumes need a format upgrade first (&lt;code&gt;cryptsetup convert --type luks2&lt;/code&gt;, after backups).&lt;/p&gt;

&lt;p&gt;Supported enrollment kinds (from the man page):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PKCS#11 tokens (RSA or EC key pair)&lt;/li&gt;
&lt;li&gt;FIDO2 tokens with the &lt;code&gt;hmac-secret&lt;/code&gt; extension&lt;/li&gt;
&lt;li&gt;TPM2 security devices&lt;/li&gt;
&lt;li&gt;Regular passphrases&lt;/li&gt;
&lt;li&gt;Recovery keys (machine-generated, high entropy, QR-friendly)&lt;/li&gt;
&lt;/ol&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Tooling&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; systemd-cryptenroll
systemd-cryptenroll &lt;span class="nt"&gt;--version&lt;/span&gt;   &lt;span class="c"&gt;# features below note added-in versions&lt;/span&gt;

&lt;span class="c"&gt;# You need an existing LUKS2 volume and a working unlock factor today&lt;/span&gt;
&lt;span class="c"&gt;# (usually a passphrase). Root (or equivalent) required.&lt;/span&gt;

&lt;span class="c"&gt;# Discover candidate LUKS devices (257+)&lt;/span&gt;
systemd-cryptenroll &lt;span class="nt"&gt;--list-devices&lt;/span&gt;

&lt;span class="c"&gt;# TPM present?&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; /dev/tpmrm0 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; /dev/tpm0 2&amp;gt;/dev/null
systemd-cryptenroll &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;list

&lt;span class="c"&gt;# FIDO2 present?&lt;/span&gt;
systemd-cryptenroll &lt;span class="nt"&gt;--fido2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep a second session or live USB path open before you wipe passphrase slots. A bad wipe without recovery is a brick for the volume key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mental model
&lt;/h2&gt;

&lt;p&gt;Enrollment does &lt;strong&gt;not&lt;/strong&gt; store your LUKS master key in the clear on the token.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TPM2:&lt;/strong&gt; a random unlock key is sealed to the TPM (primary key derived from the TPM seed / SRK). Sealed blob + policy metadata land in the LUKS2 JSON token header. Unseal succeeds only when the TPM can reproduce the policy (PCRs, optional PIN, signed PCR policy, pcrlock, …).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FIDO2:&lt;/strong&gt; a random salt/secret is HMAC'd on the token with a secret that never leaves the authenticator; the HMAC output unlocks the volume. Presence (tap), client PIN, and user verification are configurable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery key:&lt;/strong&gt; like a passphrase, but generated for entropy and typeability; intended as the “token lost / TPM state drifted” escape hatch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Default device when you omit &lt;code&gt;DEVICE&lt;/code&gt; and are not wiping: the block device backing &lt;code&gt;/var/&lt;/code&gt; (which is usually the root FS device when &lt;code&gt;/var&lt;/code&gt; is not separate). Prefer naming the device explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 0 — Safety first: recovery key
&lt;/h2&gt;

&lt;p&gt;Before any TPM or FIDO experiment, enroll a recovery key and store it offline (password manager, printed QR, sealed envelope — not next to the machine).&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;# Replace with your LUKS partition (by path or /dev/disk/by-uuid/…)&lt;/span&gt;
&lt;span class="nv"&gt;DEV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/disk/by-uuid/YOUR-LUKS-UUID

&lt;span class="c"&gt;# Prompts for an existing passphrase to authorize the new slot&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-cryptenroll &lt;span class="nt"&gt;--recovery-key&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;systemd-cryptenroll&lt;/code&gt; prints the recovery key and can show a QR code. Treat it like a root password with higher entropy.&lt;/p&gt;

&lt;p&gt;List what is enrolled (via cryptsetup token listing):&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;cryptsetup luksDump &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'/Tokens/,/Keyslots/p'&lt;/span&gt;
&lt;span class="c"&gt;# or full dump:&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup luksDump &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 1 — TPM2 automatic unlock (no PCR bind)
&lt;/h2&gt;

&lt;p&gt;Simplest useful path for a trusted physical host: seal to the TPM with &lt;strong&gt;no PCR policy&lt;/strong&gt;. The volume unlocks whenever this TPM is present. Stolen disk alone is not enough; stolen disk &lt;strong&gt;plus&lt;/strong&gt; the same machine’s TPM (or a cloned TPM state, which you should assume is hard on discrete/fTPM designs) is the threat you are accepting.&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;# List TPM devices&lt;/span&gt;
systemd-cryptenroll &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;list

&lt;span class="c"&gt;# Enroll; unlocks with your existing passphrase unless you pass --unlock-*&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-cryptenroll &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Empty string / omitted --tpm2-pcrs= means: bind to no PCRs (man page default)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire crypttab so boot uses the TPM:&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;# /etc/crypttab — third field "none" when token metadata is in the LUKS header&lt;/span&gt;
&lt;span class="c"&gt;# volume-name  encrypted-device                         key-file  options&lt;/span&gt;
rootfs &lt;span class="nv"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR-LUKS-UUID none tpm2-device&lt;span class="o"&gt;=&lt;/span&gt;auto,x-initrd.attach
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes from &lt;code&gt;crypttab(5)&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tpm2-device=auto&lt;/code&gt; discovers the chip; or pin &lt;code&gt;/dev/tpmrm0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x-initrd.attach&lt;/code&gt; is recommended for root (and other initrd-attached crypt devices) so detach ordering during shutdown is correct&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;headless=true&lt;/code&gt; refuses interactive password fallback — only enable after TPM path is proven&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;token-timeout=&lt;/code&gt; (default 30s) waits for tokens before falling back to password&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rebuild initramfs so early unlock sees the TPM stack (distro-specific):&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;# Debian/Ubuntu&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;update-initramfs &lt;span class="nt"&gt;-u&lt;/span&gt;

&lt;span class="c"&gt;# Fedora&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dracut &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="c"&gt;# Arch&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mkinitcpio &lt;span class="nt"&gt;-P&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test without rebooting when the volume is not your live root (extra data disk):&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 start systemd-cryptsetup@datadisk.service
&lt;span class="c"&gt;# or:&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/lib/systemd/systemd-cryptsetup attach datadisk &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; - tpm2-device&lt;span class="o"&gt;=&lt;/span&gt;auto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;strong&gt;root&lt;/strong&gt;, plan a controlled reboot and keep recovery key + passphrase slots until you have one clean boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 2 — Bind to Secure Boot / UKI-friendly PCRs
&lt;/h2&gt;

&lt;p&gt;PCR binding refuses unseal if measured boot state drifts. The man page’s practical guidance:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In general, encrypted volumes would be bound to some combination of PCRs &lt;strong&gt;7, 11, and 14&lt;/strong&gt; (if shim/MOK is used). Prefer certificate-backed measurements over raw firmware code PCRs (0/2), which change on every firmware update.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PCR&lt;/th&gt;
&lt;th&gt;Name (systemd)&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;secure-boot-policy&lt;/td&gt;
&lt;td&gt;Secure Boot on/off and db/KEK/PK material&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;kernel-boot&lt;/td&gt;
&lt;td&gt;systemd-stub UKI measurements; pcrphase milestones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;shim-policy&lt;/td&gt;
&lt;td&gt;MOK certificates/hashes when shim is in the path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;boot-loader-code&lt;/td&gt;
&lt;td&gt;Boot loader PE — changes on bootloader updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;kernel-initrd&lt;/td&gt;
&lt;td&gt;Kernel-measured initrds (Linux 5.17+ LOAD_FILE2 path)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example: Secure Boot policy + UKI/kernel-boot measurements&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-cryptenroll &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tpm2-pcrs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7+11 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Named form also works:&lt;/span&gt;
&lt;span class="c"&gt;# --tpm2-pcrs=secure-boot-policy+kernel-boot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Operational cost:&lt;/strong&gt; kernel/UKI/Secure Boot changes that alter those PCRs break unlock until you re-enroll or fall back to recovery/passphrase. That is the point — and why a recovery key is mandatory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signed PCR policies (update-friendly)
&lt;/h3&gt;

&lt;p&gt;Instead of sealing to &lt;strong&gt;today’s&lt;/strong&gt; PCR digests, bind to a &lt;strong&gt;public key&lt;/strong&gt; that signs allowed PCR states (&lt;code&gt;--tpm2-public-key=&lt;/code&gt;, &lt;code&gt;--tpm2-public-key-pcrs=&lt;/code&gt;, optional &lt;code&gt;--tpm2-signature=&lt;/code&gt;). Vendors or your own UKI pipeline (see &lt;code&gt;systemd-measure&lt;/code&gt;) can ship new signatures when kernels change without re-sealing every disk. Default public-key PCR set is &lt;strong&gt;11&lt;/strong&gt; when unset. systemd 252+.&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;# Sketch — paths follow man-page search order if omitted:&lt;/span&gt;
&lt;span class="c"&gt;# /etc/systemd/tpm2-pcr-public-key.pem and tpm2-pcr-signature.json&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-cryptenroll &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tpm2-public-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/etc/systemd/tpm2-pcr-public-key.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tpm2-public-key-pcrs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;11 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tpm2-signature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/etc/systemd/tpm2-pcr-signature.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a signature file is provided, cryptenroll verifies it unlocks the &lt;strong&gt;current&lt;/strong&gt; PCR state before writing the slot — a safety net against locking yourself out mid-enrollment.&lt;/p&gt;

&lt;p&gt;systemd 262 adds &lt;code&gt;--tpm2-public-key-policyref=&lt;/code&gt; so one signing key can scope slots to a boot phase (for example initrd-only) when &lt;code&gt;systemd-measure&lt;/code&gt; used a matching &lt;code&gt;--policyref=&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 3 — TPM2 + PIN (second factor)
&lt;/h2&gt;

&lt;p&gt;A bare TPM unseal is “something you have” (the machine). Add a PIN so a cold-booted chassis still needs knowledge:&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;# systemd 262+: --tpm2-with-pin=yes hardens the PIN with Argon2id before the TPM&lt;/span&gt;
&lt;span class="c"&gt;# (compromised TPM alone should not yield the volume key without the PIN)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-cryptenroll &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tpm2-pcrs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7+11 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tpm2-with-pin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Older compatibility path: --tpm2-with-pin=direct (PBKDF2, systemd 253+ unlockers)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rootfs UUID=… none tpm2-device=auto,tpm2-pin=yes,x-initrd.attach
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dictionary lockout:&lt;/strong&gt; wrong PINs hit the TPM’s global DA lockout. systemd does not own that policy — use &lt;code&gt;tpm2_getcap&lt;/code&gt; / &lt;code&gt;tpm2_dictionarylockout&lt;/code&gt; from tpm2-tools if you need to inspect or tune it. Keep recovery keys offline.&lt;/p&gt;

&lt;p&gt;Optional Argon2id knobs (262+): &lt;code&gt;--tpm2-argon2id-memory=&lt;/code&gt;, &lt;code&gt;--tpm2-argon2id-iterations=&lt;/code&gt;, &lt;code&gt;--tpm2-argon2id-parallelism=&lt;/code&gt;, &lt;code&gt;--tpm2-argon2id-iter-time=&lt;/code&gt; (default target ~2s).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 4 — FIDO2 token (YubiKey and friends)
&lt;/h2&gt;

&lt;p&gt;Prefer FIDO2 over PKCS#11 on tokens that speak both — simpler enrollment, modern defaults.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-cryptenroll &lt;span class="nt"&gt;--fido2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;list

&lt;span class="c"&gt;# Defaults: clientPin yes, user presence (tap) yes, user verification no&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-cryptenroll &lt;span class="nt"&gt;--fido2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Explicit policy example:&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-cryptenroll &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fido2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fido2-with-client-pin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fido2-with-user-presence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fido2-with-user-verification&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rootfs UUID=… none fido2-device=auto,x-initrd.attach
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Credential algorithm default is &lt;code&gt;es256&lt;/code&gt;; &lt;code&gt;rs256&lt;/code&gt; and &lt;code&gt;eddsa&lt;/code&gt; are available via &lt;code&gt;--fido2-credential-algorithm=&lt;/code&gt; (251+). systemd 257 adds &lt;code&gt;--fido2-salt-file=&lt;/code&gt; and &lt;code&gt;--fido2-parameters-in-header=&lt;/code&gt; (header storage is the default and what &lt;code&gt;auto&lt;/code&gt; unlock expects).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-token caveat:&lt;/strong&gt; several FIDO2 enrollments work, but tokens with user verification may force sequential PIN/UV prompts because pre-flight identity is limited. PKCS#11 does not have that limitation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 5 — Rotate and wipe without bricking
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--wipe-slot=&lt;/code&gt; accepts numeric indexes or type names: &lt;code&gt;password&lt;/code&gt;, &lt;code&gt;recovery&lt;/code&gt;, &lt;code&gt;tpm2&lt;/code&gt;, &lt;code&gt;fido2&lt;/code&gt;, &lt;code&gt;pkcs11&lt;/code&gt;, &lt;code&gt;empty&lt;/code&gt;, &lt;code&gt;all&lt;/code&gt; (with safety refusal of wiping &lt;em&gt;everything&lt;/em&gt; with no remaining unlock path in some combinations — still: never wipe recovery until the new factor boots cleanly).&lt;/p&gt;

&lt;p&gt;Re-enroll TPM and drop old TPM slots in one shot:&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-cryptenroll &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--wipe-slot&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tpm2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tpm2-pcrs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7+11 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--unlock-tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enrollment runs first; the new slot is excluded from the wipe. Other patterns from the man page:&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;# Empty password → TPM (image factory pattern)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-cryptenroll &lt;span class="nt"&gt;--wipe-slot&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# PKCS#11 → FIDO2&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-cryptenroll &lt;span class="nt"&gt;--wipe-slot&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pkcs11 &lt;span class="nt"&gt;--fido2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlock helpers when stdin passphrase is inconvenient (versions in man page):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--unlock-key-file=PATH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Key file instead of typed passphrase (252+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--unlock-fido2-device=auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unlock with existing FIDO2 to enroll something else (253+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--unlock-tpm2-device=auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unlock with existing TPM2 (256+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--unlock-empty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Empty passphrase volumes (262+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--unlock-headless&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Try TPM2 then empty — provisioning automation (262+)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Lab 6 — Loop-device dry run (no production disk)
&lt;/h2&gt;

&lt;p&gt;When you want to practice without touching 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;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;IMG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/cryptenroll-lab.img
&lt;span class="nv"&gt;MNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/cryptenroll-mnt
&lt;span class="nb"&gt;sudo truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 256M &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;LOOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;losetup &lt;span class="nt"&gt;--find&lt;/span&gt; &lt;span class="nt"&gt;--show&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup luksFormat &lt;span class="nt"&gt;--type&lt;/span&gt; luks2 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOOP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup open &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOOP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; cryptenroll-lab
&lt;span class="nb"&gt;sudo &lt;/span&gt;mkfs.ext4 /dev/mapper/cryptenroll-lab
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MNT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount /dev/mapper/cryptenroll-lab &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MNT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo &lt;/span&gt;ok | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MNT&lt;/span&gt;&lt;span class="s2"&gt;/proof"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null

&lt;span class="c"&gt;# Enroll recovery + TPM (if present) while passphrase still works&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-cryptenroll &lt;span class="nt"&gt;--recovery-key&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOOP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;systemd-cryptenroll &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;list 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; .&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-cryptenroll &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOOP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup luksDump &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOOP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Cleanup&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;umount &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MNT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup close cryptenroll-lab
&lt;span class="nb"&gt;sudo &lt;/span&gt;losetup &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOOP&lt;/span&gt;&lt;span class="s2"&gt;"&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;$IMG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On hosts without a TPM, skip the TPM block and practice FIDO2 with a real key, or recovery + password only.&lt;/p&gt;

&lt;h2&gt;
  
  
  crypttab options worth knowing
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;crypttab(5)&lt;/code&gt;, focused on token unlock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# TPM2
name UUID=… none tpm2-device=auto,tpm2-pin=yes,x-initrd.attach

# FIDO2
name UUID=… none fido2-device=auto,x-initrd.attach

# PKCS#11
name UUID=… none pkcs11-uri=auto,x-initrd.attach

# Headless server: no password prompt (only after hardware path is proven)
name UUID=… none tpm2-device=auto,headless=true,x-initrd.attach
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Related knobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tpm2-pcrs=&lt;/code&gt; — manual PCR list when enrollment metadata is not in the header (unusual if you used cryptenroll defaults)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tpm2-signature=&lt;/code&gt; / &lt;code&gt;tpm2-pcrlock=&lt;/code&gt; — signed policy and pcrlock policy paths (auto-search under &lt;code&gt;/etc/systemd&lt;/code&gt;, &lt;code&gt;/run/systemd&lt;/code&gt;, …)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tpm2-measure-pcr=yes&lt;/code&gt; — measure the activated volume key into PCR 15 (253+), useful so later TPM objects can bind to “this install’s root volume key”&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;token-timeout=30s&lt;/code&gt; — how long to wait for hardware before password fallback&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_netdev&lt;/code&gt; — for network-backed keying (NBDE/Clevis territory); not required for local TPM/FIDO&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Initrd checklist (root volume)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Enroll recovery key; store offline
&lt;/li&gt;
&lt;li&gt;Enroll TPM2 and/or FIDO2; leave passphrase slot intact
&lt;/li&gt;
&lt;li&gt;Update &lt;code&gt;/etc/crypttab&lt;/code&gt; with &lt;code&gt;tpm2-device=auto&lt;/code&gt; and &lt;code&gt;x-initrd.attach&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ensure initramfs includes systemd cryptsetup TPM/FIDO hooks (distro packages vary: &lt;code&gt;libtss2&lt;/code&gt;, &lt;code&gt;libfido2&lt;/code&gt;, cryptsetup token plugins)
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;update-initramfs&lt;/code&gt; / &lt;code&gt;dracut&lt;/code&gt; / &lt;code&gt;mkinitcpio&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reboot once; confirm unlock path in journal: &lt;code&gt;journalctl -b -u systemd-cryptsetup@*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Only then consider &lt;code&gt;--wipe-slot=password&lt;/code&gt; if policy requires it — many operators keep a long passphrase slot forever
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Failure modes and recovery
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Move&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Drops to passphrase after TPM enroll&lt;/td&gt;
&lt;td&gt;crypttab missing &lt;code&gt;tpm2-device=&lt;/code&gt;, initrd missing TSS stack, or wrong device&lt;/td&gt;
&lt;td&gt;Fix crypttab; rebuild initrd; check &lt;code&gt;tpm2-device=list&lt;/code&gt; in initrd rescue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works until kernel/Secure Boot change&lt;/td&gt;
&lt;td&gt;PCR bind (7/11/…) changed&lt;/td&gt;
&lt;td&gt;Unlock with recovery; re-enroll TPM with same or signed policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPM PIN lockout&lt;/td&gt;
&lt;td&gt;DA counter&lt;/td&gt;
&lt;td&gt;Wait out lockout or reset per site TPM policy; use recovery key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FIDO prompts repeatedly&lt;/td&gt;
&lt;td&gt;Multiple UV tokens&lt;/td&gt;
&lt;td&gt;Unplug extras; prefer single UV token or PKCS#11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cannot enroll from PKCS#11-only volume&lt;/td&gt;
&lt;td&gt;Limitation: PKCS#11 cannot authorize new enrollments&lt;/td&gt;
&lt;td&gt;Always keep passphrase, recovery, FIDO2, or TPM2 for roll-over&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compatibility guarantee from the man page: &lt;strong&gt;old enrollments unlock on newer cryptsetup&lt;/strong&gt;; the reverse is not guaranteed. Match &lt;code&gt;systemd-cryptenroll&lt;/code&gt; and &lt;code&gt;systemd-cryptsetup&lt;/code&gt; versions when possible; re-enroll after major upgrades if you want new hardening (Argon2id PIN, policyref, …).&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundary: when to use Clevis/Tang instead
&lt;/h2&gt;

&lt;p&gt;Use &lt;strong&gt;Clevis + Tang&lt;/strong&gt; when unlock should depend on &lt;strong&gt;network presence&lt;/strong&gt; (machine in the right building/VLAN), not on the local TPM. Use &lt;strong&gt;cryptenroll TPM2&lt;/strong&gt; when the trust anchor is the &lt;strong&gt;device&lt;/strong&gt; (and optional PCRs/PIN). They can coexist as separate LUKS slots; they solve different threat models. Neither replaces off-host backups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick reference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Discover&lt;/span&gt;
systemd-cryptenroll &lt;span class="nt"&gt;--list-devices&lt;/span&gt;          &lt;span class="c"&gt;# 257+&lt;/span&gt;
systemd-cryptenroll &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;list
systemd-cryptenroll &lt;span class="nt"&gt;--fido2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;list

&lt;span class="c"&gt;# Enroll&lt;/span&gt;
systemd-cryptenroll &lt;span class="nt"&gt;--recovery-key&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
systemd-cryptenroll &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="nt"&gt;--tpm2-pcrs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7+11 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
systemd-cryptenroll &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="nt"&gt;--tpm2-with-pin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# 262+ Argon2id&lt;/span&gt;
systemd-cryptenroll &lt;span class="nt"&gt;--fido2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
systemd-cryptenroll &lt;span class="nt"&gt;--password&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Rotate TPM slots&lt;/span&gt;
systemd-cryptenroll &lt;span class="nt"&gt;--wipe-slot&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tpm2 &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--unlock-tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# crypttab core&lt;/span&gt;
&lt;span class="c"&gt;# name UUID=… none tpm2-device=auto,x-initrd.attach&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/systemd-cryptenroll.1.en" rel="noopener noreferrer"&gt;systemd-cryptenroll(1)&lt;/a&gt; — enrollment, PCR table, wipe-slot, unlock helpers
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/crypttab.5.en" rel="noopener noreferrer"&gt;crypttab(5)&lt;/a&gt; — &lt;code&gt;tpm2-device=&lt;/code&gt;, &lt;code&gt;fido2-device=&lt;/code&gt;, &lt;code&gt;pkcs11-uri=&lt;/code&gt;, measure/timeout/headless options
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/systemd-cryptsetup@.service.8.en" rel="noopener noreferrer"&gt;systemd-cryptsetup@.service(8)&lt;/a&gt; — activation unit
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://uapi-group.org/specifications/specs/linux_tpm_pcr_registry/" rel="noopener noreferrer"&gt;UAPI.7 Linux TPM PCR Registry&lt;/a&gt; — authoritative PCR meanings
&lt;/li&gt;
&lt;li&gt;Upstream man source: &lt;a href="https://github.com/systemd/systemd/blob/main/man/systemd-cryptenroll.xml" rel="noopener noreferrer"&gt;systemd/systemd &lt;code&gt;man/systemd-cryptenroll.xml&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Related: &lt;code&gt;systemd-measure(1)&lt;/code&gt;, &lt;code&gt;systemd-pcrlock(8)&lt;/code&gt;, &lt;code&gt;systemd-tpm2-setup.service(8)&lt;/code&gt;, &lt;code&gt;cryptsetup(8)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Passphrase-at-boot encryption is better than nothing. LUKS2 plus &lt;code&gt;systemd-cryptenroll&lt;/code&gt; is how you keep the confidentiality without making every power cycle a human dependency — as long as recovery keys are real, PCR choices match your update story, and you wipe old slots only after a boot you have watched succeed.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Putting Secrets in Environment Variables: Practical systemd-creds on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Wed, 30 Sep 2026 05:02:13 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-putting-secrets-in-environment-variables-practical-systemd-creds-on-linux-4gfi</link>
      <guid>https://dev.to/lyraalishaikh/stop-putting-secrets-in-environment-variables-practical-systemd-creds-on-linux-4gfi</guid>
      <description>&lt;p&gt;Passwords in &lt;code&gt;Environment=&lt;/code&gt; lines, API tokens in world-readable unit drop-ins, and &lt;code&gt;EnvironmentFile=/etc/myapp.env&lt;/code&gt; that every process on the host can &lt;code&gt;cat&lt;/code&gt; once it gets a shell — that pattern still shows up in homelab and production units alike.&lt;/p&gt;

&lt;p&gt;systemd has a better primitive: &lt;strong&gt;credentials&lt;/strong&gt;. They are limited-size binary or text blobs that the service manager loads at activation time, decrypts if needed, places in a private directory, and exposes to the service via &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt;. The operator tool is &lt;code&gt;systemd-creds&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This guide is operational. Commands and behavior come from &lt;code&gt;systemd-creds(1)&lt;/code&gt;, the &lt;strong&gt;Credentials&lt;/strong&gt; section of &lt;code&gt;systemd.exec(5)&lt;/code&gt;, and the upstream &lt;a href="https://systemd.io/CREDENTIALS/" rel="noopener noreferrer"&gt;System and Service Credentials&lt;/a&gt; document. Examples target systemd 250+ (encryption tooling); user-scoped encrypt/decrypt needs 256+. The host used for command verification here runs systemd 257.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use an env var?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Environment=SECRET=…&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unit files are world-readable on disk and via D-Bus; children inherit the env&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EnvironmentFile=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same visibility problem; easy to leave mode &lt;code&gt;0644&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plain file under &lt;code&gt;/etc&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Readable by any process that can open it; not scoped to the unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SOPS/age in Git&lt;/td&gt;
&lt;td&gt;Great for repo secrets; still need a &lt;strong&gt;runtime&lt;/strong&gt; hand-off into the service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clevis/Tang / LUKS&lt;/td&gt;
&lt;td&gt;Disk unlock, not per-service secret injection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Credentials fix the runtime side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Acquired at activation, released on deactivation, immutable while the service runs&lt;/li&gt;
&lt;li&gt;Files under &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt;, mode-restricted to the service UID (and root)&lt;/li&gt;
&lt;li&gt;Optionally AES-256-GCM encrypted/authenticated at rest (TPM2 and/or host secret)&lt;/li&gt;
&lt;li&gt;Preferable to env for secrets; binary-safe; ~1 MB accumulated size limit per unit&lt;/li&gt;
&lt;li&gt;Work cleanly with &lt;code&gt;RootDirectory=&lt;/code&gt; / &lt;code&gt;RootImage=&lt;/code&gt; / portable services (host path not required inside the image)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&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;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; systemd-creds
systemd-creds &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# encrypt/decrypt/list/cat/setup: systemd 250+&lt;/span&gt;
&lt;span class="c"&gt;# --user / --uid= encrypt: 256+&lt;/span&gt;

&lt;span class="c"&gt;# Optional: see whether TPM2 is usable for credential protection&lt;/span&gt;
systemd-creds has-tpm2 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need root (or equivalent) for host-key setup and for system-unit examples. Transient labs use &lt;code&gt;systemd-run&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five unit settings (and when to use each)
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;systemd.exec(5)&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Sensitive?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LoadCredential=ID[:PATH]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;File, AF_UNIX socket, or relative search path&lt;/td&gt;
&lt;td&gt;Path should be protected; data stays plaintext on disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LoadCredentialEncrypted=ID[:PATH]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Encrypted credential file/socket&lt;/td&gt;
&lt;td&gt;Yes — decrypt at activation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SetCredential=ID:VALUE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Literal in the unit file&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; for secrets (public keys, IDs only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SetCredentialEncrypted=ID:VALUE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Encrypted blob literal in the unit&lt;/td&gt;
&lt;td&gt;Yes — safe to paste ciphertext into drop-ins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ImportCredential=GLOB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Propagate system credentials (or search stores) by name/glob&lt;/td&gt;
&lt;td&gt;Depends on how the system got them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Search paths for relative sources (also used by &lt;code&gt;ImportCredential=&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plain: &lt;code&gt;/etc/credstore/&lt;/code&gt;, &lt;code&gt;/run/credstore/&lt;/code&gt;, &lt;code&gt;/usr/lib/credstore/&lt;/code&gt; (plus credentials already passed into the system)&lt;/li&gt;
&lt;li&gt;Encrypted: &lt;code&gt;/etc/credstore.encrypted/&lt;/code&gt;, &lt;code&gt;/run/credstore.encrypted/&lt;/code&gt;, &lt;code&gt;/usr/lib/credstore.encrypted/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Per-user manager: &lt;code&gt;$XDG_CONFIG_HOME/credstore/&lt;/code&gt;, &lt;code&gt;$XDG_RUNTIME_DIR/credstore/&lt;/code&gt;, &lt;code&gt;$HOME/.local/lib/credstore/&lt;/code&gt; (and &lt;code&gt;.encrypted&lt;/code&gt; counterparts)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Inside the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$CREDENTIALS_DIRECTORY/&amp;lt;ID&amp;gt;   # primary interface
%d/&amp;lt;ID&amp;gt;                       # unit-file specifier for Environment=
${CREDENTIALS_DIRECTORY}/&amp;lt;ID&amp;gt; # ExecStart= interpolation
/run/credentials/&amp;lt;unit&amp;gt;       # system services only — prefer $CREDENTIALS_DIRECTORY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 1 — Plain LoadCredential with a transient unit
&lt;/h2&gt;

&lt;p&gt;Prove the plumbing before encryption:&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;# As root&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'hello-from-disk'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /root/demo-plain.txt
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /root/demo-plain.txt

systemd-run &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;LoadCredential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo:/root/demo-plain.txt &lt;span class="se"&gt;\&lt;/span&gt;
  systemd-creds &lt;span class="nb"&gt;cat &lt;/span&gt;demo
&lt;span class="c"&gt;# → hello-from-disk&lt;/span&gt;

systemd-run &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;LoadCredential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo:/root/demo-plain.txt &lt;span class="se"&gt;\&lt;/span&gt;
  systemd-creds list
&lt;span class="c"&gt;# Shows name, size, and security state (secure/weak/insecure)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;systemd-creds list&lt;/code&gt; and &lt;code&gt;cat&lt;/code&gt; read &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt; in the &lt;strong&gt;current&lt;/strong&gt; execution context — that is why the lab runs them &lt;em&gt;inside&lt;/em&gt; &lt;code&gt;systemd-run&lt;/code&gt; with &lt;code&gt;LoadCredential=&lt;/code&gt; attached.&lt;/p&gt;

&lt;p&gt;Reference the path from a shell-style &lt;code&gt;ExecStart&lt;/code&gt; without putting the secret in the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-run &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;LoadCredential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo:/root/demo-plain.txt &lt;span class="se"&gt;\&lt;/span&gt;
  bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'wc -c &amp;lt; "$CREDENTIALS_DIRECTORY/demo"; sha256sum "$CREDENTIALS_DIRECTORY/demo"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 2 — Encrypt with the host key
&lt;/h2&gt;

&lt;p&gt;Default &lt;code&gt;encrypt&lt;/code&gt; picks a key mode automatically: TPM2 when present and not in a container, host secret when &lt;code&gt;/var/lib/systemd/&lt;/code&gt; is persistent media, and &lt;strong&gt;both&lt;/strong&gt; when both apply (&lt;code&gt;host+tpm2&lt;/code&gt;). On VMs or hosts without a usable TPM, force the host key:&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;# Ensures /var/lib/systemd/credential.secret exists (also done implicitly on encrypt)&lt;/span&gt;
systemd-creds setup

&lt;span class="c"&gt;# Encrypt stdin → file. Output basename becomes the embedded credential name.&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s3cr3t-db-password'&lt;/span&gt; | systemd-creds encrypt &lt;span class="nt"&gt;--with-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host - /etc/credstore.encrypted/db-password.cred
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /etc/credstore.encrypted/db-password.cred
&lt;span class="c"&gt;# Directory may need creating first:&lt;/span&gt;
&lt;span class="c"&gt;# mkdir -p /etc/credstore.encrypted &amp;amp;&amp;amp; chmod 700 /etc/credstore.encrypted&lt;/span&gt;

&lt;span class="c"&gt;# Round-trip check (name must match the embedded name = filename by default)&lt;/span&gt;
systemd-creds decrypt /etc/credstore.encrypted/db-password.cred
&lt;span class="c"&gt;# → s3cr3t-db-password&lt;/span&gt;

&lt;span class="c"&gt;# Wipe any leftover plaintext you used during creation&lt;/span&gt;
&lt;span class="nb"&gt;shred&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; /root/demo-plain.txt 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key modes from &lt;code&gt;systemd-creds(1)&lt;/code&gt; (&lt;code&gt;--with-key=&lt;/code&gt; / &lt;code&gt;-H&lt;/code&gt; / &lt;code&gt;-T&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Binds decryption to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;host&lt;/code&gt; (&lt;code&gt;-H&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/var/lib/systemd/credential.secret&lt;/code&gt; (root-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tpm2&lt;/code&gt; (&lt;code&gt;-T&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Local TPM2 only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;host+tpm2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Both (typical default when both available)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default selection logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auto-initrd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TPM2 if present, else null-style fallback — for initrd/generator use where &lt;code&gt;/var&lt;/code&gt; is not mounted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fixed empty key — &lt;strong&gt;no&lt;/strong&gt; confidentiality/authenticity; restricted acceptance on Secure Boot + TPM systems&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Encryption algorithm: AES-256-GCM, keyed from a SHA-256 derivation of the selected secret(s). Ciphertext is Base64.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initrd / early-boot note:&lt;/strong&gt; if a credential must decrypt before &lt;code&gt;/var&lt;/code&gt; is available, do &lt;strong&gt;not&lt;/strong&gt; bind it to the host secret. Use &lt;code&gt;--with-key=tpm2&lt;/code&gt; or &lt;code&gt;auto-initrd&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 3 — LoadCredentialEncrypted in a real unit drop-in
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example consumer: a oneshot that only prints that the secret length is correct&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/etc/systemd/system/cred-demo.service &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;'
[Unit]
Description=Credential demo service

[Service]
Type=oneshot
# Relative path → searched under credstore.encrypted (and friends)
LoadCredentialEncrypted=db-password
ExecStart=/usr/bin/bash -c 'test -n "&lt;/span&gt;&lt;span class="nv"&gt;$CREDENTIALS_DIRECTORY&lt;/span&gt;&lt;span class="sh"&gt;"; install -m 0400 "&lt;/span&gt;&lt;span class="nv"&gt;$CREDENTIALS_DIRECTORY&lt;/span&gt;&lt;span class="sh"&gt;/db-password" /run/cred-demo-check; wc -c /run/cred-demo-check; rm -f /run/cred-demo-check'
# Optional hardening that also implies PrivateMounts= on many distros:
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl daemon-reload
systemctl start cred-demo.service
systemctl status cred-demo.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; cred-demo.service &lt;span class="nt"&gt;-n&lt;/span&gt; 20 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Explicit path form (same effect):&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;LoadCredentialEncrypted&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;db-password:/etc/credstore.encrypted/db-password.cred&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Failed decrypt/authenticate for &lt;code&gt;LoadCredentialEncrypted=&lt;/code&gt; / &lt;code&gt;SetCredentialEncrypted=&lt;/code&gt; &lt;strong&gt;fails the unit&lt;/strong&gt;. Failed decrypt for something only pulled via &lt;code&gt;ImportCredential=&lt;/code&gt; is skipped with a warning (credential absent).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 4 — Paste ciphertext into the unit with SetCredentialEncrypted
&lt;/h2&gt;

&lt;p&gt;When you want the secret &lt;strong&gt;in&lt;/strong&gt; the drop-in (no separate &lt;code&gt;.cred&lt;/code&gt; file), generate a pasteable line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-ask-password &lt;span class="nt"&gt;-n&lt;/span&gt; | systemd-creds encrypt &lt;span class="nt"&gt;--with-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysql-password &lt;span class="nt"&gt;-p&lt;/span&gt; - -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-p&lt;/code&gt; / &lt;code&gt;--pretty&lt;/code&gt; prints a &lt;code&gt;SetCredentialEncrypted=mysql-password: \&lt;/code&gt; block. Pipe it into a drop-in:&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; /etc/systemd/system/myapp.service.d
systemd-ask-password &lt;span class="nt"&gt;-n&lt;/span&gt; | &lt;span class="o"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'[Service]'&lt;/span&gt;
  systemd-creds encrypt &lt;span class="nt"&gt;--with-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysql-password &lt;span class="nt"&gt;-p&lt;/span&gt; - -
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/etc/systemd/system/myapp.service.d/50-password.conf
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /etc/systemd/system/myapp.service.d/50-password.conf
systemctl daemon-reload
systemctl restart myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unit file remains world-readable in the usual case; the &lt;strong&gt;ciphertext&lt;/strong&gt; is not useful without the host secret and/or TPM. That is the point of &lt;code&gt;SetCredentialEncrypted=&lt;/code&gt; versus plaintext &lt;code&gt;SetCredential=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Name embedding: the credential name is stored inside the encrypted blob. Renaming &lt;code&gt;mysql-password.cred&lt;/code&gt; to &lt;code&gt;other.cred&lt;/code&gt; makes decrypt fail unless you override with &lt;code&gt;--name=&lt;/code&gt;. Empty &lt;code&gt;--name=&lt;/code&gt; disables the check (rarely what you want).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 5 — Wire apps that do not know $CREDENTIALS_DIRECTORY yet
&lt;/h2&gt;

&lt;p&gt;Many daemons want a path on the CLI or a single env var pointing at a file — not an env var containing the secret itself.&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;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="py"&gt;LoadCredentialEncrypted&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;api-token&lt;/span&gt;
&lt;span class="c"&gt;# Path-only env (the secret stays in the file; children do not inherit the bytes as env)
&lt;/span&gt;&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;API_TOKEN_FILE=%d/api-token&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/local/bin/myapp --token-file ${CREDENTIALS_DIRECTORY}/api-token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;%d&lt;/code&gt; expands to the credentials directory in unit settings; &lt;code&gt;${CREDENTIALS_DIRECTORY}&lt;/code&gt; works in &lt;code&gt;Exec*=&lt;/code&gt; command lines. Prefer these over copying secrets into &lt;code&gt;Environment=SECRET=…&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For software that insists on reading &lt;code&gt;/run/credentials/myapp.service/api-token&lt;/code&gt;, that path works for &lt;strong&gt;system&lt;/strong&gt; units, but &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt; is the portable interface (also correct for &lt;code&gt;systemd --user&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 6 — Propagate system credentials with ImportCredential
&lt;/h2&gt;

&lt;p&gt;Container managers, hypervisors, and cloud IMDS can pass credentials into PID 1. Enumerate them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-creds &lt;span class="nt"&gt;--system&lt;/span&gt; list
systemd-creds &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nb"&gt;cat &lt;/span&gt;some-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples of ingress (from the credentials documentation):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemd-nspawn --set-credential=name:value&lt;/code&gt; / &lt;code&gt;--load-credential=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;QEMU SMBIOS type 11: &lt;code&gt;io.systemd.credential:name=value&lt;/code&gt; or &lt;code&gt;io.systemd.credential.binary:name=&amp;lt;base64&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Kernel cmdline: &lt;code&gt;systemd.set_credential=&lt;/code&gt; / &lt;code&gt;systemd.set_credential_binary=&lt;/code&gt; (visible via &lt;code&gt;/proc/cmdline&lt;/code&gt; — &lt;strong&gt;not&lt;/strong&gt; for secrets)&lt;/li&gt;
&lt;li&gt;ESP credentials via &lt;code&gt;systemd-stub&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/run/credentials/@initrd/&lt;/code&gt; import on initrd → host transition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consume in a service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;ImportCredential&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;mycred&lt;/span&gt;
&lt;span class="c"&gt;# or rename:
# ImportCredential=my.original.cred:my.renamed.cred
# or glob:
# ImportCredential=app.*
&lt;/span&gt;&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/systemd-creds cat mycred&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well-known system credentials (provisioning, not day-2 app secrets) include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;passwd.hashed-password.&amp;lt;user&amp;gt;&lt;/code&gt;, &lt;code&gt;passwd.plaintext-password.&amp;lt;user&amp;gt;&lt;/code&gt;, &lt;code&gt;passwd.shell.&amp;lt;user&amp;gt;&lt;/code&gt; → &lt;code&gt;systemd-sysusers&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;firstboot.locale&lt;/code&gt;, &lt;code&gt;firstboot.keymap&lt;/code&gt;, &lt;code&gt;firstboot.timezone&lt;/code&gt;, … → &lt;code&gt;systemd-firstboot&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tmpfiles.extra&lt;/code&gt; → tmpfiles&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vmm.notify_socket&lt;/code&gt; → PID 1 READY notification (e.g. &lt;code&gt;vsock:CID:PORT&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ssh.authorized_keys.root&lt;/code&gt; (IMDS import path)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See &lt;code&gt;systemd.system-credentials(7)&lt;/code&gt; for the full inventory. Conditional start: &lt;code&gt;ConditionCredential=&lt;/code&gt; / &lt;code&gt;AssertCredential=&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  User services (systemd 256+)
&lt;/h2&gt;

&lt;p&gt;Encrypt for the per-user manager so the key material incorporates UID, username, and &lt;code&gt;machine-id&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;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'user-secret'&lt;/span&gt; | systemd-creds encrypt &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;--uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;self - ~/credstore.encrypted/user-secret.cred
&lt;span class="c"&gt;# Load from a user unit with LoadCredentialEncrypted=user-secret&lt;/span&gt;
&lt;span class="c"&gt;# and store under ~/.config/credstore.encrypted/ or the paths listed above&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Operational checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create&lt;/strong&gt; ciphertext with &lt;code&gt;systemd-creds encrypt&lt;/code&gt; (&lt;code&gt;--with-key=host&lt;/code&gt; in labs without TPM; default &lt;code&gt;auto&lt;/code&gt; on real hardware).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store&lt;/strong&gt; under &lt;code&gt;/etc/credstore.encrypted/&lt;/code&gt; (mode &lt;code&gt;0700&lt;/code&gt; dir, &lt;code&gt;0600&lt;/code&gt; files) or embed with &lt;code&gt;-p&lt;/code&gt; → &lt;code&gt;SetCredentialEncrypted=&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declare&lt;/strong&gt; &lt;code&gt;LoadCredentialEncrypted=&lt;/code&gt; / &lt;code&gt;ImportCredential=&lt;/code&gt; on the unit; avoid plaintext &lt;code&gt;Environment=&lt;/code&gt; for secrets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consume&lt;/strong&gt; via &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt; or &lt;code&gt;%d&lt;/code&gt; path references.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sandbox&lt;/strong&gt; with &lt;code&gt;ProtectSystem=&lt;/code&gt;, &lt;code&gt;PrivateTmp=&lt;/code&gt;, &lt;code&gt;ProtectHome=&lt;/code&gt;, or at least &lt;code&gt;PrivateMounts=&lt;/code&gt; so other units cannot see the credentials mount.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate&lt;/strong&gt; by re-encrypting and restarting the unit; ciphertext is immutable for the life of an activation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backup&lt;/strong&gt; &lt;code&gt;/var/lib/systemd/credential.secret&lt;/code&gt; with the same care as disk-encryption keys if you rely on host binding — losing it bricks host-bound credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiry&lt;/strong&gt; (optional): &lt;code&gt;systemd-creds encrypt --not-after=…&lt;/code&gt; embeds a hard fail-after timestamp checked at decrypt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Priority when the same ID is configured multiple ways (from &lt;code&gt;systemd.exec(5)&lt;/code&gt;): &lt;code&gt;LoadCredential*&lt;/code&gt; / &lt;code&gt;ImportCredential=&lt;/code&gt; win over &lt;code&gt;SetCredential*&lt;/code&gt;. &lt;code&gt;SetCredential=&lt;/code&gt; is a useful &lt;strong&gt;default&lt;/strong&gt; that is overridden when a real credential appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundaries (what this article is not)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; SOPS + age in Git (repo encryption / GitOps). Credentials are the &lt;strong&gt;node-local runtime&lt;/strong&gt; injection layer after deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; Clevis + Tang NBDE or &lt;code&gt;systemd-cryptenroll&lt;/code&gt; (volume unlock at boot).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; &lt;code&gt;systemd-homed&lt;/code&gt; per-user home encryption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; a full replacement for a corporate secret manager — but an excellent native fit for machine-local service secrets on systemd hosts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; &lt;code&gt;varlinkctl&lt;/code&gt; / D-Bus service APIs, linger/user managers, or image tooling from recent posts — only the credential object model and &lt;code&gt;systemd-creds&lt;/code&gt; CLI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl stop cred-demo.service 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/systemd/system/cred-demo.service
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/systemd/system/myapp.service.d/50-password.conf
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/credstore.encrypted/db-password.cred
systemctl daemon-reload
&lt;span class="c"&gt;# Keep /var/lib/systemd/credential.secret unless you intend to invalidate host-bound creds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/testing/systemd/systemd-creds.1.en.html" rel="noopener noreferrer"&gt;systemd-creds(1)&lt;/a&gt; — list, cat, setup, encrypt, decrypt, key modes, examples&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/testing/systemd/systemd.exec.5.en.html" rel="noopener noreferrer"&gt;systemd.exec(5) — Credentials&lt;/a&gt; — &lt;code&gt;LoadCredential*&lt;/code&gt;, &lt;code&gt;SetCredential*&lt;/code&gt;, &lt;code&gt;ImportCredential=&lt;/code&gt;, &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt;, size limit&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://systemd.io/CREDENTIALS/" rel="noopener noreferrer"&gt;System and Service Credentials&lt;/a&gt; — design goals, encryption, ingress paths, well-known credentials, search paths&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/testing/systemd/systemd.system-credentials.7.en.html" rel="noopener noreferrer"&gt;systemd.system-credentials(7)&lt;/a&gt; — well-known system credential names&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/testing/systemd-container/systemd-nspawn.1.en.html" rel="noopener noreferrer"&gt;systemd-nspawn(1)&lt;/a&gt; — &lt;code&gt;--set-credential=&lt;/code&gt; / &lt;code&gt;--load-credential=&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop putting long-lived secrets in &lt;code&gt;Environment=&lt;/code&gt; and world-readable env files. Encrypt once with &lt;code&gt;systemd-creds&lt;/code&gt;, load with &lt;code&gt;LoadCredentialEncrypted=&lt;/code&gt; or &lt;code&gt;SetCredentialEncrypted=&lt;/code&gt;, and let the service read a file from &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt; like any other path-based secret API — with activation-time decrypt and per-unit isolation included.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Guessing systemd Service APIs: Practical varlinkctl on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Tue, 29 Sep 2026 05:02:08 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-guessing-systemd-service-apis-practical-varlinkctl-on-linux-59n</link>
      <guid>https://dev.to/lyraalishaikh/stop-guessing-systemd-service-apis-practical-varlinkctl-on-linux-59n</guid>
      <description>&lt;p&gt;You need one clean answer from a local service: resolve a name, describe the host, extend a PCR, or talk to a custom tool that already speaks JSON on stdin/stdout. The usual options are brittle: scrape &lt;code&gt;systemctl&lt;/code&gt; text, write a one-off D-Bus client, or invent yet another Unix-socket protocol.&lt;/p&gt;

&lt;p&gt;Modern systemd components expose &lt;strong&gt;Varlink&lt;/strong&gt; interfaces instead. &lt;code&gt;varlinkctl&lt;/code&gt; is the operator-facing client for those interfaces: discover what a service implements, print the interface IDL, call methods with JSON arguments, stream multi-reply methods, tunnel calls over SSH, and even wrap a sandboxed command as a Varlink service.&lt;/p&gt;

&lt;p&gt;This guide is operational. Commands and behavior come from &lt;code&gt;varlinkctl(1)&lt;/code&gt;, the UAPI.20 Varlink IPC specification, and the Debian/unstable systemd man pages for the services used in the labs (notably &lt;code&gt;systemd-resolved&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What Varlink is (and is not)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Varlink&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JSON method calls over a stream (typically &lt;code&gt;AF_UNIX&lt;/code&gt;), with a typed interface definition language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UAPI.20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The UAPI Group spec consolidating protocol + transport bindings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;varlinkctl&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CLI to introspect and invoke Varlink services (systemd 255+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;busctl&lt;/code&gt; / D-Bus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Parallel IPC stack; still widely used, different wire format and tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;systemctl&lt;/code&gt; text&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Human UI — fine for shells, awkward as a stable machine API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Varlink messages are plain JSON objects. On stream transports each message is NUL-terminated. Services describe themselves at runtime, so clients can list interfaces and fetch the IDL without a separate schema package.&lt;/p&gt;

&lt;p&gt;You do &lt;strong&gt;not&lt;/strong&gt; need to replace every D-Bus workflow. Use &lt;code&gt;varlinkctl&lt;/code&gt; when the component already speaks Varlink (resolved, hostnamed, many systemd helpers) or when you want a JSON-friendly, socket-activated service of your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&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;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; varlinkctl
varlinkctl &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# Added in systemd 255; several commands below need 257–262 features.&lt;/span&gt;

&lt;span class="c"&gt;# Labs that call resolved need the stub/service running:&lt;/span&gt;
systemctl is-active systemd-resolved.service
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /run/systemd/resolve/io.systemd.Resolve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Privileged examples (PCR extend, some hostnamed paths) need root or an authorized policy. DNS resolve calls as a normal user usually work when resolved is active and the socket is accessible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Address forms you will actually use
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;varlinkctl&lt;/code&gt; accepts several service address syntaxes (from &lt;code&gt;varlinkctl(1)&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;unix:/run/path.sock&lt;/code&gt; or &lt;code&gt;/run/path.sock&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Connect to an &lt;code&gt;AF_UNIX&lt;/code&gt; stream socket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@name&lt;/code&gt; after &lt;code&gt;unix:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Abstract-namespace socket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exec:/usr/lib/systemd/tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fork the binary and speak Varlink on the passed socket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssh-unix:host:/run/...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenSSH ≥ 9.4 path to a remote &lt;code&gt;AF_UNIX&lt;/code&gt; socket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssh-exec:host:cmdline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run a remote command and speak Varlink on its stdio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relative &lt;code&gt;./socket&lt;/code&gt; or &lt;code&gt;./binary&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Local path forms (must start with &lt;code&gt;/&lt;/code&gt; or &lt;code&gt;./&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Convenience: a bare absolute socket path or executable path is enough when the target is local.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 1 — Inventory a live service (resolved)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# General metadata&lt;/span&gt;
varlinkctl info /run/systemd/resolve/io.systemd.Resolve

&lt;span class="c"&gt;# Interface names only&lt;/span&gt;
varlinkctl list-interfaces /run/systemd/resolve/io.systemd.Resolve

&lt;span class="c"&gt;# Method names (list-methods added in 257)&lt;/span&gt;
varlinkctl list-methods /run/systemd/resolve/io.systemd.Resolve

&lt;span class="c"&gt;# Full IDL for the primary interface&lt;/span&gt;
varlinkctl introspect /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical &lt;code&gt;info&lt;/code&gt; fields include vendor, product, version, URL, and the interface list (&lt;code&gt;io.systemd&lt;/code&gt;, &lt;code&gt;io.systemd.Resolve&lt;/code&gt;, &lt;code&gt;org.varlink.service&lt;/code&gt;, …).&lt;/p&gt;

&lt;p&gt;Pretty JSON for scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;varlinkctl info /run/systemd/resolve/io.systemd.Resolve &lt;span class="nt"&gt;-j&lt;/span&gt;
varlinkctl list-methods /run/systemd/resolve/io.systemd.Resolve &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-j&lt;/code&gt; is “pretty when interactive, short when piped”; &lt;code&gt;--json=pretty|short&lt;/code&gt; forces the mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 2 — Call a method with JSON arguments
&lt;/h2&gt;

&lt;p&gt;Resolve a hostname through resolved’s &lt;code&gt;ResolveHostname&lt;/code&gt; method (example shape from &lt;code&gt;varlinkctl(1)&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;varlinkctl call &lt;span class="se"&gt;\&lt;/span&gt;
  /run/systemd/resolve/io.systemd.Resolve &lt;span class="se"&gt;\&lt;/span&gt;
  io.systemd.Resolve.ResolveHostname &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"name":"systemd.io","family":2}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-j&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes that prevent foot-guns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Method names are &lt;strong&gt;fully qualified&lt;/strong&gt;: &lt;code&gt;interface.Method&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Parameters are a &lt;strong&gt;JSON object&lt;/strong&gt;. Use &lt;code&gt;{}&lt;/code&gt; for empty input.&lt;/li&gt;
&lt;li&gt;If you omit the arguments parameter, &lt;code&gt;varlinkctl&lt;/code&gt; reads JSON from STDIN.&lt;/li&gt;
&lt;li&gt;Replies are JSON objects on STDOUT — pipe to &lt;code&gt;jq&lt;/code&gt; when you want fields only.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;varlinkctl call &lt;span class="se"&gt;\&lt;/span&gt;
  /run/systemd/resolve/io.systemd.Resolve &lt;span class="se"&gt;\&lt;/span&gt;
  io.systemd.Resolve.ResolveHostname &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"name":"systemd.io","family":2}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short &lt;span class="se"&gt;\&lt;/span&gt;
| jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.addresses[]? | "\(.family) \(.address)"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;family: 2&lt;/code&gt; is &lt;code&gt;AF_INET&lt;/code&gt; in the usual Linux numbering; adjust if you want IPv6 (&lt;code&gt;10&lt;/code&gt; / &lt;code&gt;AF_INET6&lt;/code&gt;) or leave the field out when the interface allows defaults (check the IDL from &lt;code&gt;introspect&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 3 — Multi-reply methods, collection, and oneway
&lt;/h2&gt;

&lt;p&gt;Some methods stream updates or enumerate objects. Flags from &lt;code&gt;varlinkctl(1)&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;# Expect a sequence of replies (JSON-SEQ). Default call timeout is still 45s.&lt;/span&gt;
varlinkctl call &lt;span class="nt"&gt;--more&lt;/span&gt; ADDRESS INTERFACE.Method &lt;span class="s1"&gt;'{"...":"..."}'&lt;/span&gt;

&lt;span class="c"&gt;# Same idea, but keep listening (timeout disabled) — shortcut -E&lt;/span&gt;
varlinkctl call &lt;span class="nt"&gt;-E&lt;/span&gt; ADDRESS INTERFACE.Method &lt;span class="s1"&gt;'{}'&lt;/span&gt;

&lt;span class="c"&gt;# Gather every reply into one JSON array&lt;/span&gt;
varlinkctl call &lt;span class="nt"&gt;--collect&lt;/span&gt; ADDRESS INTERFACE.Method &lt;span class="s1"&gt;'{}'&lt;/span&gt;

&lt;span class="c"&gt;# Fire-and-forget (no reply expected)&lt;/span&gt;
varlinkctl call &lt;span class="nt"&gt;--oneway&lt;/span&gt; ADDRESS INTERFACE.Method &lt;span class="s1"&gt;'{"...":"..."}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Timeout control:&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;# Default 45s; disable for long subscriptions&lt;/span&gt;
varlinkctl call &lt;span class="nt"&gt;--more&lt;/span&gt; &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;infinity ADDRESS INTERFACE.Method &lt;span class="s1"&gt;'{}'&lt;/span&gt;

&lt;span class="c"&gt;# Treat a specific Varlink error as success (257+)&lt;/span&gt;
varlinkctl call &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--graceful&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;org.varlink.service.InvalidParameter &lt;span class="se"&gt;\&lt;/span&gt;
  ADDRESS INTERFACE.Method &lt;span class="s1"&gt;'{"experimental":true}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;--more&lt;/code&gt; with a sane timeout for finite enumerations; use &lt;code&gt;-E&lt;/code&gt; / &lt;code&gt;--timeout=infinity&lt;/code&gt; only for true subscriptions so a stuck peer cannot hang a cron job forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 4 — Exec targets and helper binaries
&lt;/h2&gt;

&lt;p&gt;Not every Varlink peer is a long-running daemon socket. Some tools speak Varlink when executed. The man page demonstrates &lt;code&gt;systemd-pcrextend&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;# Inspect the executable as a Varlink service (requires privileges for real PCR ops)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;varlinkctl info /usr/lib/systemd/systemd-pcrextend
&lt;span class="nb"&gt;sudo &lt;/span&gt;varlinkctl introspect /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend

&lt;span class="c"&gt;# Example method shape from the man page — only on systems where PCR extend is appropriate&lt;/span&gt;
&lt;span class="c"&gt;# sudo varlinkctl call /usr/lib/systemd/systemd-pcrextend \&lt;/span&gt;
&lt;span class="c"&gt;#   io.systemd.PCRExtend.Extend '{"pcr":15,"text":"foobar"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;exec:&lt;/code&gt; form is equivalent when you want to be explicit:&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;varlinkctl info &lt;span class="nb"&gt;exec&lt;/span&gt;:/usr/lib/systemd/systemd-pcrextend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treat PCR/measurement labs as &lt;strong&gt;maintenance-window&lt;/strong&gt; work on hosts where measured boot policy is intentional. Do not extend PCRs on production machines as a casual test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 5 — Remote calls over SSH
&lt;/h2&gt;

&lt;p&gt;When the remote side has OpenSSH 9.4+ (for &lt;code&gt;ssh-unix:&lt;/code&gt;) and the socket path exists:&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;# Talk to hostnamed on a remote machine via its AF_UNIX socket&lt;/span&gt;
&lt;span class="c"&gt;# varlinkctl call ssh-unix:somehost:/run/systemd/io.systemd.Hostname \&lt;/span&gt;
&lt;span class="c"&gt;#   io.systemd.Hostname.Describe '{}' -j&lt;/span&gt;

&lt;span class="c"&gt;# Or run a Varlink-capable binary on the remote stdio path&lt;/span&gt;
&lt;span class="c"&gt;# varlinkctl call ssh-exec:somehost:systemd-creds \&lt;/span&gt;
&lt;span class="c"&gt;#   org.varlink.service.GetInfo '{}' -j&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for fleet introspection without installing a custom agent: SSH provides transport and auth; Varlink provides a typed method call. Abstract-namespace sockets are &lt;strong&gt;not&lt;/strong&gt; supported over &lt;code&gt;ssh-unix:&lt;/code&gt; (filesystem path sockets only).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 6 — Registry and socket discovery (260+)
&lt;/h2&gt;

&lt;p&gt;Newer systemd builds keep well-known entrypoints under &lt;code&gt;/run/varlink/registry/&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;# System registry (default)&lt;/span&gt;
varlinkctl list-registry

&lt;span class="c"&gt;# Per-user registry when applicable&lt;/span&gt;
varlinkctl list-registry &lt;span class="nt"&gt;--user&lt;/span&gt;

&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /run/varlink/registry/ 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;list-sockets&lt;/code&gt; (262+) enumerates listening &lt;code&gt;AF_UNIX&lt;/code&gt; stream sockets marked as Varlink entrypoints via the &lt;code&gt;user.varlink=entrypoint&lt;/code&gt; xattr (needs kernel support for xattrs on socket inodes; documented as Linux 7.0+ in the man page). Prefer &lt;code&gt;list-registry&lt;/code&gt; on typical current distro kernels if &lt;code&gt;list-sockets&lt;/code&gt; is unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 7 — Serve a sandboxed stdio tool as Varlink (261+)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;varlinkctl serve&lt;/code&gt; turns a command that speaks a protocol on stdio into a socket-activated Varlink service. On upgrade, the client’s connection is handed to the command. The man page’s decompressor example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;/etc/systemd/system/varlink-decompress-xz.socket&lt;/code&gt;&lt;/strong&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;[Socket]&lt;/span&gt;
&lt;span class="py"&gt;ListenStream&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/run/varlink/registry/com.example.Decompress.XZ&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;sockets.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;/etc/systemd/system/varlink-decompress-xz.service&lt;/code&gt;&lt;/strong&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;[Service]&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;varlinkctl serve com.example.Decompress.XZ xz -d&lt;/span&gt;
&lt;span class="py"&gt;DynamicUser&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;PrivateNetwork&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;ProtectSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;
&lt;span class="py"&gt;ProtectHome&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;SystemCallFilter&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;~@privileged @resources&lt;/span&gt;
&lt;span class="py"&gt;MemoryMax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;256M&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable and call:&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 &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; varlink-decompress-xz.socket

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"hello"&lt;/span&gt; | xz | varlinkctl call &lt;span class="nt"&gt;--upgrade&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  unix:/run/varlink/registry/com.example.Decompress.XZ &lt;span class="se"&gt;\&lt;/span&gt;
  com.example.Decompress.XZ &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;span class="c"&gt;# expected stdout: hello&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick test without unit files (ephemeral listen socket):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-socket-activate &lt;span class="nt"&gt;-l&lt;/span&gt; /tmp/decompress.sock &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  varlinkctl serve com.example.Decompress.XZ xz &lt;span class="nt"&gt;-d&lt;/span&gt; &amp;amp;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"hello"&lt;/span&gt; | xz | varlinkctl call &lt;span class="nt"&gt;--upgrade&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  unix:/tmp/decompress.sock &lt;span class="se"&gt;\&lt;/span&gt;
  com.example.Decompress.XZ &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;The heavy lifting stays in a forked child with systemd sandboxing (&lt;code&gt;ProtectSystem=&lt;/code&gt;, &lt;code&gt;MemoryMax=&lt;/code&gt;, …).&lt;/li&gt;
&lt;li&gt;Clients discover a stable method name instead of ad-hoc FIFO paths.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--upgrade&lt;/code&gt; moves from JSON control plane to raw stream payload once the call succeeds.&lt;/li&gt;
&lt;li&gt;Pair with &lt;code&gt;--exec&lt;/code&gt; on the client when the reply includes file descriptors (&lt;code&gt;$LISTEN_FDS&lt;/code&gt; hand-off; 258+).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lab 8 — Validate IDL before you ship an interface
&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;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/com.example.Echo.varlink &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;'
# Minimal echo interface for validation practice
interface com.example.Echo

method Ping(message: string) -&amp;gt; (message: string)

error EmptyMessage ()
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;varlinkctl validate-idl /tmp/com.example.Echo.varlink
&lt;span class="c"&gt;# prints the definition with syntax highlighting on success&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Catch naming and structure mistakes early; the wire protocol expects the same IDL shape services return from &lt;code&gt;org.varlink.service&lt;/code&gt; introspection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operator cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;Command pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who am I talking to?&lt;/td&gt;
&lt;td&gt;&lt;code&gt;varlinkctl info ADDRESS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What can it do?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;list-interfaces&lt;/code&gt; / &lt;code&gt;list-methods&lt;/code&gt; / &lt;code&gt;introspect&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One shot call&lt;/td&gt;
&lt;td&gt;&lt;code&gt;call ADDRESS Fully.Qualified.Method '{"k":"v"}' -j&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream / subscribe&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;call -E …&lt;/code&gt; or &lt;code&gt;call --more --timeout=…&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle replies&lt;/td&gt;
&lt;td&gt;&lt;code&gt;call --collect …&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No reply&lt;/td&gt;
&lt;td&gt;&lt;code&gt;call --oneway …&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote socket&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ssh-unix:host:/path&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote binary&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ssh-exec:host:command&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrap stdio tool&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;varlinkctl serve METHOD cmdline…&lt;/code&gt; + socket unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check IDL file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;validate-idl FILE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registry&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;list-registry&lt;/code&gt; (260+)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;No such file or directory&lt;/code&gt; on socket&lt;/td&gt;
&lt;td&gt;Service down or path wrong&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemctl status …&lt;/code&gt;; confirm path under &lt;code&gt;/run/systemd&lt;/code&gt; or &lt;code&gt;/run/varlink/registry&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection refused / permission denied&lt;/td&gt;
&lt;td&gt;Socket mode or user&lt;/td&gt;
&lt;td&gt;Check unit &lt;code&gt;SocketUser=&lt;/code&gt;/&lt;code&gt;SocketGroup=&lt;/code&gt;; use root only when required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Method not found&lt;/td&gt;
&lt;td&gt;Typo or old package&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;list-methods&lt;/code&gt; / &lt;code&gt;introspect&lt;/code&gt;; upgrade systemd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hang then timeout&lt;/td&gt;
&lt;td&gt;Multi-reply method without &lt;code&gt;--more&lt;/code&gt;, or stuck peer&lt;/td&gt;
&lt;td&gt;Match flags to IDL; set &lt;code&gt;--timeout=&lt;/code&gt; deliberately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH unix path fails&lt;/td&gt;
&lt;td&gt;OpenSSH &amp;lt; 9.4 or abstract socket&lt;/td&gt;
&lt;td&gt;Upgrade SSH; use filesystem-bound sockets only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;serve&lt;/code&gt; never accepts&lt;/td&gt;
&lt;td&gt;Missing socket activation&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;.socket&lt;/code&gt; unit or &lt;code&gt;systemd-socket-activate&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON parse errors&lt;/td&gt;
&lt;td&gt;Shell quoting&lt;/td&gt;
&lt;td&gt;Single-quote the JSON object; or pass args via STDIN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pretty JSON breaks scripts&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;-j&lt;/code&gt; on a TTY&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;--json=short&lt;/code&gt; in pipelines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; systemd-resolved.service &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'varlink-*.service'&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How this fits nearby tooling
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;busctl&lt;/code&gt; / D-Bus&lt;/strong&gt; — still the right tool for classic desktop and many system APIs. Varlink is an additional, JSON-native path used heavily by newer systemd components.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;resolvectl&lt;/code&gt; / &lt;code&gt;hostnamectl&lt;/code&gt;&lt;/strong&gt; — polished UX on top of the same daemons; &lt;code&gt;varlinkctl&lt;/code&gt; is the generic wrench when you need raw methods or automation without scraping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;systemd-ssh-proxy&lt;/code&gt; / generators&lt;/strong&gt; — solve VM/container SSH transport; orthogonal to Varlink method calls (though &lt;code&gt;ssh-unix:&lt;/code&gt; reuses SSH as a Varlink tunnel).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom REST agents&lt;/strong&gt; — often unnecessary on localhost when a sandboxed &lt;code&gt;varlinkctl serve&lt;/code&gt; unit already gives you discovery, socket activation, and cgroup limits.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/unstable/systemd/varlinkctl.1.en.html" rel="noopener noreferrer"&gt;varlinkctl(1)&lt;/a&gt; — commands, address forms, flags, examples&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://uapi-group.org/specifications/specs/varlink/" rel="noopener noreferrer"&gt;UAPI.20 Varlink IPC&lt;/a&gt; — interface IDL, JSON mapping, wire protocol&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/unstable/systemd-resolved/systemd-resolved.service.8.en.html" rel="noopener noreferrer"&gt;systemd-resolved.service(8)&lt;/a&gt; — DNS resolver service behind &lt;code&gt;io.systemd.Resolve&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/unstable/systemd/busctl.1.en.html" rel="noopener noreferrer"&gt;busctl(1)&lt;/a&gt; — D-Bus counterpart for comparison&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.debian.org/unstable/systemd/systemd-socket-activate.1.en.html" rel="noopener noreferrer"&gt;systemd-socket-activate(1)&lt;/a&gt; — ad-hoc socket activation for &lt;code&gt;serve&lt;/code&gt; labs&lt;/li&gt;
&lt;li&gt;Upstream man source: &lt;a href="https://github.com/systemd/systemd" rel="noopener noreferrer"&gt;systemd &lt;code&gt;varlinkctl&lt;/code&gt; documentation&lt;/a&gt; (package man pages track your installed version)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have been parsing semi-stable command output or writing miniature D-Bus clients for one method call, point &lt;code&gt;varlinkctl&lt;/code&gt; at the socket, read the IDL, and call the method with JSON. Same skill scales from a quick resolved query to a socket-activated, sandboxed helper you can ship as a pair of unit files.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Losing User Services on Logout: Practical loginctl Linger on Linux</title>
      <dc:creator>Lyra</dc:creator>
      <pubDate>Mon, 28 Sep 2026 05:02:22 +0000</pubDate>
      <link>https://dev.to/lyraalishaikh/stop-losing-user-services-on-logout-practical-loginctl-linger-on-linux-2m8o</link>
      <guid>https://dev.to/lyraalishaikh/stop-losing-user-services-on-logout-practical-loginctl-linger-on-linux-2m8o</guid>
      <description>&lt;h1&gt;
  
  
  Stop Losing User Services on Logout: Practical loginctl Linger on Linux
&lt;/h1&gt;

&lt;p&gt;You enable a rootless Podman container, a &lt;code&gt;systemd --user&lt;/code&gt; timer, or a small agent under your account. It works while the SSH session is open. You disconnect — and everything under &lt;code&gt;/run/user/$UID&lt;/code&gt; vanishes with it.&lt;/p&gt;

&lt;p&gt;That is not random flakiness. It is how &lt;code&gt;systemd-logind&lt;/code&gt; and &lt;code&gt;pam_systemd&lt;/code&gt; are designed to behave when &lt;strong&gt;user lingering&lt;/strong&gt; is off: the per-user service manager (&lt;code&gt;user@.service&lt;/code&gt;) is tied to login sessions, and the runtime directory is removed when the last session ends.&lt;/p&gt;

&lt;p&gt;This guide is operational. Commands and defaults come from &lt;code&gt;loginctl(1)&lt;/code&gt;, &lt;code&gt;logind.conf(5)&lt;/code&gt;, &lt;code&gt;pam_systemd(8)&lt;/code&gt;, &lt;code&gt;user@.service(5)&lt;/code&gt;, and &lt;code&gt;systemd-logind.service(8)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What logind owns (and what it does not)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemd-logind.service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tracks users, sessions, seats; starts &lt;code&gt;user@.service&lt;/code&gt;; handles idle/power policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pam_systemd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Registers each login with logind; creates session scope + &lt;code&gt;/run/user/$UID&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user@UID.service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-user service manager (&lt;code&gt;systemd --user&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user-runtime-dir@UID.service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates/removes &lt;code&gt;/run/user/UID&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loginctl enable-linger&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep &lt;code&gt;user@.service&lt;/code&gt; at boot and after logout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;system units / root services&lt;/td&gt;
&lt;td&gt;Independent of linger; use those when the workload is truly system-wide&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Sessions&lt;/strong&gt; are login contexts (SSH, TTY, graphical). &lt;strong&gt;The user manager&lt;/strong&gt; is a long-lived service instance shared across a user’s sessions. Linger decides whether that manager (and the runtime dir) outlive the last session.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model in one tree
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;user@.service(5)&lt;/code&gt; and the &lt;code&gt;loginctl user-status&lt;/code&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;user.slice
└─user-1000.slice
   ├─user@1000.service          # systemd --user (shared)
   │  ├─init.scope
   │  └─your.timer / your.service
   ├─session-3.scope            # one SSH login
   └─session-12.scope           # another TTY/GUI login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Processes started by &lt;code&gt;systemd --user&lt;/code&gt; live under &lt;code&gt;user@UID.service&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Interactive login processes live under &lt;code&gt;session-N.scope&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Both sit under &lt;code&gt;user-UID.slice&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the last session ends and linger is &lt;strong&gt;disabled&lt;/strong&gt;, logind stops the user manager (after &lt;code&gt;UserStopDelaySec=&lt;/code&gt;) and tears down &lt;code&gt;/run/user/UID&lt;/code&gt;. Anything that needed &lt;code&gt;$XDG_RUNTIME_DIR&lt;/code&gt; (user D-Bus, rootless containers, many socket paths) is gone.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# loginctl ships with systemd on essentially every modern distro&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; loginctl
loginctl &lt;span class="nt"&gt;--version&lt;/span&gt;
systemctl is-active systemd-logind.service

&lt;span class="c"&gt;# You need a normal (non-system) user for the labs below&lt;/span&gt;
&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;   &lt;span class="c"&gt;# e.g. 1000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Privileged linger changes typically need root (or polkit authorization). Inspecting your own sessions usually does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 1 — Inventory sessions, users, and seats
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;loginctl list-sessions
loginctl list-users
loginctl list-seats

&lt;span class="c"&gt;# Human-readable status for the caller&lt;/span&gt;
loginctl session-status
loginctl user-status

&lt;span class="c"&gt;# Machine-readable properties&lt;/span&gt;
loginctl show-user &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; Name &lt;span class="nt"&gt;-p&lt;/span&gt; UID &lt;span class="nt"&gt;-p&lt;/span&gt; Linger &lt;span class="nt"&gt;-p&lt;/span&gt; State &lt;span class="nt"&gt;-p&lt;/span&gt; Sessions &lt;span class="nt"&gt;-p&lt;/span&gt; RuntimePath
loginctl show-session self &lt;span class="nt"&gt;-p&lt;/span&gt; Id &lt;span class="nt"&gt;-p&lt;/span&gt; Name &lt;span class="nt"&gt;-p&lt;/span&gt; Class &lt;span class="nt"&gt;-p&lt;/span&gt; Type &lt;span class="nt"&gt;-p&lt;/span&gt; Active &lt;span class="nt"&gt;-p&lt;/span&gt; State &lt;span class="nt"&gt;-p&lt;/span&gt; Display &lt;span class="nt"&gt;-p&lt;/span&gt; Remote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful properties (names from &lt;code&gt;loginctl show-*&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Linger&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;yes&lt;/code&gt; / &lt;code&gt;no&lt;/code&gt; — whether this user is set to linger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;State&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User/session state (&lt;code&gt;active&lt;/code&gt;, &lt;code&gt;online&lt;/code&gt;, &lt;code&gt;closing&lt;/code&gt;, …)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sessions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Session IDs belonging to the user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RuntimePath&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/run/user/UID&lt;/code&gt; when the runtime dir exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Class&lt;/code&gt; / &lt;code&gt;Type&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Session class (&lt;code&gt;user&lt;/code&gt;, &lt;code&gt;background&lt;/code&gt;, …) and type (&lt;code&gt;tty&lt;/code&gt;, &lt;code&gt;wayland&lt;/code&gt;, …)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;JSON forms (newer systemd builds):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;loginctl list-sessions &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pretty
loginctl list-users &lt;span class="nt"&gt;-j&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lab 2 — Prove the “logout kills user services” failure mode
&lt;/h2&gt;

&lt;p&gt;Run this from an interactive login as the target user (not via a lingering manager you already enabled).&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;# Start a trivial user service that just sleeps&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/systemd/user
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.config/systemd/user/linger-demo.service &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;'
[Unit]
Description=Linger demo heartbeat

[Service]
Type=simple
ExecStart=/bin/bash -c 'while true; do echo "alive &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-Is&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"; sleep 30; done'
Restart=always

[Install]
WantedBy=default.target
&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; &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; linger-demo.service
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status linger-demo.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"XDG_RUNTIME_DIR=&lt;/span&gt;&lt;span class="nv"&gt;$XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ld&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a &lt;strong&gt;second&lt;/strong&gt; root shell, watch the user manager:&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;UID_NUM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; alice&lt;span class="si"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;# replace alice with the lab user&lt;/span&gt;
systemctl status &lt;span class="s2"&gt;"user@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID_NUM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.service"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; /run/user/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now fully log the user out of &lt;strong&gt;all&lt;/strong&gt; sessions (close SSH, log out of desktop, etc.). With linger disabled and after &lt;code&gt;UserStopDelaySec=&lt;/code&gt; (default &lt;strong&gt;10s&lt;/strong&gt; per &lt;code&gt;logind.conf(5)&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;systemctl status &lt;span class="s2"&gt;"user@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID_NUM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.service"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="c"&gt;# expected: inactive/dead once the user fully logged out&lt;/span&gt;

&lt;span class="nb"&gt;ls&lt;/span&gt; /run/user/
&lt;span class="c"&gt;# expected: UID directory gone&lt;/span&gt;

&lt;span class="c"&gt;# The unit may still exist on disk under ~/.config/systemd/user,&lt;/span&gt;
&lt;span class="c"&gt;# but nothing is running it until the user manager returns.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the bug report you keep getting from “my user timer stopped overnight after I closed the laptop lid / SSH’d out.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 3 — Enable linger the supported way
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# As root (or authorized admin)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl enable-linger alice

&lt;span class="c"&gt;# Verify&lt;/span&gt;
loginctl show-user alice &lt;span class="nt"&gt;-p&lt;/span&gt; Linger
&lt;span class="c"&gt;# Linger=yes&lt;/span&gt;

&lt;span class="c"&gt;# Persistent marker files live here (one empty file per lingering user):&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /var/lib/systemd/linger/
&lt;span class="c"&gt;# ... alice&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What &lt;code&gt;enable-linger&lt;/code&gt; does, per &lt;code&gt;loginctl(1)&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If enabled for a specific user, a user manager is spawned for the user at boot and kept around after logouts. This allows users who are not logged in to run long-running services.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Effects you should see:&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;# Even with zero interactive sessions:&lt;/span&gt;
loginctl list-users
systemctl is-active user@1000.service          &lt;span class="c"&gt;# active&lt;/span&gt;
systemctl is-active user-runtime-dir@1000.service
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ld&lt;/span&gt; /run/user/1000
&lt;span class="c"&gt;# XDG_RUNTIME_DIR still present for that UID&lt;/span&gt;

&lt;span class="c"&gt;# User services survive&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; alice &lt;span class="nv"&gt;XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/run/user/1000 &lt;span class="se"&gt;\&lt;/span&gt;
  systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status linger-demo.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Talking to another user’s bus from root (documented &lt;code&gt;loginctl&lt;/code&gt; / &lt;code&gt;systemctl&lt;/code&gt; machine syntax pattern):&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="nt"&gt;--machine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;alice@.host status linger-demo.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="c"&gt;# or&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; alice &lt;span class="nv"&gt;DBUS_SESSION_BUS_ADDRESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unix:path&lt;span class="o"&gt;=&lt;/span&gt;/run/user/1000/bus &lt;span class="se"&gt;\&lt;/span&gt;
  systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; list-units &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;service &lt;span class="nt"&gt;--state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disable when you no longer need 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;loginctl disable-linger alice
&lt;span class="nb"&gt;ls&lt;/span&gt; /var/lib/systemd/linger/   &lt;span class="c"&gt;# alice marker gone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After disable, the next full logout (and &lt;code&gt;UserStopDelaySec=&lt;/code&gt; expiry) returns to the non-linger teardown behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 4 — Boot-time user manager without an interactive login
&lt;/h2&gt;

&lt;p&gt;With linger enabled, reboot (or start on a host where the user never logged in this 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="c"&gt;# After boot, as root&lt;/span&gt;
loginctl show-user alice &lt;span class="nt"&gt;-p&lt;/span&gt; Linger &lt;span class="nt"&gt;-p&lt;/span&gt; State
systemctl status user@1000.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ld&lt;/span&gt; /run/user/1000

&lt;span class="c"&gt;# Enable lingering user units so they start with the user manager&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; alice &lt;span class="nv"&gt;XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/run/user/1000 &lt;span class="se"&gt;\&lt;/span&gt;
  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; linger-demo.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Units linked to &lt;code&gt;default.target&lt;/code&gt; under &lt;code&gt;~/.config/systemd/user/&lt;/code&gt; start with the lingering user manager — no SSH required.&lt;/p&gt;

&lt;p&gt;Practical workloads that usually want this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rootless Podman / Quadlet user units&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd --user&lt;/code&gt; timers for backups, sync, cert renewals&lt;/li&gt;
&lt;li&gt;Personal agents bound to &lt;code&gt;$XDG_RUNTIME_DIR&lt;/code&gt; sockets&lt;/li&gt;
&lt;li&gt;Lingering &lt;code&gt;pipewire&lt;/code&gt; / session buses on headless seats (when you intentionally run them that way)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Workloads that usually should &lt;strong&gt;not&lt;/strong&gt; linger as a user service:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anything that must run before &lt;code&gt;systemd-user-sessions.service&lt;/code&gt; allows logins&lt;/li&gt;
&lt;li&gt;Multi-tenant daemons that belong under system units with explicit &lt;code&gt;User=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Secrets you only want available while a human is present&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lab 5 — KillUserProcesses, UserStopDelaySec, and tmux myths
&lt;/h2&gt;

&lt;p&gt;Linger is not the only dial. From &lt;code&gt;logind.conf(5)&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;# Vendor defaults are commented in the shipped file; local drop-ins win.&lt;/span&gt;
man logind.conf
&lt;span class="nb"&gt;ls&lt;/span&gt; /etc/systemd/logind.conf.d/ 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Default (upstream man page)&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KillUserProcesses=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;If &lt;code&gt;yes&lt;/code&gt;, session scope processes are killed on logout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;KillOnlyUsers=&lt;/code&gt; / &lt;code&gt;KillExcludeUsers=&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;(see man)&lt;/td&gt;
&lt;td&gt;Overrides who gets session-kill behavior; root excluded by default when unset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UserStopDelaySec=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How long to keep &lt;code&gt;user@.service&lt;/code&gt; after the last session ends (&lt;code&gt;0&lt;/code&gt; = immediate; &lt;code&gt;infinity&lt;/code&gt; = never stop after first login)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RemoveIPC=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove SysV/POSIX IPC objects on full logout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RuntimeDirectorySize=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;10%&lt;/code&gt; of RAM&lt;/td&gt;
&lt;td&gt;Safety limit for each &lt;code&gt;/run/user/UID&lt;/code&gt; tmpfs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Important nuance from the man page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;KillUserProcesses=yes&lt;/code&gt; kills processes in the &lt;strong&gt;session scope&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Independently, linger controls whether &lt;strong&gt;&lt;code&gt;user@.service&lt;/code&gt;&lt;/strong&gt; stays up.&lt;/li&gt;
&lt;li&gt;Tools like &lt;code&gt;tmux&lt;/code&gt;/&lt;code&gt;screen&lt;/code&gt; that stay inside the session scope die when session-kill is on, unless moved out (for example with &lt;code&gt;systemd-run --user --scope&lt;/code&gt; as discussed in &lt;code&gt;systemd-run(1)&lt;/code&gt; / &lt;code&gt;logind.conf(5)&lt;/code&gt; notes).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop-in example (only if you understand the impact on shared hosts):&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/systemd/logind.conf.d
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/logind.conf.d/20-lab-kill.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;'
[Login]
# Example only — many desktops/distros already ship an opinion here.
KillUserProcesses=yes
UserStopDelaySec=10s
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart systemd-logind.service
&lt;span class="c"&gt;# Caution: restarting logind can disrupt active sessions; do this in a maintenance window.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer &lt;strong&gt;linger for user services&lt;/strong&gt; over globally turning off process cleanup when the real goal is “keep my timer alive.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 6 — Session hygiene: lock, terminate, kill
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Inspect&lt;/span&gt;
loginctl list-sessions &lt;span class="nt"&gt;--no-legend&lt;/span&gt;
loginctl session-status 3   &lt;span class="c"&gt;# replace with a real ID&lt;/span&gt;

&lt;span class="c"&gt;# Request screen lock where the session supports it&lt;/span&gt;
loginctl lock-session 3
loginctl unlock-session 3
loginctl lock-sessions      &lt;span class="c"&gt;# all lock-capable sessions&lt;/span&gt;

&lt;span class="c"&gt;# Clean teardown of one session (kills its processes, frees resources)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl terminate-session 3

&lt;span class="c"&gt;# Signal without full teardown&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl kill-session 3 &lt;span class="nt"&gt;--kill-whom&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;all &lt;span class="nt"&gt;--signal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SIGTERM

&lt;span class="c"&gt;# Tear down every session for a user (does not by itself clear linger)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl terminate-user alice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;terminate-user&lt;/code&gt; ends sessions and deallocates runtime resources attached to those sessions. With linger still enabled, expect &lt;code&gt;user@.service&lt;/code&gt; to come back (or stay) according to linger policy rather than “user permanently gone.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab 7 — Cap resources for lingering users
&lt;/h2&gt;

&lt;p&gt;Lingering users still consume a user manager and a runtime directory. Bound them with the slice hierarchy from &lt;code&gt;user@.service(5)&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;# All users under user.slice&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/systemd/system/user-.slice.d
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/user-.slice.d/20-limits.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;'
[Slice]
# Defaults already include TasksMax=33% via vendor drop-in on many systems.
MemoryHigh=2G
MemoryMax=3G
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# One specific UID&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/systemd/system/user-1000.slice.d
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/user-1000.slice.d/20-limits.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;'
[Slice]
CPUQuota=200%
MemoryMax=4G
TasksMax=4096
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="c"&gt;# Apply to live slices when present:&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart user-1000.slice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Session-scoped limits can also be applied via PAM data keys documented in &lt;code&gt;pam_systemd(8)&lt;/code&gt; (&lt;code&gt;systemd.memory_max&lt;/code&gt;, &lt;code&gt;systemd.tasks_max&lt;/code&gt;, …). Those apply to &lt;strong&gt;session scopes&lt;/strong&gt;, not to the shared &lt;code&gt;user@.service&lt;/code&gt; tree — another reason lingering services need slice-level policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal “rootless service that survives logout” recipe
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;USER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;UID_NUM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl enable-linger &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Wait until runtime dir exists (boot or first activation)&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 50&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"/run/user/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID_NUM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break
  sleep &lt;/span&gt;0.2
&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/run/user/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID_NUM&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;
  systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload

&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/run/user/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID_NUM&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;
  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; linger-demo.service

loginctl show-user &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; Linger &lt;span class="nt"&gt;-p&lt;/span&gt; RuntimePath &lt;span class="nt"&gt;-p&lt;/span&gt; State
systemctl status &lt;span class="s2"&gt;"user@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID_NUM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.service"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;linger-demo.service&lt;/code&gt; with your Quadlet, timer, or agent unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User timer dies after SSH logout&lt;/td&gt;
&lt;td&gt;Linger disabled; &lt;code&gt;user@.service&lt;/code&gt; stopped&lt;/td&gt;
&lt;td&gt;&lt;code&gt;loginctl enable-linger USER&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Failed to connect to bus&lt;/code&gt; as user&lt;/td&gt;
&lt;td&gt;No &lt;code&gt;/run/user/UID&lt;/code&gt; / user manager&lt;/td&gt;
&lt;td&gt;Enable linger or stay logged in; export &lt;code&gt;XDG_RUNTIME_DIR&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linger set but services still stop&lt;/td&gt;
&lt;td&gt;Units not enabled under &lt;code&gt;--user&lt;/code&gt;; wrong &lt;code&gt;WantedBy=&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemctl --user enable --now …&lt;/code&gt;; link to &lt;code&gt;default.target&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rootless Podman breaks after logout&lt;/td&gt;
&lt;td&gt;Runtime dir removed; containers needed linger&lt;/td&gt;
&lt;td&gt;Enable linger for that UID; confirm &lt;code&gt;/run/user/UID&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tmux&lt;/code&gt; dies on logout&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;KillUserProcesses=yes&lt;/code&gt; and tmux still in session scope&lt;/td&gt;
&lt;td&gt;Move work to &lt;code&gt;systemd --user&lt;/code&gt; / &lt;code&gt;systemd-run --user --scope&lt;/code&gt;, or adjust kill policy carefully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/run/user/UID&lt;/code&gt; grows large&lt;/td&gt;
&lt;td&gt;Runtime tmpfs pressure&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;RuntimeDirectorySize=&lt;/code&gt;; stop leaking caches into the runtime dir&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;enable-linger&lt;/code&gt; fails&lt;/td&gt;
&lt;td&gt;Authorization&lt;/td&gt;
&lt;td&gt;Run as root; check polkit / &lt;code&gt;systemd-logind&lt;/code&gt; logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User manager flap on rapid logout/login&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UserStopDelaySec=0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Raise delay (default 10s) if appropriate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; systemd-logind.service &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'user@*.service'&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How this fits nearby systemd pieces
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System services&lt;/strong&gt; (&lt;code&gt;systemctl enable&lt;/code&gt;) — always-on, root or static &lt;code&gt;User=&lt;/code&gt;, independent of linger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;run0&lt;/code&gt; / &lt;code&gt;systemd-run&lt;/code&gt;&lt;/strong&gt; — transient elevation or one-shot scopes; not a substitute for a lingering user manager.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Socket activation&lt;/strong&gt; — on-demand start is orthogonal; user socket units still need a living &lt;code&gt;systemd --user&lt;/code&gt; if they are user units.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;homed / encrypted homes&lt;/strong&gt; — home unlock is separate from linger; a lingering manager with an unavailable home can still surprise you at boot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;lingering ≠ immortality&lt;/strong&gt; — &lt;code&gt;terminate-user&lt;/code&gt;, resource OOM policy, and admin &lt;code&gt;disable-linger&lt;/code&gt; still apply.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/loginctl.1.en" rel="noopener noreferrer"&gt;loginctl(1)&lt;/a&gt; — &lt;code&gt;enable-linger&lt;/code&gt; / &lt;code&gt;disable-linger&lt;/code&gt;, session and user commands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/logind.conf.5.en" rel="noopener noreferrer"&gt;logind.conf(5)&lt;/a&gt; — &lt;code&gt;KillUserProcesses=&lt;/code&gt;, &lt;code&gt;UserStopDelaySec=&lt;/code&gt;, runtime dir limits&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/systemd-logind.service.8.en" rel="noopener noreferrer"&gt;systemd-logind.service(8)&lt;/a&gt; — login manager responsibilities&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/pam_systemd.8.en" rel="noopener noreferrer"&gt;pam_systemd(8)&lt;/a&gt; — session registration, &lt;code&gt;/run/user/$UID&lt;/code&gt;, logout teardown&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/user@.service.5.en" rel="noopener noreferrer"&gt;user@.service(5)&lt;/a&gt; — user manager + &lt;code&gt;user-runtime-dir@.service&lt;/code&gt; + slice hierarchy&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.archlinux.org/man/systemd-run.1.en" rel="noopener noreferrer"&gt;systemd-run(1)&lt;/a&gt; — transient user scopes/services&lt;/li&gt;
&lt;li&gt;Debian man page mirror: &lt;a href="https://manpages.debian.org/bookworm/systemd/loginctl.1.en.html" rel="noopener noreferrer"&gt;loginctl(1) on manpages.debian.org&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your “set it and forget it” user service only lives as long as an SSH tty, turn linger on deliberately, enable the user unit, and verify &lt;code&gt;Linger=yes&lt;/code&gt; plus a living &lt;code&gt;/run/user/$UID&lt;/code&gt; after logout. That single control is the difference between a rootless stack that survives disconnects and one that only works while you are watching it.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systemd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
