<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>linux · wleeaf field notes</title><link>https://articles.wleeaf.dev/tags/linux/</link><description>Hands-on guides and build logs from the wleeaf studio: self-hosting, Linux, networking, Docker, the command line and reverse engineering.</description><language>en-us</language><lastBuildDate>Sat, 04 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://articles.wleeaf.dev/tags/linux/feed.xml" rel="self" type="application/rss+xml"/><item><title>awk in 10 Minutes With Practical Examples</title><link>https://articles.wleeaf.dev/posts/awk-practical-examples/</link><pubDate>Sat, 04 Apr 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/awk-practical-examples/</guid><description>Nobody reads the awk manual. Here are the patterns that cover 90% of real-world use.</description><category>awk</category><category>cli</category><category>linux</category><content:encoded><![CDATA[<p>awk is one of those tools that looks intimidating until you learn five patterns. Then you use it every day. I am not going to explain the full language. Here are the patterns that actually matter.</p>
<h2 id="pattern-1-print-a-column">Pattern 1: Print a Column<a class="anchor" href="#pattern-1-print-a-column" aria-label="Link to “Pattern 1: Print a Column”">#</a></h2>
<p>awk splits each line into fields. <code>$1</code> is the first, <code>$2</code> is the second, <code>$0</code> is the whole line:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Print the first column of ps output</span>
</span></span><span class="line"><span class="cl">ps aux <span class="p">|</span> awk <span class="s1">&#39;{print $1}&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Print columns 1 and 11 (user and command)</span>
</span></span><span class="line"><span class="cl">ps aux <span class="p">|</span> awk <span class="s1">&#39;{print $1, $11}&#39;</span></span></span></code></pre></div>
</div>
<p>The default field separator is whitespace. Change it with <code>-F</code>:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Parse /etc/passwd (colon-separated)</span>
</span></span><span class="line"><span class="cl">awk -F: <span class="s1">&#39;{print $1, $7}&#39;</span> /etc/passwd
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Parse CSV</span>
</span></span><span class="line"><span class="cl">awk -F, <span class="s1">&#39;{print $2}&#39;</span> data.csv</span></span></code></pre></div>
</div>
<h2 id="pattern-2-filter-rows">Pattern 2: Filter Rows<a class="anchor" href="#pattern-2-filter-rows" aria-label="Link to “Pattern 2: Filter Rows”">#</a></h2>
<p>Put a condition before the action:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Lines where column 3 is greater than 100</span>
</span></span><span class="line"><span class="cl">awk <span class="s1">&#39;$3 &gt; 100&#39;</span> data.txt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Lines containing &#34;ERROR&#34;</span>
</span></span><span class="line"><span class="cl">awk <span class="s1">&#39;/ERROR/&#39;</span> app.log
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Lines where the first field matches a pattern</span>
</span></span><span class="line"><span class="cl">awk <span class="s1">&#39;$1 ~ /^192\.168/&#39;</span> access.log</span></span></code></pre></div>
</div>
<p>Combine conditions:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Memory usage over 50% AND the process is not root</span>
</span></span><span class="line"><span class="cl">ps aux <span class="p">|</span> awk <span class="s1">&#39;$4 &gt; 50.0 &amp;&amp; $1 != &#34;root&#34; {print $1, $4, $11}&#39;</span></span></span></code></pre></div>
</div>
<h2 id="pattern-3-sum-a-column">Pattern 3: Sum a Column<a class="anchor" href="#pattern-3-sum-a-column" aria-label="Link to “Pattern 3: Sum a Column”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Total bytes transferred (column 10 in access logs)</span>
</span></span><span class="line"><span class="cl">awk <span class="s1">&#39;{sum += $10} END {print sum}&#39;</span> access.log
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Average response time</span>
</span></span><span class="line"><span class="cl">awk <span class="s1">&#39;{sum += $NF; n++} END {print sum/n}&#39;</span> response_times.log</span></span></code></pre></div>
</div>
<p><code>END</code> runs after all lines are processed. <code>NF</code> is the number of fields on the current line, so <code>$NF</code> is the last field.</p>
<h2 id="pattern-4-count-and-group">Pattern 4: Count and Group<a class="anchor" href="#pattern-4-count-and-group" aria-label="Link to “Pattern 4: Count and Group”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Count HTTP status codes</span>
</span></span><span class="line"><span class="cl">awk <span class="s1">&#39;{count[$9]++} END {for (c in count) print c, count[c]}&#39;</span> access.log <span class="p">|</span> sort -rn -k2
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Output:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 200 45231</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 304 12044</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 404 891</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 500 23</span></span></span></code></pre></div>
</div>
<p>This uses an associative array. <code>count[$9]++</code> increments the count for each unique value in column 9.</p>
<p>Top 10 IP addresses by request count:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">awk <span class="s1">&#39;{count[$1]++} END {for (ip in count) print count[ip], ip}&#39;</span> access.log <span class="p">|</span> sort -rn <span class="p">|</span> head -10</span></span></code></pre></div>
</div>
<h2 id="pattern-5-transform-fields">Pattern 5: Transform Fields<a class="anchor" href="#pattern-5-transform-fields" aria-label="Link to “Pattern 5: Transform Fields”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Convert bytes to megabytes</span>
</span></span><span class="line"><span class="cl">awk <span class="s1">&#39;{printf &#34;%s %.1fMB\n&#34;, $1, $2/1048576}&#39;</span> sizes.txt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Add a prefix to every line</span>
</span></span><span class="line"><span class="cl">awk <span class="s1">&#39;{print &#34;[INFO]&#34;, $0}&#39;</span> app.log
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Swap two columns</span>
</span></span><span class="line"><span class="cl">awk <span class="s1">&#39;{temp=$1; $1=$2; $2=temp; print}&#39;</span> data.txt</span></span></code></pre></div>
</div>
<h2 id="real-world-examples">Real-World Examples<a class="anchor" href="#real-world-examples" aria-label="Link to “Real-World Examples”">#</a></h2>
<h3 id="parse-docker-ps-output">Parse Docker ps output<a class="anchor" href="#parse-docker-ps-output" aria-label="Link to “Parse Docker ps output”">#</a></h3>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker ps --format <span class="s1">&#39;{{.Names}}\t{{.Status}}\t{{.Ports}}&#39;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  awk -F<span class="s1">&#39;\t&#39;</span> <span class="s1">&#39;{printf &#34;%-20s %-15s %s\n&#34;, $1, $2, $3}&#39;</span></span></span></code></pre></div>
</div>
<h3 id="find-the-largest-docker-images">Find the largest Docker images<a class="anchor" href="#find-the-largest-docker-images" aria-label="Link to “Find the largest Docker images”">#</a></h3>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker images --format <span class="s1">&#39;{{.Repository}}:{{.Tag}}\t{{.Size}}&#39;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  awk -F<span class="s1">&#39;\t&#39;</span> <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">    size = $2
</span></span></span><span class="line"><span class="cl"><span class="s1">    if (size ~ /GB/) {
</span></span></span><span class="line"><span class="cl"><span class="s1">      gsub(/GB/, &#34;&#34;, size)
</span></span></span><span class="line"><span class="cl"><span class="s1">      mb = size * 1024
</span></span></span><span class="line"><span class="cl"><span class="s1">    } else if (size ~ /MB/) {
</span></span></span><span class="line"><span class="cl"><span class="s1">      gsub(/MB/, &#34;&#34;, size)
</span></span></span><span class="line"><span class="cl"><span class="s1">      mb = size
</span></span></span><span class="line"><span class="cl"><span class="s1">    } else {
</span></span></span><span class="line"><span class="cl"><span class="s1">      mb = 0
</span></span></span><span class="line"><span class="cl"><span class="s1">    }
</span></span></span><span class="line"><span class="cl"><span class="s1">    printf &#34;%8.0fMB  %s\n&#34;, mb, $1
</span></span></span><span class="line"><span class="cl"><span class="s1">  }&#39;</span> <span class="p">|</span> sort -rn <span class="p">|</span> head -10</span></span></code></pre></div>
</div>
<h3 id="calculate-uptime-percentage">Calculate uptime percentage<a class="anchor" href="#calculate-uptime-percentage" aria-label="Link to “Calculate uptime percentage”">#</a></h3>
<p>Given a log with timestamps and status:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">awk <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">  /UP/   { up++ }
</span></span></span><span class="line"><span class="cl"><span class="s1">  /DOWN/ { down++ }
</span></span></span><span class="line"><span class="cl"><span class="s1">  END {
</span></span></span><span class="line"><span class="cl"><span class="s1">    total = up + down
</span></span></span><span class="line"><span class="cl"><span class="s1">    if (total &gt; 0)
</span></span></span><span class="line"><span class="cl"><span class="s1">      printf &#34;Uptime: %.2f%% (%d/%d checks)\n&#34;, (up/total)*100, up, total
</span></span></span><span class="line"><span class="cl"><span class="s1">  }
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span> monitoring.log</span></span></code></pre></div>
</div>
<h3 id="extract-unique-error-messages">Extract unique error messages<a class="anchor" href="#extract-unique-error-messages" aria-label="Link to “Extract unique error messages”">#</a></h3>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">awk <span class="s1">&#39;/ERROR/ {
</span></span></span><span class="line"><span class="cl"><span class="s1">  msg = &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">  for (i=4; i&lt;=NF; i++) msg = msg &#34; &#34; $i
</span></span></span><span class="line"><span class="cl"><span class="s1">  errors[msg]++
</span></span></span><span class="line"><span class="cl"><span class="s1">}
</span></span></span><span class="line"><span class="cl"><span class="s1">END {
</span></span></span><span class="line"><span class="cl"><span class="s1">  for (e in errors) printf &#34;%5d %s\n&#34;, errors[e], e
</span></span></span><span class="line"><span class="cl"><span class="s1">}&#39;</span> app.log <span class="p">|</span> sort -rn <span class="p">|</span> head -20</span></span></code></pre></div>
</div>
<h2 id="built-in-variables">Built-in Variables<a class="anchor" href="#built-in-variables" aria-label="Link to “Built-in Variables”">#</a></h2>
<table>
  <thead>
      <tr>
          <th>Variable</th>
          <th>Meaning</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>NR</code></td>
          <td>Current line number</td>
      </tr>
      <tr>
          <td><code>NF</code></td>
          <td>Number of fields on current line</td>
      </tr>
      <tr>
          <td><code>FS</code></td>
          <td>Field separator</td>
      </tr>
      <tr>
          <td><code>OFS</code></td>
          <td>Output field separator</td>
      </tr>
      <tr>
          <td><code>$0</code></td>
          <td>Entire line</td>
      </tr>
      <tr>
          <td><code>$1..$n</code></td>
          <td>Individual fields</td>
      </tr>
      <tr>
          <td><code>FILENAME</code></td>
          <td>Current filename</td>
      </tr>
  </tbody>
</table>
<h2 id="when-to-use-awk-vs-other-tools">When to Use awk vs. Other Tools<a class="anchor" href="#when-to-use-awk-vs-other-tools" aria-label="Link to “When to Use awk vs. Other Tools”">#</a></h2>
<ul>
<li><strong>cut</strong>: When you just need a column and nothing else. <code>cut -d: -f1 /etc/passwd</code> is simpler than awk for trivial extraction.</li>
<li><strong>grep</strong>: When you just need to filter lines by a pattern.</li>
<li><strong>sed</strong>: When you need to do find-and-replace on text.</li>
<li><strong>awk</strong>: When you need to do math, grouping, conditional logic, or work with multiple fields at once.</li>
</ul>
<p>The mental model is simple. awk reads lines, splits them into fields, and lets you write rules about what to do with each line. Once that clicks, you start seeing awk solutions everywhere.</p>
]]></content:encoded></item><item><title>Python One-Liners for Server Troubleshooting</title><link>https://articles.wleeaf.dev/posts/python-one-liners-sysadmin/</link><pubDate>Thu, 02 Apr 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/python-one-liners-sysadmin/</guid><description>When awk gets ugly and bash gets painful, Python -c is right there in your PATH.</description><category>python</category><category>linux</category><category>sysadmin</category><content:encoded><![CDATA[<p>Sometimes awk is not enough and a full script is too much. Python sits in the middle. Every Linux server has it installed, and <code>python3 -c</code> lets you run code inline.</p>
<h2 id="quick-http-server">Quick HTTP Server<a class="anchor" href="#quick-http-server" aria-label="Link to “Quick HTTP Server”">#</a></h2>
<p>The classic. Serve the current directory:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 -m http.server <span class="m">8080</span></span></span></code></pre></div>
</div>
<p>Bind to a specific interface:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 -m http.server <span class="m">8080</span> --bind 127.0.0.1</span></span></code></pre></div>
</div>
<p>This is genuinely useful for transferring files between machines, testing static sites, or sharing a directory with a coworker.</p>
<h2 id="json-pretty-printing">JSON Pretty Printing<a class="anchor" href="#json-pretty-printing" aria-label="Link to “JSON Pretty Printing”">#</a></h2>
<p>When you do not have jq installed:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://api.example.com/data <span class="p">|</span> python3 -m json.tool</span></span></code></pre></div>
</div>
<p>Or with color:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;{&#34;a&#34;:1,&#34;b&#34;:[2,3]}&#39;</span> <span class="p">|</span> python3 -c <span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">import json, sys
</span></span></span><span class="line"><span class="cl"><span class="s2">data = json.load(sys.stdin)
</span></span></span><span class="line"><span class="cl"><span class="s2">print(json.dumps(data, indent=2, sort_keys=True))
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span></span></span></code></pre></div>
</div>
<h2 id="parse-timestamps">Parse Timestamps<a class="anchor" href="#parse-timestamps" aria-label="Link to “Parse Timestamps”">#</a></h2>
<p>Convert epoch timestamps that show up in logs:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 -c <span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">from datetime import datetime
</span></span></span><span class="line"><span class="cl"><span class="s2">import sys
</span></span></span><span class="line"><span class="cl"><span class="s2">for line in sys.stdin:
</span></span></span><span class="line"><span class="cl"><span class="s2">    ts = int(line.strip())
</span></span></span><span class="line"><span class="cl"><span class="s2">    print(datetime.fromtimestamp(ts).isoformat())
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span> <span class="o">&lt;&lt;&lt;</span> <span class="s2">&#34;1711708800&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 2024-03-29T12:00:00</span></span></span></code></pre></div>
</div>
<h2 id="find-large-files">Find Large Files<a class="anchor" href="#find-large-files" aria-label="Link to “Find Large Files”">#</a></h2>
<p><code>find</code> works, but if you want more control over the output:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 -c <span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">import os
</span></span></span><span class="line"><span class="cl"><span class="s2">files = []
</span></span></span><span class="line"><span class="cl"><span class="s2">for root, dirs, fnames in os.walk(&#39;/var/log&#39;):
</span></span></span><span class="line"><span class="cl"><span class="s2">    for f in fnames:
</span></span></span><span class="line"><span class="cl"><span class="s2">        path = os.path.join(root, f)
</span></span></span><span class="line"><span class="cl"><span class="s2">        try:
</span></span></span><span class="line"><span class="cl"><span class="s2">            size = os.path.getsize(path)
</span></span></span><span class="line"><span class="cl"><span class="s2">            if size &gt; 100_000_000:  # 100MB
</span></span></span><span class="line"><span class="cl"><span class="s2">                files.append((size, path))
</span></span></span><span class="line"><span class="cl"><span class="s2">        except OSError:
</span></span></span><span class="line"><span class="cl"><span class="s2">            pass
</span></span></span><span class="line"><span class="cl"><span class="s2">for size, path in sorted(files, reverse=True):
</span></span></span><span class="line"><span class="cl"><span class="s2">    print(f&#39;{size / 1e9:.1f}G  {path}&#39;)
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span></span></span></code></pre></div>
</div>
<h2 id="base64-decode">Base64 Decode<a class="anchor" href="#base64-decode" aria-label="Link to “Base64 Decode”">#</a></h2>
<p>Decode those opaque values in Kubernetes secrets or JWTs:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 -c <span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">import base64, json, sys
</span></span></span><span class="line"><span class="cl"><span class="s2">token = sys.argv[1]
</span></span></span><span class="line"><span class="cl"><span class="s2">parts = token.split(&#39;.&#39;)
</span></span></span><span class="line"><span class="cl"><span class="s2">for part in parts[:2]:
</span></span></span><span class="line"><span class="cl"><span class="s2">    padded = part + &#39;=&#39; * (4 - len(part) % 4)
</span></span></span><span class="line"><span class="cl"><span class="s2">    decoded = base64.urlsafe_b64decode(padded)
</span></span></span><span class="line"><span class="cl"><span class="s2">    print(json.dumps(json.loads(decoded), indent=2))
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span> <span class="s2">&#34;eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.signature&#34;</span></span></span></code></pre></div>
</div>
<h2 id="port-scanner">Port Scanner<a class="anchor" href="#port-scanner" aria-label="Link to “Port Scanner”">#</a></h2>
<p>Quick check which ports are open without installing nmap:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 -c <span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">import socket, sys
</span></span></span><span class="line"><span class="cl"><span class="s2">host = sys.argv[1]
</span></span></span><span class="line"><span class="cl"><span class="s2">for port in range(1, 1025):
</span></span></span><span class="line"><span class="cl"><span class="s2">    s = socket.socket()
</span></span></span><span class="line"><span class="cl"><span class="s2">    s.settimeout(0.1)
</span></span></span><span class="line"><span class="cl"><span class="s2">    if s.connect_ex((host, port)) == 0:
</span></span></span><span class="line"><span class="cl"><span class="s2">        print(f&#39;{port}/tcp open&#39;)
</span></span></span><span class="line"><span class="cl"><span class="s2">    s.close()
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span> 10.0.0.1</span></span></code></pre></div>
</div>
<h2 id="monitor-a-log-file-with-filtering">Monitor a Log File With Filtering<a class="anchor" href="#monitor-a-log-file-with-filtering" aria-label="Link to “Monitor a Log File With Filtering”">#</a></h2>
<p>Like <code>tail -f | grep</code> but with more logic:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 -c <span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">import sys, re, time
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">pattern = re.compile(r&#39;(ERROR|WARN|FATAL)&#39;, re.IGNORECASE)
</span></span></span><span class="line"><span class="cl"><span class="s2">seen = set()
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">for line in sys.stdin:
</span></span></span><span class="line"><span class="cl"><span class="s2">    if pattern.search(line):
</span></span></span><span class="line"><span class="cl"><span class="s2">        key = line.strip()
</span></span></span><span class="line"><span class="cl"><span class="s2">        if key not in seen:
</span></span></span><span class="line"><span class="cl"><span class="s2">            seen.add(key)
</span></span></span><span class="line"><span class="cl"><span class="s2">            print(line, end=&#39;&#39;)
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span> &lt; /var/log/syslog</span></span></code></pre></div>
</div>
<p>This deduplicates repeated errors so you see each unique message only once.</p>
<h2 id="csv-to-json">CSV to JSON<a class="anchor" href="#csv-to-json" aria-label="Link to “CSV to JSON”">#</a></h2>
<p>Convert that spreadsheet export into something usable:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 -c <span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">import csv, json, sys
</span></span></span><span class="line"><span class="cl"><span class="s2">reader = csv.DictReader(sys.stdin)
</span></span></span><span class="line"><span class="cl"><span class="s2">print(json.dumps(list(reader), indent=2))
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span> &lt; data.csv</span></span></code></pre></div>
</div>
<h2 id="url-encode-and-decode">URL Encode and Decode<a class="anchor" href="#url-encode-and-decode" aria-label="Link to “URL Encode and Decode”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Encode</span>
</span></span><span class="line"><span class="cl">python3 -c <span class="s2">&#34;from urllib.parse import quote; print(quote(&#39;hello world &amp; friends&#39;))&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># hello%20world%20%26%20friends</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Decode</span>
</span></span><span class="line"><span class="cl">python3 -c <span class="s2">&#34;from urllib.parse import unquote; print(unquote(&#39;hello%20world%20%26%20friends&#39;))&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># hello world &amp; friends</span></span></span></code></pre></div>
</div>
<h2 id="generate-random-passwords">Generate Random Passwords<a class="anchor" href="#generate-random-passwords" aria-label="Link to “Generate Random Passwords”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 -c <span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">import secrets, string
</span></span></span><span class="line"><span class="cl"><span class="s2">alphabet = string.ascii_letters + string.digits + &#39;!@#</span>$<span class="s2">%&#39;
</span></span></span><span class="line"><span class="cl"><span class="s2">password = &#39;&#39;.join(secrets.choice(alphabet) for _ in range(24))
</span></span></span><span class="line"><span class="cl"><span class="s2">print(password)
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span></span></span></code></pre></div>
</div>
<h2 id="ip-address-math">IP Address Math<a class="anchor" href="#ip-address-math" aria-label="Link to “IP Address Math”">#</a></h2>
<p>Check if an address is in a subnet:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 -c <span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">from ipaddress import ip_address, ip_network
</span></span></span><span class="line"><span class="cl"><span class="s2">net = ip_network(&#39;10.0.0.0/24&#39;)
</span></span></span><span class="line"><span class="cl"><span class="s2">print(ip_address(&#39;10.0.0.42&#39;) in net)  # True
</span></span></span><span class="line"><span class="cl"><span class="s2">print(ip_address(&#39;10.14.0.1&#39;) in net)    # False
</span></span></span><span class="line"><span class="cl"><span class="s2">print(f&#39;Network: {net.network_address}&#39;)
</span></span></span><span class="line"><span class="cl"><span class="s2">print(f&#39;Broadcast: {net.broadcast_address}&#39;)
</span></span></span><span class="line"><span class="cl"><span class="s2">print(f&#39;Hosts: {net.num_addresses - 2}&#39;)
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span></span></span></code></pre></div>
</div>
<p>The standard library covers most sysadmin needs without pip. <code>json</code>, <code>csv</code>, <code>http.server</code>, <code>ipaddress</code>, <code>secrets</code>, <code>urllib</code>, <code>base64</code>, <code>socket</code>. It is all there. When the bash one-liner turns into a bash five-liner with nested quotes and escaped dollars signs, switch to Python. Your future self will thank you.</p>
]]></content:encoded></item><item><title>Writing systemd Services That Actually Work</title><link>https://articles.wleeaf.dev/posts/systemd-services-from-scratch/</link><pubDate>Wed, 01 Apr 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/systemd-services-from-scratch/</guid><description>Unit files, restart policies, environment variables, and journalctl. A practical guide to running your own daemons.</description><category>linux</category><category>systemd</category><category>infrastructure</category><content:encoded><![CDATA[<p>At some point you write a script or a server that needs to run forever. You could use screen, tmux, nohup, or a cron <code>@reboot</code> hack. Or you could write a systemd service and get automatic restarts, logging, resource limits, and dependency management for free.</p>
<h2 id="the-minimal-service">The Minimal Service<a class="anchor" href="#the-minimal-service" aria-label="Link to “The Minimal Service”">#</a></h2>
<p>Create <code>/etc/systemd/system/myapp.service</code>:</p>
<div class="code">
<div class="code-head"><span class="code-lang">ini</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">My Application</span>
</span></span><span class="line"><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">network.target</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">simple</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/bin/node /opt/myapp/server.js</span>
</span></span><span class="line"><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">unless-stopped</span>
</span></span><span class="line"><span class="cl"><span class="na">RestartSec</span><span class="o">=</span><span class="s">5</span>
</span></span><span class="line"><span class="cl"><span class="na">WorkingDirectory</span><span class="o">=</span><span class="s">/opt/myapp</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">multi-user.target</span></span></span></code></pre></div>
</div>
<p>Then:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl daemon-reload
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> myapp
</span></span><span class="line"><span class="cl">sudo systemctl start myapp</span></span></code></pre></div>
</div>
<p>That is it. The app starts on boot, restarts on crash, and logs to journald.</p>
<h2 id="service-types">Service Types<a class="anchor" href="#service-types" aria-label="Link to “Service Types”">#</a></h2>
<p><code>Type=simple</code> is correct for most things. The process you start is the service. But there are others:</p>
<div class="code">
<div class="code-head"><span class="code-lang">ini</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># App forks into background (like old-school daemons)</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">forking</span>
</span></span><span class="line"><span class="cl"><span class="na">PIDFile</span><span class="o">=</span><span class="s">/var/run/myapp.pid</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># App signals readiness via sd_notify</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">notify</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># App is a one-shot script, not a long-running process</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="cl"><span class="na">RemainAfterExit</span><span class="o">=</span><span class="s">yes</span></span></span></code></pre></div>
</div>
<p>Use <code>simple</code> unless you have a specific reason not to. <code>oneshot</code> is useful for startup scripts that need to run once.</p>
<h2 id="environment-variables">Environment Variables<a class="anchor" href="#environment-variables" aria-label="Link to “Environment Variables”">#</a></h2>
<p>Three ways to pass them:</p>
<div class="code">
<div class="code-head"><span class="code-lang">ini</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># Inline (good for a few vars)</span>
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">PORT=3000</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">NODE_ENV=production</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># From a file (good for secrets)</span>
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">EnvironmentFile</span><span class="o">=</span><span class="s">/opt/myapp/.env</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Multiple files</span>
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">EnvironmentFile</span><span class="o">=</span><span class="s">/opt/myapp/.env</span>
</span></span><span class="line"><span class="cl"><span class="na">EnvironmentFile</span><span class="o">=</span><span class="s">/opt/myapp/.env.local</span></span></span></code></pre></div>
</div>
<p>The <code>EnvironmentFile</code> approach keeps secrets out of the unit file. The unit file itself might be in version control or readable by any user. The <code>.env</code> file can be locked down with permissions.</p>
<h2 id="running-as-a-non-root-user">Running As a Non-Root User<a class="anchor" href="#running-as-a-non-root-user" aria-label="Link to “Running As a Non-Root User”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">ini</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">User</span><span class="o">=</span><span class="s">myapp</span>
</span></span><span class="line"><span class="cl"><span class="na">Group</span><span class="o">=</span><span class="s">myapp</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/bin/node /opt/myapp/server.js</span></span></span></code></pre></div>
</div>
<p>Create the user first:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo useradd -r -s /usr/sbin/nologin -d /opt/myapp myapp
</span></span><span class="line"><span class="cl">sudo chown -R myapp:myapp /opt/myapp</span></span></code></pre></div>
</div>
<p>The <code>-r</code> flag creates a system user. <code>-s /usr/sbin/nologin</code> prevents interactive login.</p>
<h2 id="resource-limits">Resource Limits<a class="anchor" href="#resource-limits" aria-label="Link to “Resource Limits”">#</a></h2>
<p>Prevent a runaway process from eating all your RAM or CPU:</p>
<div class="code">
<div class="code-head"><span class="code-lang">ini</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">MemoryMax</span><span class="o">=</span><span class="s">512M</span>
</span></span><span class="line"><span class="cl"><span class="na">CPUQuota</span><span class="o">=</span><span class="s">50%</span>
</span></span><span class="line"><span class="cl"><span class="na">TasksMax</span><span class="o">=</span><span class="s">100</span>
</span></span><span class="line"><span class="cl"><span class="na">LimitNOFILE</span><span class="o">=</span><span class="s">65535</span></span></span></code></pre></div>
</div>
<p><code>MemoryMax</code> kills the process if it exceeds the limit. <code>CPUQuota</code> throttles but does not kill. <code>TasksMax</code> limits the number of threads and child processes. <code>LimitNOFILE</code> sets the open file descriptor limit.</p>
<h2 id="restart-policies">Restart Policies<a class="anchor" href="#restart-policies" aria-label="Link to “Restart Policies”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">ini</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># Restart on any exit (crash, signal, clean exit)</span>
</span></span><span class="line"><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">always</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Restart on failure only (non-zero exit, signal)</span>
</span></span><span class="line"><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">on-failure</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Restart unless you explicitly stopped it</span>
</span></span><span class="line"><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">unless-stopped</span></span></span></code></pre></div>
</div>
<p>I use <code>unless-stopped</code> for everything. It restarts on crashes but stays down when I manually stop the service for debugging.</p>
<p>Add rate limiting to prevent restart loops:</p>
<div class="code">
<div class="code-head"><span class="code-lang">ini</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">RestartSec</span><span class="o">=</span><span class="s">5</span>
</span></span><span class="line"><span class="cl"><span class="na">StartLimitIntervalSec</span><span class="o">=</span><span class="s">60</span>
</span></span><span class="line"><span class="cl"><span class="na">StartLimitBurst</span><span class="o">=</span><span class="s">5</span></span></span></code></pre></div>
</div>
<p>This allows 5 restarts within 60 seconds. After that, the service enters a failed state and stops trying. Check what happened with <code>journalctl -u myapp</code>.</p>
<h2 id="logging">Logging<a class="anchor" href="#logging" aria-label="Link to “Logging”">#</a></h2>
<p>systemd captures stdout and stderr automatically:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># View logs</span>
</span></span><span class="line"><span class="cl">journalctl -u myapp
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Follow in real time</span>
</span></span><span class="line"><span class="cl">journalctl -u myapp -f
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Last 50 lines</span>
</span></span><span class="line"><span class="cl">journalctl -u myapp -n <span class="m">50</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Since last boot</span>
</span></span><span class="line"><span class="cl">journalctl -u myapp -b
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># JSON output for parsing</span>
</span></span><span class="line"><span class="cl">journalctl -u myapp -o json-pretty</span></span></code></pre></div>
</div>
<p>No log rotation config needed. journald handles it automatically based on disk space.</p>
<h2 id="dependencies-and-ordering">Dependencies and Ordering<a class="anchor" href="#dependencies-and-ordering" aria-label="Link to “Dependencies and Ordering”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">ini</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">My App</span>
</span></span><span class="line"><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">network.target postgresql.service</span>
</span></span><span class="line"><span class="cl"><span class="na">Requires</span><span class="o">=</span><span class="s">postgresql.service</span></span></span></code></pre></div>
</div>
<p><code>After</code> controls startup order. <code>Requires</code> means if PostgreSQL stops, myapp stops too. Use <code>Wants</code> instead of <code>Requires</code> for soft dependencies where your app can handle the other service being down.</p>
<h2 id="pre-and-post-commands">Pre and Post Commands<a class="anchor" href="#pre-and-post-commands" aria-label="Link to “Pre and Post Commands”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">ini</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStartPre</span><span class="o">=</span><span class="s">/opt/myapp/migrate.sh</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/bin/node /opt/myapp/server.js</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStartPost</span><span class="o">=</span><span class="s">/usr/bin/curl -s http://localhost:3000/health</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStopPost</span><span class="o">=</span><span class="s">/opt/myapp/cleanup.sh</span></span></span></code></pre></div>
</div>
<p><code>ExecStartPre</code> runs before the main process. Good for database migrations or config validation. If it fails, the service does not start.</p>
<h2 id="debugging">Debugging<a class="anchor" href="#debugging" aria-label="Link to “Debugging”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check service status</span>
</span></span><span class="line"><span class="cl">systemctl status myapp
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># View full logs on failure</span>
</span></span><span class="line"><span class="cl">journalctl -u myapp -e --no-pager
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check the actual unit file being used</span>
</span></span><span class="line"><span class="cl">systemctl cat myapp
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Verify unit file syntax</span>
</span></span><span class="line"><span class="cl">systemd-analyze verify myapp.service</span></span></code></pre></div>
</div>
<p>The <code>systemd-analyze verify</code> command catches syntax errors without starting anything. Use it before reloading.</p>
<p>systemd is verbose and opinionated, but once you get past the initial learning curve, it handles all the edge cases that screen and nohup do not. Crashes, reboots, logging, resource limits, dependencies. Write the unit file once and forget about it.</p>
]]></content:encoded></item><item><title>Debugging Running Docker Containers Without Restarting Them</title><link>https://articles.wleeaf.dev/posts/docker-debugging-running-containers/</link><pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/docker-debugging-running-containers/</guid><description>exec, logs, inspect, and nsenter. Four tools for figuring out what is happening inside a container without touching it.</description><category>docker</category><category>debugging</category><category>linux</category><content:encoded><![CDATA[<p>Something is wrong with your container. It is running but not behaving. You could rebuild and redeploy, but that might hide the problem. Here is how to investigate a running container without restarting it, using <code>docker exec</code>, <code>docker logs</code>, <code>docker inspect</code>, <code>docker cp</code>, <code>nsenter</code> and <code>docker diff</code>.</p>
<h2 id="docker-exec">docker exec<a class="anchor" href="#docker-exec" aria-label="Link to “docker exec”">#</a></h2>
<p>The obvious one. Drop into a shell:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker <span class="nb">exec</span> -it mycontainer sh</span></span></code></pre></div>
</div>
<p>Use <code>sh</code> not <code>bash</code> because Alpine images do not have bash. Once inside, you can poke around:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check what processes are running</span>
</span></span><span class="line"><span class="cl">ps aux
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Look at environment variables</span>
</span></span><span class="line"><span class="cl">env <span class="p">|</span> sort
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Test network connectivity</span>
</span></span><span class="line"><span class="cl">wget -qO- http://other-service:3000/health
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check disk usage</span>
</span></span><span class="line"><span class="cl">df -h</span></span></code></pre></div>
</div>
<p>But what if the container has no shell? Minimal images built <code>FROM scratch</code> or distroless images strip everything out.</p>
<h2 id="docker-logs">docker logs<a class="anchor" href="#docker-logs" aria-label="Link to “docker logs”">#</a></h2>
<p>Before exec-ing into anything, check the logs:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Last 100 lines</span>
</span></span><span class="line"><span class="cl">docker logs --tail <span class="m">100</span> mycontainer
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Follow in real time</span>
</span></span><span class="line"><span class="cl">docker logs -f mycontainer
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Logs since a specific time</span>
</span></span><span class="line"><span class="cl">docker logs --since 2026-03-29T10:00:00 mycontainer
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># With timestamps</span>
</span></span><span class="line"><span class="cl">docker logs -t mycontainer</span></span></code></pre></div>
</div>
<p>The <code>--since</code> flag is underrated. When a container has been running for weeks, you do not want to scroll through megabytes of old logs.</p>
<h2 id="docker-inspect">docker inspect<a class="anchor" href="#docker-inspect" aria-label="Link to “docker inspect”">#</a></h2>
<p>This tells you everything about the container&rsquo;s configuration:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Full JSON dump</span>
</span></span><span class="line"><span class="cl">docker inspect mycontainer
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Just the IP address</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}&#39;</span> mycontainer
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Environment variables</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{json .Config.Env}}&#39;</span> mycontainer <span class="p">|</span> jq .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Mount points</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{json .Mounts}}&#39;</span> mycontainer <span class="p">|</span> jq .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Port mappings</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{json .NetworkSettings.Ports}}&#39;</span> mycontainer <span class="p">|</span> jq .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Health check status</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{json .State.Health}}&#39;</span> mycontainer <span class="p">|</span> jq .</span></span></code></pre></div>
</div>
<h2 id="docker-cp">docker cp<a class="anchor" href="#docker-cp" aria-label="Link to “docker cp”">#</a></h2>
<p>Copy files in or out of a running container:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Copy a config file out for inspection</span>
</span></span><span class="line"><span class="cl">docker cp mycontainer:/etc/nginx/nginx.conf ./nginx.conf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Copy a fix in</span>
</span></span><span class="line"><span class="cl">docker cp ./fixed-config.json mycontainer:/app/config.json</span></span></code></pre></div>
</div>
<p>This works even if the container has no shell or text editor.</p>
<h2 id="nsenter-the-nuclear-option">nsenter: The Nuclear Option<a class="anchor" href="#nsenter-the-nuclear-option" aria-label="Link to “nsenter: The Nuclear Option”">#</a></h2>
<p>When <code>docker exec</code> is not enough or the container has no shell, <code>nsenter</code> lets you enter the container&rsquo;s namespaces from the host:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Get the container&#39;s PID</span>
</span></span><span class="line"><span class="cl"><span class="nv">PID</span><span class="o">=</span><span class="k">$(</span>docker inspect -f <span class="s1">&#39;{{.State.Pid}}&#39;</span> mycontainer<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Enter its network namespace</span>
</span></span><span class="line"><span class="cl">sudo nsenter -t <span class="nv">$PID</span> -n ss -tlnp
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Enter all namespaces (like exec but from the host)</span>
</span></span><span class="line"><span class="cl">sudo nsenter -t <span class="nv">$PID</span> -m -u -i -n -p -- /bin/sh</span></span></code></pre></div>
</div>
<p>This is useful for network debugging. You can run <code>tcpdump</code>, <code>ss</code>, or <code>ip</code> inside the container&rsquo;s network namespace even if those tools are not installed in the container.</p>
<h2 id="docker-diff">docker diff<a class="anchor" href="#docker-diff" aria-label="Link to “docker diff”">#</a></h2>
<p>See what files changed since the container started:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker diff mycontainer</span></span></code></pre></div>
</div>
<p>Output looks like:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>C /app
A /app/data/cache.db
C /tmp
A /tmp/something-unexpected</code></pre>
</div>
<p><code>A</code> means added, <code>C</code> means changed, <code>D</code> means deleted. This is great for finding containers that are writing to unexpected places.</p>
<h2 id="debugging-network-issues">Debugging Network Issues<a class="anchor" href="#debugging-network-issues" aria-label="Link to “Debugging Network Issues”">#</a></h2>
<p>When containers cannot talk to each other:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check which networks a container is on</span>
</span></span><span class="line"><span class="cl">docker inspect mycontainer -f <span class="s1">&#39;{{json .NetworkSettings.Networks}}&#39;</span> <span class="p">|</span> jq <span class="s1">&#39;keys&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Ping from one container to another</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> web ping -c <span class="m">3</span> api
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check DNS resolution</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> web nslookup api
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># List all containers on a network</span>
</span></span><span class="line"><span class="cl">docker network inspect mynetwork -f <span class="s1">&#39;{{range .Containers}}{{.Name}} {{.IPv4Address}}{{end}}&#39;</span></span></span></code></pre></div>
</div>
<h2 id="one-liner-health-checks">One-Liner Health Checks<a class="anchor" href="#one-liner-health-checks" aria-label="Link to “One-Liner Health Checks”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Which containers are unhealthy?</span>
</span></span><span class="line"><span class="cl">docker ps --filter <span class="nv">health</span><span class="o">=</span>unhealthy --format <span class="s1">&#39;{{.Names}}: {{.Status}}&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Which containers restarted recently?</span>
</span></span><span class="line"><span class="cl">docker ps --format <span class="s1">&#39;{{.Names}}: {{.Status}}&#39;</span> <span class="p">|</span> grep -i restarting
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Resource usage snapshot</span>
</span></span><span class="line"><span class="cl">docker stats --no-stream --format <span class="s1">&#39;table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}&#39;</span></span></span></code></pre></div>
</div>
<p>The instinct to restart a broken container is strong. But nine times out of ten, the information you need to fix the actual problem is sitting right there in the logs, the inspect output, or the filesystem diff. Restarting just resets the evidence.</p>
]]></content:encoded></item><item><title>Bypassing ISP Deep Packet Inspection on Linux</title><link>https://articles.wleeaf.dev/posts/dpi-bypass-linux/</link><pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/dpi-bypass-linux/</guid><description>When your ISP blocks websites at the packet level, here is how to get around it without a full VPN.</description><category>networking</category><category>privacy</category><category>linux</category><content:encoded><![CDATA[<p>My ISP does deep packet inspection. Not the basic DNS-level blocking that you can get around by switching to a different resolver. Actual packet inspection that looks at TLS handshakes and blocks connections to certain domains based on the SNI field in the ClientHello.</p>
<p>This means changing your DNS server does nothing. The ISP sees the domain name in the TLS handshake before the connection is encrypted and drops the packet. You need a different approach. Mine has two parts: zapret, which splits up the TLS ClientHello so the DPI system cannot read the SNI field in time, and DNS-over-TLS, so DNS queries stop travelling in plain text.</p>
<h2 id="how-dpi-blocking-works">How DPI Blocking Works<a class="anchor" href="#how-dpi-blocking-works" aria-label="Link to “How DPI Blocking Works”">#</a></h2>
<p>When you connect to a website over HTTPS, the first thing your browser sends is a TLS ClientHello message. This message is not encrypted. It contains the Server Name Indication field, which tells the server which domain you are trying to reach. This is necessary because multiple domains can share the same IP address, and the server needs to know which certificate to present.</p>
<p>Your ISP inspects this field. If the domain matches their blocklist, they inject a TCP RST packet or simply drop the connection. From your perspective, the site just times out or you get a connection reset.</p>
<h2 id="the-zapret-approach">The Zapret Approach<a class="anchor" href="#the-zapret-approach" aria-label="Link to “The Zapret Approach”">#</a></h2>
<p>There is a tool called zapret that manipulates outgoing packets to confuse DPI systems. It does not encrypt anything or route traffic through a proxy. Instead, it exploits the fact that DPI systems have to make real-time decisions about packets and often take shortcuts.</p>
<p>The basic techniques include fragmenting the ClientHello across multiple TCP segments, adding padding to push the SNI field into a second packet, or modifying TCP flags in ways that confuse stateful inspection engines but are tolerated by the destination server.</p>
<p>I run zapret as a systemd service. It hooks into the outgoing packet flow using nfqueue and modifies specific packets before they leave my machine. The destination server receives the same data, just split up differently, and handles it fine. The ISP DPI system sees fragments that it cannot reassemble quickly enough and lets them through.</p>
<h2 id="dns-over-tls-complement">DNS-over-TLS Complement<a class="anchor" href="#dns-over-tls-complement" aria-label="Link to “DNS-over-TLS Complement”">#</a></h2>
<p>Zapret handles the TLS handshake problem, but DNS queries can still leak information. Even if the ISP cannot block the connection, they can see which domains you are resolving and log that data.</p>
<p>I force all DNS through DNS-over-TLS using systemd-resolved. The configuration tells resolved to only use TLS-encrypted DNS and to require valid certificates from the upstream resolver. I use Cloudflare and Google as upstream servers since both support DNS-over-TLS.</p>
<p>The resolved config looks straightforward. You set the DNS servers and tell it to require encrypted transport. After restarting the service, all DNS queries from the machine go through TLS on port 853 instead of plaintext on port 53.</p>
<h2 id="the-vpn-conflict">The VPN Conflict<a class="anchor" href="#the-vpn-conflict" aria-label="Link to “The VPN Conflict”">#</a></h2>
<p>This setup conflicts with split-tunnel VPN configurations. When the VPN is active, DNS queries for private services need to go to the VPN DNS server over the tunnel. But the DPI bypass forces all DNS through TLS, which means queries to the VPN DNS server also try to use TLS.</p>
<p>The VPN DNS server is just dnsmasq. It does not speak TLS. So the queries fail and private domain names do not resolve.</p>
<p>The fix is a PostUp hook in the WireGuard configuration that disables DNS-over-TLS specifically on the VPN interface. The system still uses DoT for all other DNS queries, but queries routed through the VPN go as plain DNS over the encrypted tunnel. Since the tunnel itself is encrypted, this is fine from a privacy perspective.</p>
<p>Getting this right took some trial and error. The symptoms were confusing: public websites worked, VPN connected successfully, but private subdomains just would not resolve. Once I traced it to the DoT setting, the fix was one line.</p>
<h2 id="performance-impact">Performance Impact<a class="anchor" href="#performance-impact" aria-label="Link to “Performance Impact”">#</a></h2>
<p>Zapret adds essentially zero latency. The packet manipulation happens in kernel space and takes microseconds. DNS-over-TLS adds a small amount of latency to the first query for each domain, maybe 10 to 20 milliseconds. Subsequent queries are cached by resolved so you do not notice it.</p>
<p>I have been running this setup for months and have never noticed any performance impact in normal usage. Streaming, gaming, and large downloads all work at full speed. The only time I notice it is when I forget to check if the service is running and a blocked site fails to load.</p>
<h2 id="reliability">Reliability<a class="anchor" href="#reliability" aria-label="Link to “Reliability”">#</a></h2>
<p>The DPI bypass works about 99 percent of the time. Occasionally the ISP updates their inspection rules and a specific technique stops working. Zapret has multiple strategies and you can configure fallbacks. Usually updating to the latest version fixes any regressions within a day or two.</p>
<p>The DNS-over-TLS setup is rock solid. I have never had it fail. Systemd-resolved handles reconnections and failovers between upstream servers automatically.</p>
<h2 id="legal-and-ethical-notes">Legal and Ethical Notes<a class="anchor" href="#legal-and-ethical-notes" aria-label="Link to “Legal and Ethical Notes”">#</a></h2>
<p>This is not about accessing illegal content. My ISP blocks legitimate websites, often because a previous user of the domain or IP address did something that triggered a broad block. The blocking is opaque, there is no notification, and there is no process for getting a site unblocked.</p>
<p>Bypassing DPI to access legal content that your ISP has incorrectly or broadly blocked is a reasonable thing to do. The tools involved are all open source and widely used. Nobody has ever gotten in trouble for running zapret on their own machine.</p>
<p>If you are in a situation where your ISP does DPI blocking, this setup gives you back control over your own internet connection without the overhead and privacy implications of routing all your traffic through a third-party VPN.</p>
]]></content:encoded></item><item><title>jq Is the SQL of JSON</title><link>https://articles.wleeaf.dev/posts/jq-command-line-json/</link><pubDate>Fri, 27 Mar 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/jq-command-line-json/</guid><description>Every API returns JSON now. jq turns your terminal into a query engine for it.</description><category>cli</category><category>json</category><category>linux</category><content:encoded><![CDATA[<p>Every API you talk to returns JSON. Every config file is JSON. Every log pipeline emits JSON. And yet most people still pipe <code>curl</code> output through <code>grep</code> and pray.</p>
<p>jq is the tool that makes JSON usable from the command line. Once you learn the basics, you will never go back.</p>
<h2 id="basics">Basics<a class="anchor" href="#basics" aria-label="Link to “Basics”">#</a></h2>
<p>Pretty-print some JSON:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://api.github.com/users/torvalds <span class="p">|</span> jq .</span></span></code></pre></div>
</div>
<p>Extract a single field:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;{&#34;name&#34;: &#34;Linus&#34;, &#34;repos&#34;: 5}&#39;</span> <span class="p">|</span> jq .name
</span></span><span class="line"><span class="cl"><span class="c1"># &#34;Linus&#34;</span></span></span></code></pre></div>
</div>
<p>Drop the quotes with <code>-r</code>:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;{&#34;name&#34;: &#34;Linus&#34;}&#39;</span> <span class="p">|</span> jq -r .name
</span></span><span class="line"><span class="cl"><span class="c1"># Linus</span></span></span></code></pre></div>
</div>
<h2 id="arrays">Arrays<a class="anchor" href="#arrays" aria-label="Link to “Arrays”">#</a></h2>
<p>Get the first element:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;[1, 2, 3]&#39;</span> <span class="p">|</span> jq <span class="s1">&#39;.[0]&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 1</span></span></span></code></pre></div>
</div>
<p>Iterate over all elements:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;[{&#34;name&#34;: &#34;a&#34;}, {&#34;name&#34;: &#34;b&#34;}]&#39;</span> <span class="p">|</span> jq <span class="s1">&#39;.[].name&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># &#34;a&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># &#34;b&#34;</span></span></span></code></pre></div>
</div>
<h2 id="filtering">Filtering<a class="anchor" href="#filtering" aria-label="Link to “Filtering”">#</a></h2>
<p>This is where it gets powerful. Say you have a list of GitHub repos:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://api.github.com/users/torvalds/repos <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  jq <span class="s1">&#39;[.[] | select(.stargazers_count &gt; 1000)] | length&#39;</span></span></span></code></pre></div>
</div>
<p>That filters repos with more than 1000 stars and counts them.</p>
<h2 id="constructing-objects">Constructing Objects<a class="anchor" href="#constructing-objects" aria-label="Link to “Constructing Objects”">#</a></h2>
<p>You can reshape JSON on the fly:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://api.github.com/users/torvalds/repos <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  jq <span class="s1">&#39;.[] | {name: .name, stars: .stargazers_count, lang: .language}&#39;</span></span></span></code></pre></div>
</div>
<p>This outputs a new object for each repo with only the fields you care about.</p>
<h2 id="real-world-parsing-docker">Real World: Parsing Docker<a class="anchor" href="#real-world-parsing-docker" aria-label="Link to “Real World: Parsing Docker”">#</a></h2>
<p>List container names and their memory usage:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker stats --no-stream --format <span class="s1">&#39;{{json .}}&#39;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  jq -r <span class="s1">&#39;[.Name, .MemUsage] | @tsv&#39;</span></span></span></code></pre></div>
</div>
<p>Find containers using more than 100MB:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker stats --no-stream --format <span class="s1">&#39;{{json .}}&#39;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  jq -r <span class="s1">&#39;select(.MemPerc | gsub(&#34;%&#34;; &#34;&#34;) | tonumber &gt; 1) | .Name&#39;</span></span></span></code></pre></div>
</div>
<h2 id="working-with-nested-data">Working With Nested Data<a class="anchor" href="#working-with-nested-data" aria-label="Link to “Working With Nested Data”">#</a></h2>
<p>Given deeply nested JSON:</p>
<div class="code">
<div class="code-head"><span class="code-lang">json</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;data&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;users&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span><span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nt">&#34;profile&#34;</span><span class="p">:</span> <span class="p">{</span><span class="nt">&#34;email&#34;</span><span class="p">:</span> <span class="s2">&#34;a@b.com&#34;</span><span class="p">}},</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span><span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="nt">&#34;profile&#34;</span><span class="p">:</span> <span class="p">{</span><span class="nt">&#34;email&#34;</span><span class="p">:</span> <span class="s2">&#34;c@d.com&#34;</span><span class="p">}}</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p>Extract all emails:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">jq <span class="s1">&#39;[.data.users[].profile.email]&#39;</span> data.json
</span></span><span class="line"><span class="cl"><span class="c1"># [&#34;a@b.com&#34;, &#34;c@d.com&#34;]</span></span></span></code></pre></div>
</div>
<h2 id="combining-with-other-tools">Combining With Other Tools<a class="anchor" href="#combining-with-other-tools" aria-label="Link to “Combining With Other Tools”">#</a></h2>
<p>jq plays well with the rest of the Unix toolkit:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Get all unique languages across repos</span>
</span></span><span class="line"><span class="cl">curl -s https://api.github.com/users/torvalds/repos <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  jq -r <span class="s1">&#39;.[].language // empty&#39;</span> <span class="p">|</span> sort -u
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Convert JSON array to lines for xargs</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;[&#34;file1.txt&#34;, &#34;file2.txt&#34;]&#39;</span> <span class="p">|</span> jq -r <span class="s1">&#39;.[]&#39;</span> <span class="p">|</span> xargs rm
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Merge multiple JSON files</span>
</span></span><span class="line"><span class="cl">jq -s <span class="s1">&#39;add&#39;</span> file1.json file2.json &gt; merged.json</span></span></code></pre></div>
</div>
<h2 id="the-cheat-sheet">The Cheat Sheet<a class="anchor" href="#the-cheat-sheet" aria-label="Link to “The Cheat Sheet”">#</a></h2>
<table>
  <thead>
      <tr>
          <th>Pattern</th>
          <th>What it does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>.field</code></td>
          <td>Get field</td>
      </tr>
      <tr>
          <td><code>.[]</code></td>
          <td>Iterate array</td>
      </tr>
      <tr>
          <td><code>select(cond)</code></td>
          <td>Filter</td>
      </tr>
      <tr>
          <td><code>map(expr)</code></td>
          <td>Transform array</td>
      </tr>
      <tr>
          <td><code>length</code></td>
          <td>Count</td>
      </tr>
      <tr>
          <td><code>keys</code></td>
          <td>Object keys</td>
      </tr>
      <tr>
          <td><code>to_entries</code></td>
          <td>Object to key-value pairs</td>
      </tr>
      <tr>
          <td><code>@csv</code></td>
          <td>Output as CSV</td>
      </tr>
      <tr>
          <td><code>@tsv</code></td>
          <td>Output as TSV</td>
      </tr>
      <tr>
          <td><code>//</code></td>
          <td>Default value</td>
      </tr>
  </tbody>
</table>
<p>jq has a learning curve, but it is one of those tools where the investment pays off within a week. Every time you find yourself writing a Python script just to parse some JSON, remember that jq probably does it in one line.</p>
]]></content:encoded></item><item><title>SSH Config Tricks I Use Every Day</title><link>https://articles.wleeaf.dev/posts/ssh-config-tricks/</link><pubDate>Thu, 26 Mar 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/ssh-config-tricks/</guid><description>Stop typing full hostnames and passwords. Your SSH config file is more powerful than you think.</description><category>ssh</category><category>linux</category><category>productivity</category><content:encoded><![CDATA[<p>I SSH into servers maybe thirty times a day. Without a proper SSH config, that would mean typing out usernames, hostnames, ports, and key paths every single time. Life is too short for that.</p>
<h2 id="the-basics">The Basics<a class="anchor" href="#the-basics" aria-label="Link to “The Basics”">#</a></h2>
<p>The SSH config lives at <code>~/.ssh/config</code>. Here is a minimal example:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>Host myserver
  HostName 203.0.113.50
  User root
  IdentityFile ~/.ssh/id_ed25519</code></pre>
</div>
<p>Now <code>ssh myserver</code> is all you need. But it gets better.</p>
<h2 id="jump-hosts">Jump Hosts<a class="anchor" href="#jump-hosts" aria-label="Link to “Jump Hosts”">#</a></h2>
<p>If you need to reach a server through a bastion:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>Host bastion
  HostName bastion.example.com
  User admin

Host internal
  HostName 10.0.1.50
  User deploy
  ProxyJump bastion</code></pre>
</div>
<p><code>ssh internal</code> automatically tunnels through the bastion. No manual port forwarding, no two-step login.</p>
<h2 id="wildcard-hosts">Wildcard Hosts<a class="anchor" href="#wildcard-hosts" aria-label="Link to “Wildcard Hosts”">#</a></h2>
<p>This is the one most people miss:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>Host *.prod
  User deploy
  IdentityFile ~/.ssh/prod_key
  StrictHostKeyChecking accept-new

Host web.prod
  HostName 10.0.1.10

Host api.prod
  HostName 10.0.1.11

Host db.prod
  HostName 10.0.1.12</code></pre>
</div>
<p>The wildcard block sets defaults for anything ending in <code>.prod</code>. Individual host blocks just add the hostname. You get consistent configuration without repetition.</p>
<h2 id="keep-connections-alive">Keep Connections Alive<a class="anchor" href="#keep-connections-alive" aria-label="Link to “Keep Connections Alive”">#</a></h2>
<p>Add this to your <code>Host *</code> block:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>Host *
  ServerAliveInterval 60
  ServerAliveCountMax 3
  AddKeysToAgent yes
  IdentitiesOnly yes</code></pre>
</div>
<p><code>ServerAliveInterval</code> sends a keepalive every 60 seconds. This stops firewalls and NATs from killing idle connections. <code>AddKeysToAgent</code> automatically loads keys into ssh-agent after first use.</p>
<h2 id="connection-multiplexing">Connection Multiplexing<a class="anchor" href="#connection-multiplexing" aria-label="Link to “Connection Multiplexing”">#</a></h2>
<p>This is the real speed trick:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>Host *
  ControlMaster auto
  ControlPath ~/.ssh/sockets/%r@%h-%p
  ControlPersist 600</code></pre>
</div>
<p>The first SSH connection to a host creates a socket. Subsequent connections to the same host reuse it. No new TCP handshake, no new key exchange. The second <code>ssh myserver</code> connects instantly.</p>
<p><code>ControlPersist 600</code> keeps the socket alive for 10 minutes after you disconnect. So if you SSH in, run a command, disconnect, and SSH back in within 10 minutes, the second connection is instant.</p>
<p>Create the socket directory first:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p ~/.ssh/sockets</span></span></code></pre></div>
</div>
<h2 id="port-forwarding-shortcuts">Port Forwarding Shortcuts<a class="anchor" href="#port-forwarding-shortcuts" aria-label="Link to “Port Forwarding Shortcuts”">#</a></h2>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>Host db-tunnel
  HostName myserver.com
  User admin
  LocalForward 5432 localhost:5432
  RequestTTY no
  ExitOnForwardFailure yes</code></pre>
</div>
<p>Now <code>ssh db-tunnel</code> opens a tunnel to the remote Postgres. No remembering the <code>-L</code> syntax. <code>ExitOnForwardFailure</code> means if the port is already in use, SSH exits instead of connecting without the tunnel.</p>
<h2 id="sshfp-for-host-verification">SSHFP for Host Verification<a class="anchor" href="#sshfp-for-host-verification" aria-label="Link to “SSHFP for Host Verification”">#</a></h2>
<p>Tired of the &ldquo;Are you sure you want to continue connecting?&rdquo; messages? If you control the DNS, add SSHFP records:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-keygen -r myserver.com</span></span></code></pre></div>
</div>
<p>This outputs DNS records you can add to your zone. Then set:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>Host *
  VerifyHostKeyDNS yes</code></pre>
</div>
<p>SSH verifies the host key against DNS automatically. No more blindly accepting fingerprints.</p>
<p>The SSH config file is probably the highest-leverage dotfile you can invest time in. Five minutes of setup saves thousands of keystrokes.</p>
]]></content:encoded></item><item><title>Bash Traps and the Art of Not Leaving a Mess</title><link>https://articles.wleeaf.dev/posts/bash-traps-error-handling/</link><pubDate>Wed, 25 Mar 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/bash-traps-error-handling/</guid><description>How trap, set -e, and a cleanup function can save you from half-finished deployments.</description><category>bash</category><category>linux</category><category>scripting</category><content:encoded><![CDATA[<p>I once ran a deploy script that got killed halfway through an rsync. The remote server had half the files from the new version and half from the old one. The site was broken for twenty minutes while I figured out what happened.</p>
<p>That is when I started using traps. <code>trap cleanup EXIT</code> runs a cleanup function however the script ends: a normal exit, an error or a Ctrl-C.</p>
<h2 id="the-problem">The Problem<a class="anchor" href="#the-problem" aria-label="Link to “The Problem”">#</a></h2>
<p>Bash scripts are fragile by default. If a command fails, the script keeps going. If you hit Ctrl-C, whatever state you left things in is where they stay. If your SSH connection drops mid-script, same thing.</p>
<p>Most people know about <code>set -e</code> which exits on the first error. But that only covers one failure mode. What about cleanup?</p>
<h2 id="enter-trap">Enter trap<a class="anchor" href="#enter-trap" aria-label="Link to “Enter trap”">#</a></h2>
<p>The <code>trap</code> builtin lets you run a command when the script receives a signal or exits:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">TMPDIR</span><span class="o">=</span><span class="k">$(</span>mktemp -d<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">cleanup<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Cleaning up </span><span class="nv">$TMPDIR</span><span class="s2">...&#34;</span>
</span></span><span class="line"><span class="cl">  rm -rf <span class="s2">&#34;</span><span class="nv">$TMPDIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> cleanup EXIT
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Your actual work here</span>
</span></span><span class="line"><span class="cl">cp important-files <span class="s2">&#34;</span><span class="nv">$TMPDIR</span><span class="s2">/&#34;</span>
</span></span><span class="line"><span class="cl">process <span class="s2">&#34;</span><span class="nv">$TMPDIR</span><span class="s2">/important-files&#34;</span>
</span></span><span class="line"><span class="cl">mv <span class="s2">&#34;</span><span class="nv">$TMPDIR</span><span class="s2">/important-files&#34;</span> /final/destination/</span></span></code></pre></div>
</div>
<p>The <code>EXIT</code> trap fires no matter how the script ends. Normal exit, error, Ctrl-C, even <code>kill</code>. The temp directory always gets cleaned up.</p>
<h2 id="a-real-deploy-script">A Real Deploy Script<a class="anchor" href="#a-real-deploy-script" aria-label="Link to “A Real Deploy Script”">#</a></h2>
<p>Here is the pattern I use for deployments:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">SERVER</span><span class="o">=</span><span class="s2">&#34;myserver&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">REMOTE_DIR</span><span class="o">=</span><span class="s2">&#34;/opt/app&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">BACKUP_DIR</span><span class="o">=</span><span class="s2">&#34;/opt/app-backup-</span><span class="k">$(</span>date +%s<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">ROLLED_BACK</span><span class="o">=</span><span class="nb">false</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">rollback<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$ROLLED_BACK</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="nb">true</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span> <span class="k">return</span><span class="p">;</span> <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  <span class="nv">ROLLED_BACK</span><span class="o">=</span><span class="nb">true</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Deploy failed. Rolling back...&#34;</span>
</span></span><span class="line"><span class="cl">  ssh <span class="s2">&#34;</span><span class="nv">$SERVER</span><span class="s2">&#34;</span> <span class="s2">&#34;rm -rf </span><span class="nv">$REMOTE_DIR</span><span class="s2"> &amp;&amp; mv </span><span class="nv">$BACKUP_DIR</span><span class="s2"> </span><span class="nv">$REMOTE_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Rollback complete.&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> rollback ERR
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Backup current version</span>
</span></span><span class="line"><span class="cl">ssh <span class="s2">&#34;</span><span class="nv">$SERVER</span><span class="s2">&#34;</span> <span class="s2">&#34;cp -r </span><span class="nv">$REMOTE_DIR</span><span class="s2"> </span><span class="nv">$BACKUP_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Deploy</span>
</span></span><span class="line"><span class="cl">rsync -a --delete ./dist/ <span class="s2">&#34;</span><span class="nv">$SERVER</span><span class="s2">:</span><span class="nv">$REMOTE_DIR</span><span class="s2">/&#34;</span>
</span></span><span class="line"><span class="cl">ssh <span class="s2">&#34;</span><span class="nv">$SERVER</span><span class="s2">&#34;</span> <span class="s2">&#34;cd </span><span class="nv">$REMOTE_DIR</span><span class="s2"> &amp;&amp; docker compose up -d&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Success - remove backup</span>
</span></span><span class="line"><span class="cl">ssh <span class="s2">&#34;</span><span class="nv">$SERVER</span><span class="s2">&#34;</span> <span class="s2">&#34;rm -rf </span><span class="nv">$BACKUP_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Deploy complete.&#34;</span></span></span></code></pre></div>
</div>
<p>If anything after the backup fails, the old version gets restored automatically. No manual intervention, no half-broken state.</p>
<h2 id="set--euo-pipefail">set -euo pipefail<a class="anchor" href="#set--euo-pipefail" aria-label="Link to “set -euo pipefail”">#</a></h2>
<p>This trio should be at the top of every script you write:</p>
<ul>
<li><code>set -e</code> exits on any command failure</li>
<li><code>set -u</code> treats unset variables as errors instead of empty strings</li>
<li><code>set -o pipefail</code> makes pipes fail if any command in the chain fails, not just the last one</li>
</ul>
<p>Without <code>pipefail</code>, this silently succeeds even when <code>curl</code> fails:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl https://example.com/data <span class="p">|</span> jq .result
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Done&#34;</span></span></span></code></pre></div>
</div>
<p><code>jq</code> gets an empty input, outputs null, and the script continues. With <code>pipefail</code>, the whole pipe fails and <code>set -e</code> kills the script.</p>
<h2 id="trapping-multiple-signals">Trapping Multiple Signals<a class="anchor" href="#trapping-multiple-signals" aria-label="Link to “Trapping Multiple Signals”">#</a></h2>
<p>You can trap specific signals separately:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">trap</span> <span class="s2">&#34;echo Interrupted; cleanup; exit 130&#34;</span> INT
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> <span class="s2">&#34;echo Terminated; cleanup; exit 143&#34;</span> TERM
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> cleanup EXIT</span></span></code></pre></div>
</div>
<p>But usually just trapping <code>EXIT</code> is enough. It covers everything.</p>
<p>The key insight is that bash does not protect you from yourself. You have to opt into safety. But once you do, scripts become surprisingly reliable.</p>
]]></content:encoded></item></channel></rss>