<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xml" href="/feed.xlst"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Thomas Williams blog</title>
  <subtitle>Thomas Williams blog from Hobart, Tasmania.
</subtitle>

  <link href="https://thomasswilliams.github.io/atom.xml" rel="self"/>
  <link href="https://thomasswilliams.github.io/"/>
  <updated>2026-05-10T12:10:19+00:00</updated>
  <id>https://thomasswilliams.github.io</id>
  <author>
    <name>Thomas Williams</name>
  </author>
  <rights>This work licensed under a Creative Commons Attribution 4.0 International License.</rights>

  
    <entry>
      <title>Mermaid diagrams for Jekyll</title>
      <link href="https://thomasswilliams.github.io/general/2026/05/10/mermaid-diagrams-jekyll.html"/>
      <updated>2026-05-10T02:00:00+00:00</updated>
      <id>https://thomasswilliams.github.io/general/2026/05/10/mermaid-diagrams-jekyll</id>
      <content type="html">&lt;p&gt;Mermaid &lt;a href=&quot;https://mermaid.ai/&quot;&gt;https://mermaid.ai/&lt;/a&gt; is “Markdown inspired” diagrams as code. With the diagram defined as text, special/proprietary software isn’t needed to create or edit diagrams. The text defining the diagrams can be edited by anyone - so can be kept up-to-date. Diagrams can also be version controlled, like any other code.&lt;/p&gt;

&lt;p&gt;This matters because better diagrams improve documentation, and communication.&lt;/p&gt;

&lt;p&gt;Mermaid supports a lot of different diagram types (flowcharts, Gantt charts, mindmaps, and org charts, among others). Mermaid syntax is supported in Confluence, GitHub, VS Code, MediaWiki, and more.&lt;/p&gt;

&lt;p&gt;If you don’t want to fiddle with text, there’s online, free tools to visually create diagrams, which can then be exported to Mermaid syntax (though be aware, there’s perhaps not the level of fine-grained control that might be expected using afore-mentioned proprietary software like Visio or &lt;a href=&quot;https://app.diagrams.net/&quot;&gt;https://app.diagrams.net/&lt;/a&gt;, which is a trade-off for the simplicity of text).&lt;/p&gt;

&lt;p&gt;Here’s a simple example of a flowchart diagram in Mermaid. The code block below results in the diagram underneath:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;```mermaid
flowchart TD
  A([&quot;Normal text and unicode characters 👤 ★&quot;]) --&amp;gt; B(&quot;&amp;lt;strong&amp;gt;HTML&amp;lt;/strong&amp;gt;&amp;lt;br&amp;gt;&amp;lt;em&amp;gt;format&amp;lt;/em&amp;gt;&quot;)
  B -.-&amp;gt; C[&quot;`Or **Markdown** _format_`&quot;]
  D@{ shape: text, label: &quot;&amp;lt;small&amp;gt;See full syntax at &amp;lt;a href=https://mermaid.ai/open-source/syntax/flowchart.html&amp;gt;https://mermaid.ai/open-source/syntax/flowchart.html&amp;lt;a&amp;gt;&amp;lt;/small&amp;gt;&quot; }
  style A stroke-width:2px
  style C fill:#FFE97F,stroke:#fc0,stroke-dasharray:3,color:#333
```
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TD
  A([&quot;Normal text and unicode characters 👤 ★&quot;]) --&amp;gt; B(&quot;&amp;lt;strong&amp;gt;HTML&amp;lt;/strong&amp;gt;&amp;lt;br&amp;gt;&amp;lt;em&amp;gt;format&amp;lt;/em&amp;gt;&quot;)
  B -.-&amp;gt; C[&quot;`Or **Markdown** _format_`&quot;]
  D@{ shape: text, label: &quot;&amp;lt;small&amp;gt;See full syntax at &amp;lt;a href=https://mermaid.ai/open-source/syntax/flowchart.html&amp;gt;https://mermaid.ai/open-source/syntax/flowchart.html&amp;lt;a&amp;gt;&amp;lt;/small&amp;gt;&quot; }
  style A stroke-width:2px
  style C fill:#FFE97F,stroke:#fc0,stroke-dasharray:3,color:#333
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I use these sort of diagrams for quick, high-level system architecture. Rendered in a browser, text in the diagram is selectable and links can be clicked.&lt;/p&gt;

&lt;p&gt;Of course, there’s a lot more than can be done with regards to styling &amp;amp; font, layout, dark mode etc. In the flowchart above, I use single upper-case letters to refer to “nodes”, with the displayed text separate (which makes connecting, referencing and styling nodes easier).&lt;/p&gt;

&lt;p&gt;Some other useful Mermaid flowchart styling:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;dotted lines on nodes: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;style &amp;lt;node&amp;gt; stroke-dasharray:3&lt;/code&gt; &lt;em&gt;(or even dots and dashes, see &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/stroke-dasharray&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/stroke-dasharray&lt;/a&gt;)&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;node text color: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;style &amp;lt;node&amp;gt; color:&amp;lt;hex color code&amp;gt;&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;node background color: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;style &amp;lt;node&amp;gt; fill:&amp;lt;hex color code&amp;gt;&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;node border width: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;style &amp;lt;node&amp;gt; stroke-width:&amp;lt;number&amp;gt;&amp;lt;unit e.g. &quot;px&quot;, &quot;pt&quot;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s how to add Mermaid to a Jekyll blog, based on &lt;a href=&quot;https://stuff-things.net/2025/01/19/mermaid-diagramming-in-jekyll-in-2025/&quot;&gt;https://stuff-things.net/2025/01/19/mermaid-diagramming-in-jekyll-in-2025/&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;create a new file &lt;em&gt;mermaid.html&lt;/em&gt; in the &lt;em&gt;_includes&lt;/em&gt; directory&lt;/li&gt;
  &lt;li&gt;put the following code into &lt;em&gt;mermaid.html&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;script&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cm&quot;&gt;/* latest version of Mermaid diagrams
     adapted from https://stuff-things.net/2025/01/19/mermaid-diagramming-in-jekyll-in-2025/ */&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;mermaid&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;cm&quot;&gt;/* initialise Mermaid */&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;mermaid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;startOnLoad&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;theme&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;cm&quot;&gt;/* run Mermaid on the page, looking for code blocks with the class &apos;language-mermaid&apos; */&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;mermaid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;cm&quot;&gt;/* specify the CSS selector for Mermaid code blocks
       by default, Mermaid looks for elements with the class &apos;mermaid&apos;, but thanks to Jekyll there&apos;s
       &quot;language-&quot; in front when using triple-backtick code blocks */&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;.language-mermaid&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/script&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;add the following lines to your Jekyll HTML head template (mine is in a file called &lt;em&gt;head.html&lt;/em&gt;):&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- if &quot;mermaid&quot; flag is true, include snippet --&amp;gt;&lt;/span&gt;
{% if page.mermaid %}{% include mermaid.html %}{% endif %}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;in the front-matter for a post, add the following to enable Mermaid:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;mermaid&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With the above in place, you can define a Mermaid code block in a blog post, and it will be rendered as a diagram.&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <title>Ten years of blogging</title>
      <link href="https://thomasswilliams.github.io/general/2026/05/03/ten-years-of-blogging.html"/>
      <updated>2026-05-03T02:00:00+00:00</updated>
      <id>https://thomasswilliams.github.io/general/2026/05/03/ten-years-of-blogging</id>
      <content type="html">&lt;p&gt;Ten years ago, I started this blog.&lt;/p&gt;

&lt;p&gt;It’s my third or fourth blog - in 2010, I retired my personal blog &lt;a href=&quot;https://thomasswilliams.blogspot.com&quot;&gt;https://thomasswilliams.blogspot.com&lt;/a&gt; as well as my second technical blog on abandoned site “TheRuntime”, now only on &lt;a href=&quot;https://web.archive.org/web/20120319215339/http://theruntime.com/blogs/thomasswilliams/&quot;&gt;archive.org&lt;/a&gt; (side note: 2010 was a busy year, I started studying a Masters degree).&lt;/p&gt;

&lt;p&gt;In 2016 I started this blog with the aim to write at least one post a month.&lt;/p&gt;

&lt;p&gt;Ten years on, I’ve written nearly 100 posts. I took a break between 2023 and 2025 as work was super-busy. But I always planned on coming back and documenting what I’d learned, in the hope that it could be a reference point for me, outlasting specific jobs, as well as help others.&lt;/p&gt;

&lt;p&gt;The posts I’m most proud of are a series on learning R Markdown (Markdown and R code for data visualisation and interactive reports), a series on learning Leaflet (web-based map tiles with an Antarctic twist), my home networking evolution, and random developer &amp;amp; IT bits and pieces that reflect me - a curious learner.&lt;/p&gt;

&lt;p&gt;There’s also tons of DBA-related posts from the day-to-day operational level, to the long-term view and product ownership level, that mirror my career journey.&lt;/p&gt;

&lt;p&gt;I’m a firm believer that the act of writing is the best way to prove and &lt;u&gt;im&lt;/u&gt;prove writing quality. Writing as a skill is critical, and technology and computing are more exciting and dynamic than ever.&lt;/p&gt;

&lt;p&gt;I hope to continue - as I have over the past 25 years of working in IT - documenting/clarifying/reflecting on whatever platform, for another 10 years.&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <title>#30DayChartChallenge: Trend change detection in R, from Strava runs</title>
      <link href="https://thomasswilliams.github.io/development/r/2026/04/25/chart-challenge-strava-trend-change.html"/>
      <updated>2026-04-25T02:00:00+00:00</updated>
      <id>https://thomasswilliams.github.io/development/r/2026/04/25/chart-challenge-strava-trend-change</id>
      <content type="html">&lt;p&gt;This is my entry for the 2026 #30DayChartChallenge &lt;a href=&quot;https://bsky.app/profile/30daychartchall.bsky.social&quot;&gt;https://bsky.app/profile/30daychartchall.bsky.social&lt;/a&gt;; day 26 is all about trends.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/30-day-chart-challenge-2026.png&quot; alt=&quot;#30DayChartChallenge&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I wanted to understand: when do trends change? So, based on a hunch and using Strava data, I fired up RStudio and set to work (Claude and ChatGPT helped with the code, especially the Strava API stuff).&lt;/p&gt;

&lt;p&gt;The resulting R Markdown file and instructions are on GitHub, ready to plug in your Strava client ID and secret: &lt;a href=&quot;https://github.com/thomasswilliams/r-strava-2026&quot;&gt;https://github.com/thomasswilliams/r-strava-2026&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLDR: the R package “RegimeChange” provides detection of trend changes using a variety of approaches. I used the Pruned Exact Linear Time (PELT) default algorithm. Read on for the full story, or take a sneak peek at the chart, generated using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Plotly&lt;/code&gt; (the red horizontal lines are the trend changes):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/strava-5km-trend-change.png&quot; alt=&quot;Trend change detection using &amp;quot;RegimeChange&amp;quot; in R Markdown from my recent Strava runs&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I don’t record all my runs in Strava, but runs over the last few months have felt faster and I hoped the data would reflect this. After a slow patch with an injury in mid-2025, I upped my running regularity to twice weekly (now, three times) and didn’t have much of a break during the Aussie summer in January 2026.&lt;/p&gt;

&lt;p&gt;Back to the chart challenge: inspired by a Nicole Rennie post &lt;a href=&quot;https://nrennie.rbind.io/blog/2022-07-18-mapping-a-marathon-with-rstrava/&quot;&gt;https://nrennie.rbind.io/blog/2022-07-18-mapping-a-marathon-with-rstrava/&lt;/a&gt;, my first step was gathering and filtering the data from the Strava API for the last 2 years. The 2-year timeframe should give enough data points to identify trends. I’m using just the first 5KMs of runs which are at least 5KM long, marked as a “run” in Strava, and have a 5KM time not slower than 50 minutes (e.g. faster than a walk). Variables are defined at the top of the code; to follow along (with RStudio and a Strava client ID and secret, easy to obtain even for free Strava users), open the R Markdown file in RStudio, install any missing libraries, enter your Strava client ID and secret on lines 54 and 55, then click &lt;strong&gt;Knit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On the first knit you’ll be prompted to open a browser window and authenticate with Strava. Depending on your settings, the knitted HTML file will appear in RStudio’s viewer. It can then be opened in a web browser too.&lt;/p&gt;

&lt;p&gt;I found the “RegimeChange” package simple to use and well documented, following the example from &lt;a href=&quot;https://cran.r-project.org/web/packages/RegimeChange/vignettes/introduction.html&quot;&gt;https://cran.r-project.org/web/packages/RegimeChange/vignettes/introduction.html&lt;/a&gt;, with the default algorithm. The raw analysis from detecting trend changes is included in the HTML output. In my case, two distinct trend “segments” were detected - a mean of 8 minutes, 53 seconds per KM up to March 2026, and a mean of 7:51 per KM since then. Exciting progress! And matches with how I thought my pace had potentially improved.&lt;/p&gt;

&lt;p&gt;Over a few more iterations, I tidied up the chart, adding a moving average and tooltips, and also cached data from the Strava API to avoid hitting limits and speed up knitting (probably still needs work).&lt;/p&gt;

&lt;p&gt;I had fun participating this year’s #30DayChartChallenge. I’m interested in how trend detection might work for other datasets. Hopefully others can use my code to detect trend changes in their own Strava data.&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <title>Improving R linting (2026 update)</title>
      <link href="https://thomasswilliams.github.io/development/r/2026/04/15/linting-in-r-update-2026.html"/>
      <updated>2026-04-15T01:00:00+00:00</updated>
      <id>https://thomasswilliams.github.io/development/r/2026/04/15/linting-in-r-update-2026</id>
      <content type="html">&lt;p&gt;Linting helps catch potential issues in code before they cause problems, and helps keep coding style consistent, whether for yourself or as part of a team or project. Linting uncovers surprising issues: recently, I added lints which caught that I was passing the same parameter by name twice in a call - meaning one of the parameter values was silently ignored.&lt;/p&gt;

&lt;p&gt;Previously, using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lintr&lt;/code&gt; for R, I specified only lint rules I wanted (whitelist). Now, I include all lint rules, and then tune (or disable) rules I don’t care about (blacklist).&lt;/p&gt;

&lt;p&gt;This has the advantage that, if new rules are added, running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lintr&lt;/code&gt; would automatically includes the new rules.&lt;/p&gt;

&lt;p&gt;My lint configuration is below. It suits my style when coding in R: explicitly scoped function calls preceded by name of library (e.g. “DT::renderDataTable” over “renderDataTable”), libraries loaded at top of each script for portability and reproducibility, spaces instead of tabs, double quotes for strings, line lengths less than 120 characters for readability, and more.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lintr&lt;/code&gt; is pretty particular about lint config file formatting, and will error if the config file isn’t correct. Don’t forget to leave a blank line at the end of the file.&lt;/p&gt;

&lt;p&gt;To lint your R code from RStudio, install the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lintr&lt;/code&gt; package, create a config file &lt;em&gt;.lintr&lt;/em&gt; and add rules (or copy-paste my lint rules below), then run from the R console either &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lintr::lint(filename = &quot;&amp;lt;file name&amp;gt;.Rmd&quot;)&lt;/code&gt; or lint the whole directory with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lintr::lint_dir()&lt;/code&gt;. See the full &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lintr&lt;/code&gt; documentation at &lt;a href=&quot;https://lintr.r-lib.org/reference/&quot;&gt;https://lintr.r-lib.org/reference/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My &lt;em&gt;.lintr&lt;/em&gt; file as at 2026:&lt;/p&gt;

&lt;div class=&quot;language-R highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;linters&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;all_linters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# use all lint rules - customise some, and disable others (see below)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# see linter details at https://lintr.r-lib.org/reference/index.html&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# --------- customised linters ---------&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# line lengths should be less than 120 characters&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;line_length_linter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;120L&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# check for unused imports&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# allow packages referenced for namespace (e.g. pkg::function)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# always allow dplyr&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unused_import_linter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;allow_ns_usage&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;TRUE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;except_packages&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;dplyr&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;interpret_glue&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;TRUE&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# variables should be snake case&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;object_name_linter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;snake_case&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;SNAKE_CASE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# assignment should use specific operators (not equal sign)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assignment_linter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;operator&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;-&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;&amp;lt;-&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;%&amp;lt;&amp;gt;%&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# consistent pipes&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pipe_consistency_linter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;auto&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# ---------- disabled linters ----------&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# lint rules are disabled by setting them to NULL&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# allow &quot;stop&quot; calls, used in Rmd database connection errors&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;condition_call_linter&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# not cyclomatic complexity&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cyclocomp_linter&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# allow relative paths (expected by some of the javascript libraries)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nonportable_path_linter&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# allow &quot;paste&quot; and &quot;paste0&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;paste_linter&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# allow comparisons to empty strings for readability&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nzchar_linter&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# allow one pipe commands&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;one_call_pipe_linter&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# allow redundant calls to &quot;== TRUE&quot; for readability&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;redundant_equals_linter&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# turn off &quot;trailing blank lines&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trailing_blank_lines_linter&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# allow calls to &quot;library&quot; (helps with portability)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;undesirable_function_linter&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encoding&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;UTF-8&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Linting can be ignored for specific lines of code. For example, if there’s a line of code longer than 12 characters, add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;# nolint: line_len&lt;/code&gt; to the end to have it ignored by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lintr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Going further: linting for R has room to improve. By comparison, linting in Python is a little more mature, and some issues can be automatically fixed.&lt;/p&gt;

&lt;p&gt;There’s the possibility of better linting with LLMs, such as assessing metrics for code like complexity &amp;amp; readability, and perhaps new metrics that don’t exist yet. LLMs could (potentially) suggest better, newer ways to achieve the same outcomes, while still keeping consistency to a preferred style or team norms.&lt;/p&gt;

&lt;p&gt;In any case - good luck with linting and finding those tricky issues!&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <title>Different SSH keys for different purposes</title>
      <link href="https://thomasswilliams.github.io/general/2026/04/06/different-ssh-keys-for-different-purposes.html"/>
      <updated>2026-04-06T01:00:00+00:00</updated>
      <id>https://thomasswilliams.github.io/general/2026/04/06/different-ssh-keys-for-different-purposes</id>
      <content type="html">&lt;p&gt;I have different SSH keys for different purposes - one for GitHub, one for GitLab, Azure SSH, other servers etc. Keys for different accounts too - work or personal. Some Git hosting sites allow SSH keys for signing commits; a &lt;em&gt;signing&lt;/em&gt; SSH key can be different than the &lt;em&gt;authorisation&lt;/em&gt; SSH key.&lt;/p&gt;

&lt;div class=&quot;note&quot;&gt;
  &lt;p&gt;Reminder: an SSH key is basically you. If someone has access to an SSH private key, they can impersonate you, and systems can’t tell the difference. SSH keys aren’t the problem, they’re great - but need to be secured, and differentiated. This post is about the latter.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Having specific keys helps with exposure if a key is copied without your knowledge, or decrypted, as well as rotating. Rotating SSH keys is considered best practice, and having a key that’s limited in scope makes it easier to rotate over a single key that accesses, well, everything.&lt;/p&gt;

&lt;p&gt;Here’s how I do different SSH keys for different purposes, on Linux or Mac, as at early 2026.&lt;/p&gt;

&lt;p&gt;But first:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;naming&lt;/strong&gt;: I suggest naming the key with it’s purpose and the year created, as well as the algorithm&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;passphrase&lt;/strong&gt;: SSH keys should be protected with a passphrase, with the passphrase being different than your password&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;an-example-of-creating-a-specific-named-ssh-key&quot;&gt;An example of creating a specific, named SSH key&lt;/h2&gt;

&lt;p&gt;As an example, I’ll generate an SSH key for Codeberg (a new, free Git hosting site) by running the following command:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh-keygen &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; ed25519 &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ~/.ssh/codeberg_ed25519_thomas_williams_2026 &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; 100 &lt;span class=&quot;nt&quot;&gt;-C&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Thomas Williams &amp;lt;https://codeberg.org/thomasswilliams&amp;gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;the key will use the “ed25519” algorithm, a strong algorithm recommended by Codeberg, Mozilla, GitHub and others (see this excellent guide at &lt;a href=&quot;https://infosec.mozilla.org/guidelines/openssh&quot;&gt;https://infosec.mozilla.org/guidelines/openssh&lt;/a&gt;, as well as other references at the bottom of this post)&lt;/li&gt;
  &lt;li&gt;the key will have a meaningful name - don’t leave it blank which will use the default name, potentially overwriting/destroying existing keys&lt;/li&gt;
  &lt;li&gt;I use my profile URL as a comment (I could use my e-mail address)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-a&lt;/code&gt; is rounds of derivation, making the key harder to brute-force&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I enter a passphrase when prompted. If everything goes right, I now have two new files in my &lt;em&gt;~/.ssh&lt;/em&gt; directory “codeberg_ed25519_thomas_williams_2026” (private key) and “codeberg_ed25519_thomas_williams_2026.pub” (public key).&lt;/p&gt;

&lt;p&gt;As there are now multiple keys in my &lt;em&gt;~/.ssh&lt;/em&gt; directory, I can specify which site or server uses which key in the &lt;em&gt;~/.ssh/config&lt;/em&gt; file as per &lt;a href=&quot;https://stackoverflow.com/a/4246809&quot;&gt;https://stackoverflow.com/a/4246809&lt;/a&gt;. Doing this means I don’t need to pass the key file name to SSH commands:&lt;/p&gt;

&lt;div class=&quot;language-conf highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Codeberg
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Host&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codeberg&lt;/span&gt;.&lt;span class=&quot;n&quot;&gt;org&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# for the specified host
&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;HostName&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codeberg&lt;/span&gt;.&lt;span class=&quot;n&quot;&gt;org&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# always uses &quot;git&quot; user
&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;git&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# specify the key file for Codeberg
&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;IdentityFile&lt;/span&gt; ~/.&lt;span class=&quot;n&quot;&gt;ssh&lt;/span&gt;/&lt;span class=&quot;n&quot;&gt;codeberg_ed25519_thomas_williams_2026&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I also need to add the public key part of the new SSH key to Codeberg - see their help at &lt;a href=&quot;https://docs.codeberg.org/security/ssh-key/&quot;&gt;https://docs.codeberg.org/security/ssh-key/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once I’ve added the key to Codeberg, I can test that it works from a command line:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-T&lt;/span&gt; git@codeberg.org
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(If you have issues, make sure the &lt;em&gt;~/.ssh/config&lt;/em&gt; file is set to user read-write only, with no other access, by running: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chmod 600 ~/.ssh/config&lt;/code&gt;. It may be worth reviewing permissions on &lt;em&gt;~/.ssh&lt;/em&gt; and &lt;em&gt;~/.ssh/authorized_hosts&lt;/em&gt;, as incorrect permissions can be a common cause of login problems.)&lt;/p&gt;

&lt;p&gt;Because this key will be used for Git operations, I can also tell Git on a per-repo basis about the key, running the following from a command line in the repo directory:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git config core.sshCommand &lt;span class=&quot;s2&quot;&gt;&quot;ssh -i ~/.ssh/codeberg_ed25519_thomas_williams_2026&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Don’t forget to back up your SSH keys (password managers are a good option to do this to). Happy - and secure - SSH keying!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://unterwaditzer.net/2025/codeberg.html&quot;&gt;https://unterwaditzer.net/2025/codeberg.html&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.ssh.com/academy/secrets-management/password-key-rotation#how-often-should-you-rotate-passwords&quot;&gt;https://www.ssh.com/academy/secrets-management/password-key-rotation#how-often-should-you-rotate-passwords&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/transfer/latest/userguide/keyrotation.html&quot;&gt;https://docs.aws.amazon.com/transfer/latest/userguide/keyrotation.html&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.beyondtrust.com/blog/entry/ssh-key-management-overview-6-best-practices&quot;&gt;https://www.beyondtrust.com/blog/entry/ssh-key-management-overview-6-best-practices&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent&quot;&gt;https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://dev.to/sebos/ssh-authentication-key-rotation-why-and-how-to-expire-ssh-keys-3hfg&quot;&gt;https://dev.to/sebos/ssh-authentication-key-rotation-why-and-how-to-expire-ssh-keys-3hfg&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.brandonchecketts.com/archives/ssh-ed25519-key-best-practices-for-2025&quot;&gt;https://www.brandonchecketts.com/archives/ssh-ed25519-key-best-practices-for-2025&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    </entry>
  
    <entry>
      <title>Personalising a Linux command line (part 3)</title>
      <link href="https://thomasswilliams.github.io/general/2026/03/28/linux-personalise-part-3.html"/>
      <updated>2026-03-28T01:00:00+00:00</updated>
      <id>https://thomasswilliams.github.io/general/2026/03/28/linux-personalise-part-3</id>
      <content type="html">&lt;p&gt;Over the last few posts I’ve outlined what I do when I first get access to a Linux server, changing settings so I’m comfortable (see &lt;a href=&quot;/general/2026/03/15/linux-personalise-part-1.html&quot;&gt;part 1&lt;/a&gt;) and installing packages to become more productive (&lt;a href=&quot;/general/2026/03/22/linux-personalise-part-2.html&quot;&gt;part 2&lt;/a&gt;).&lt;/p&gt;

&lt;h2 id=&quot;oh-my-posh&quot;&gt;Oh My Posh&lt;/h2&gt;

&lt;p&gt;For an even better prompt, I’m a fan of “Oh My Posh” &lt;a href=&quot;https://ohmyposh.dev/&quot;&gt;https://ohmyposh.dev/&lt;/a&gt;. Once installed, my prompt looks like below, shown in a Python Git repository directory - with the Oh My Posh prompt displaying my username, the directory name, the Git branch and files needing commit, the current Python version (because I’m in a Python project directory with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt;), and lastly the time it took to complete the previous command and whether the command was successful:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/oh-my-posh-mar-2026.png&quot; alt=&quot;Oh My Posh in a Python directory&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Other sections of the command prompt are hidden and shown as needed, depending on whether you’re in a Python, Node or other directory. There’s probably further surprises in the prompt I haven’t yet come across.&lt;/p&gt;

&lt;p&gt;Here’s how I set it up:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;save the install shell script from &lt;a href=&quot;https://ohmyposh.dev/install.sh&quot;&gt;https://ohmyposh.dev/install.sh&lt;/a&gt; and run it in one command: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl -s https://ohmyposh.dev/install.sh | bash -s&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;install a nerd font with glyphs (for example, Meslo from &lt;a href=&quot;https://www.nerdfonts.com/font-downloads&quot;&gt;https://www.nerdfonts.com/font-downloads&lt;/a&gt;) on the computer running your terminal software&lt;/li&gt;
  &lt;li&gt;pick a theme: I downloaded “jandedobbeleer.omp.json” from &lt;a href=&quot;https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/jandedobbeleer.omp.json&quot;&gt;https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/jandedobbeleer.omp.json&lt;/a&gt; and saved it in my home folder (or you can copy-paste the contents from the web into a file with that name)&lt;/li&gt;
  &lt;li&gt;load Oh My Posh by adding to the end of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.bashrc&lt;/code&gt; (on Mac) or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.profile&lt;/code&gt; (Linux):&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# run oh-my-posh and load local config file &quot;jandedobbeleer.omp.json&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;eval&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;oh-my-posh init bash &lt;span class=&quot;nt&quot;&gt;--config&lt;/span&gt; ~/jandedobbeleer.omp.json&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;note&quot;&gt;
  &lt;p&gt;There are complete instructions for Windows, Mac and Linux - including all the myriad shells - at &lt;a href=&quot;https://ohmyposh.dev/docs/installation/customize&quot;&gt;https://ohmyposh.dev/docs/installation/customize&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Now when I next log in, my prompt is extra-functional and futuristic!&lt;/p&gt;

&lt;p&gt;(Hat tip to my original inspiration, in 2021, of Scott Hanselman’s post at &lt;a href=&quot;https://www.hanselman.com/blog/my-ultimate-powershell-prompt-with-oh-my-posh-and-the-windows-terminal&quot;&gt;https://www.hanselman.com/blog/my-ultimate-powershell-prompt-with-oh-my-posh-and-the-windows-terminal&lt;/a&gt;. Plus, there’s other alternatives too - such as Starship &lt;a href=&quot;https://starship.rs/&quot;&gt;https://starship.rs/&lt;/a&gt; or Oh My Zsh &lt;a href=&quot;https://ohmyz.sh/&quot;&gt;https://ohmyz.sh/&lt;/a&gt;.)&lt;/p&gt;

&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;/h2&gt;

&lt;p&gt;That’s it for now, for customising my Linux command line. A little effort can go a long way! Hopefully you found something of use; good luck with your own customisations, and let me know via a comment or on &lt;a href=&quot;https://bsky.app/profile/thomasswilliams.bsky.social&quot;&gt;Bluesky&lt;/a&gt; of anything you do to Linux servers you manage.&lt;/p&gt;

&lt;p&gt;The last two “Linux personalisations” things didn’t really fit with my other posts, so I’m tacking them onto the end:&lt;/p&gt;

&lt;h3 id=&quot;patching&quot;&gt;Patching&lt;/h3&gt;

&lt;p&gt;Patching on an Ubuntu Linux server happens in 2 stages:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;update sources: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo apt update&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;upgrade packages: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo apt upgrade -y&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To upgrade between minor versions (like I needed to do recently, upgrading from Ubuntu 25.04 to 25.10), run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo do-release-upgrade&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;terminal-screensaver&quot;&gt;Terminal screensaver&lt;/h3&gt;

&lt;p&gt;OK, this is not a “must-have”, but lately I came across terminal screensavers. They’re a wonder of clever coding and animation, implemented as shell scripts. I particularly liked the “Matrix” screensaver at &lt;a href=&quot;https://github.com/attogram/bash-screensavers/blob/main/gallery/matrix/matrix.sh&quot;&gt;https://github.com/attogram/bash-screensavers/blob/main/gallery/matrix/matrix.sh&lt;/a&gt;. CTRL+C quits, and COMMAND+K clears the screen (at least it does on iTerm on a Mac, the key combination might vary depending on which terminal software you use).&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <title>Personalising a Linux command line (part 2)</title>
      <link href="https://thomasswilliams.github.io/general/2026/03/22/linux-personalise-part-2.html"/>
      <updated>2026-03-22T01:00:00+00:00</updated>
      <id>https://thomasswilliams.github.io/general/2026/03/22/linux-personalise-part-2</id>
      <content type="html">&lt;p&gt;In my last post, &lt;a href=&quot;/general/2026/03/15/linux-personalise-part-1.html&quot;&gt;I customised my Linux command line with 4 dotfiles&lt;/a&gt;. Putting the time into finding my way around the command line, and learning Linux commands, becomes more important on a Linux server because there’s no GUI.&lt;/p&gt;

&lt;p&gt;I’ll continue tweaking in this post by modernising two classic commands I use daily, and adding two helper packages.&lt;/p&gt;

&lt;p&gt;In &lt;a href=&quot;/general/2026/03/28/linux-personalise-part-3.html&quot;&gt;part 3&lt;/a&gt; I’ll tackle the big one: a super-powered prompt.&lt;/p&gt;

&lt;h2 id=&quot;highlight&quot;&gt;highlight&lt;/h2&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;highlight&lt;/code&gt; package prints the contents of a file to the command line, with automatic syntax coloring. Install &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;highlight&lt;/code&gt; by running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo apt install highlight&lt;/code&gt;. Then, add the below to the end of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.bashrc&lt;/code&gt; to replace calls to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cat&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;highlight&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# replace cat with highlight as per https://stackoverflow.com/a/27501509 (needs &quot;highlight&quot; installed)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /usr/bin/highlight &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;alias cat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;highlight -O xterm256 --force&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Make the change take effect immediately by running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source ~/.bashrc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now, when I view a file with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cat&lt;/code&gt;, I get colored syntax.&lt;/p&gt;

&lt;div class=&quot;note&quot;&gt;
  &lt;p&gt;You can still run commands you’ve aliased on Linux by starting the command with a backslash “\” - for example, to run the original &lt;code&gt;cat&lt;/code&gt;, type &lt;code&gt;\cat&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;h2 id=&quot;eza&quot;&gt;eza&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eza&lt;/code&gt; is a better directory listing. Following the directions at &lt;a href=&quot;https://github.com/eza-community/eza/blob/main/INSTALL.md&quot;&gt;https://github.com/eza-community/eza/blob/main/INSTALL.md&lt;/a&gt;, I download from &lt;a href=&quot;https://github.com/eza-community/eza/releases&quot;&gt;https://github.com/eza-community/eza/releases&lt;/a&gt; (ensuring the right architecture, in my case &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aarch64&lt;/code&gt;), unzip the download, copy the unzipped &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eza&lt;/code&gt; executable to my home directory, then run from my home directory:&lt;/p&gt;

&lt;div class=&quot;note&quot;&gt;
  &lt;p&gt;To copy files, I use an SCP program, either WinSCP &lt;a href=&quot;https://winscp.net/eng/download.php&quot;&gt;https://winscp.net/eng/download.php&lt;/a&gt; for Windows or MacSCP &lt;a href=&quot;https://github.com/macnev2013/macSCP&quot;&gt;https://github.com/macnev2013/macSCP&lt;/a&gt; on Mac.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# make the downloaded file executable&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x eza
&lt;span class=&quot;c&quot;&gt;# create the directory ~/.local/bin&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; ~/.local/bin
&lt;span class=&quot;c&quot;&gt;# move eza executable to ~/.local/bin&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;mv &lt;/span&gt;eza ~/.local/bin/eza
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Lastly, I add to my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.bashrc&lt;/code&gt;, below everything else:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# replace ls with eza, expects eza installed at below location&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# change the parameters to suit - the set below shows hidden files, in long format,&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# with color for different file types and icons too&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ~/.local/bin/eza &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;alias ls&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;eza -lhag --color=always --group-directories-first --icons&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As a result, when I list the contents of a directory with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ls&lt;/code&gt;, I see the following output from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eza&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/linux-command-line-eza-mar-2026.png&quot; alt=&quot;Running ls, seeing output from eza on Ubuntu 25.04 server command line&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The alias parts above are entirely optional, I use them as it saves me remembering another command.&lt;/p&gt;

&lt;p&gt;Something else I like to do - as I come from Windows - is make directory navigation with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cd&lt;/code&gt; case-insensitive. I do this by adding the line below to the bottom of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.inputrc&lt;/code&gt; in my home directory, adapted from &lt;a href=&quot;https://askubuntu.com/a/87066&quot;&gt;https://askubuntu.com/a/87066&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;completion-ignore-case On
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;fastfetch&quot;&gt;fastfetch&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fastfetch&lt;/code&gt; (&lt;a href=&quot;https://github.com/fastfetch-cli/fastfetch&quot;&gt;https://github.com/fastfetch-cli/fastfetch&lt;/a&gt;) is a utility that does one thing - shows system information. I install it with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo apt install fastfetch&lt;/code&gt;, then run it with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fastfetch&lt;/code&gt;. On my Linux VM, I get the following:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/linux-command-line-fastfetch-mar-2026.png&quot; alt=&quot;fastfetch output in Ubuntu 25.04 server VM on an M1 Mac&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I’ve found it helpful to have all this information in one place.&lt;/p&gt;

&lt;h2 id=&quot;ncdu&quot;&gt;ncdu&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ncdu&lt;/code&gt; is slightly different than the other packages in this post, as it’s an interactive console for file/directory sizes, similar to Treesize or WinDirStat on Windows.&lt;/p&gt;

&lt;p&gt;Either download the right version from &lt;a href=&quot;https://dev.yorhel.nl/ncdu&quot;&gt;https://dev.yorhel.nl/ncdu&lt;/a&gt;, or install with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo apt install ncdu&lt;/code&gt;. Run, showing percents, in KB/MB/GB (rather than KiB/MiB/GiB), and in dark mode:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ncdu &lt;span class=&quot;nt&quot;&gt;--show-itemcount&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--show-percent&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--si&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--color&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;dark /
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ncdu&lt;/code&gt; to identify space used. With &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ncdu&lt;/code&gt; running, browse the file system using the arrow keys and enter to move between directories; show help with “?”, and finally quit with “q”.&lt;/p&gt;

&lt;p&gt;That’s it for part 2 - &lt;a href=&quot;/general/2026/03/28/linux-personalise-part-3.html&quot;&gt;part 3&lt;/a&gt;, next, is where I power up my prompt.&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <title>Personalising a Linux command line (part 1)</title>
      <link href="https://thomasswilliams.github.io/general/2026/03/15/linux-personalise-part-1.html"/>
      <updated>2026-03-15T01:00:00+00:00</updated>
      <id>https://thomasswilliams.github.io/general/2026/03/15/linux-personalise-part-1</id>
      <content type="html">&lt;p&gt;Most of the work on a Linux server happens at the command line. So, getting to know the Linux command line and ways to list, find, update, run, troubleshoot and manage programs is critical.&lt;/p&gt;

&lt;p&gt;I customise my Linux command line to help standardise and modernise, and add functionality. I prefer unobtrusive background changes, incrementally tweaking over time. Although it can be time-consuming to research and incorporate new tools or ways of doing things, I’m often on the lookout for what others do via blog posts or lists like &lt;a href=&quot;https://github.com/awesome-lists/awesome-bash&quot;&gt;https://github.com/awesome-lists/awesome-bash&lt;/a&gt;, &lt;a href=&quot;https://python.libhunt.com/&quot;&gt;https://python.libhunt.com/&lt;/a&gt;, or &lt;a href=&quot;https://selfh.st/weekly/&quot;&gt;https://selfh.st/weekly/&lt;/a&gt; (to go even further, something like &lt;a href=&quot;https://www.chezmoi.io/links/articles/&quot;&gt;chezmoi at https://www.chezmoi.io/links/articles/&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This post is part 1 of quality-of-life changes I make when I first log in to a new Linux server. I primarily use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bash&lt;/code&gt; (though I also have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zsh&lt;/code&gt; set up on some machines):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.hushlogin&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.inputrc&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.vimrc&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.bashrc&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All these files live in my home directory - the starting directory when logging in. So, these changes only affect me.&lt;/p&gt;

&lt;p&gt;Here’s what it looks like when I initially log in to a new Ubuntu 25.04 server:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/linux-command-line-1-mar-2026.png&quot; alt=&quot;First log in to Ubuntu 25.04 server&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;hushlogin&quot;&gt;.hushlogin&lt;/h2&gt;

&lt;p&gt;First, I hide the “banner” (that big wall of text in the screenshot above) when I log in by creating an empty file called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.hushlogin&lt;/code&gt; in my home directory, from the command line:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;touch&lt;/span&gt; .hushlogin
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is slightly better - next time when logging in, I’ll go straight to a prompt.&lt;/p&gt;

&lt;h2 id=&quot;inputrc&quot;&gt;.inputrc&lt;/h2&gt;

&lt;p&gt;I use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.inputrc&lt;/code&gt; for two minor tweaks to the command line: better history autocomplete, and clearing the current input. With the changes in place, if I start typing a command then press the up or down arrow key, the history will autocomplete based on what has been typed so far e.g. typing “vi” and pressing up arrow will go through the history of previous “vi” commands. Pressing ESC (the escape key) will clear the current line (I keep forgetting the real shortcut to do so).&lt;/p&gt;

&lt;p&gt;I create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.inoutrc&lt;/code&gt; in my home directory if the file doesn’t exist. I need to log out and back in again so it will be picked up. Edit with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vi&lt;/code&gt; e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vi .inputrc&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Respect default shortcuts.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$include&lt;/span&gt; /etc/inputrc

&lt;span class=&quot;c&quot;&gt;## arrow up&lt;/span&gt;
&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\e&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;[A&quot;&lt;/span&gt;:history-search-backward
&lt;span class=&quot;c&quot;&gt;## arrow down&lt;/span&gt;
&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\e&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;[B&quot;&lt;/span&gt;:history-search-forward
&lt;span class=&quot;c&quot;&gt;## esc = clear whole line&lt;/span&gt;
&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\e&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;: kill-whole-line
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once the above is copy-pasted to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vi&lt;/code&gt;, I save the file by pressing ESC, then entering “:wq” (without the quotes).&lt;/p&gt;

&lt;h2 id=&quot;vimrc&quot;&gt;.vimrc&lt;/h2&gt;

&lt;p&gt;Speaking of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vi&lt;/code&gt;, I tend to use it because it’s always there, whether I’m using Ubuntu, Red Hat Linux, Debian, or even Solaris. My basic standard is to enable syntax highlighting.&lt;/p&gt;

&lt;p&gt;Once again, I create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.vimrc&lt;/code&gt; in my home directory if the file doesn’t exist and edit using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vi&lt;/code&gt; (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vi .vimrc&lt;/code&gt;):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;syntax on
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There’s plenty of references for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.vimrc&lt;/code&gt;, for example &lt;a href=&quot;https://linux101.dev/vim-editor/configure-vimrc/&quot;&gt;https://linux101.dev/vim-editor/configure-vimrc/&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;bashrc&quot;&gt;.bashrc&lt;/h2&gt;

&lt;p&gt;A lot can be done in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.bashrc&lt;/code&gt;. I add the code below after whatever’s already in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.bashrc&lt;/code&gt; (out of the box, the Ubuntu &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.bashrc&lt;/code&gt; is fairly comprehensive):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# set prompt, green username followed by blue current path&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# based on Ubuntu default&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;PS1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;\n[\[\033[01;32m\]\u@\h \[\033[01;34m\]\w\[\033[00m\]]\$ &apos;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# ignore pwd, exit, quit, q etc. commands in bash history&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;HISTIGNORE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;pwd:exit:quit:q:history:cls&quot;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# add timestamps to history file (makes it easier to place certain commands on a date)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# doesn&apos;t affect arrow keys or &quot;history&quot; command&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;HISTTIMEFORMAT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;%F %T &quot;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# don&apos;t put duplicate lines or lines starting with space in the history&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;HISTCONTROL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ignoreboth&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# write to history immediately (not on logout) thanks to https://www.cherryservers.com/blog/a-complete-guide-to-linux-bash-history&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;PROMPT_COMMAND&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;history -a&apos;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# don&apos;t write LESS history&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;LESSHISTFILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;-

&lt;span class=&quot;c&quot;&gt;# increase history&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;HISTSIZE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;5000
&lt;span class=&quot;nv&quot;&gt;HISTFILESIZE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;10000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After I’ve saved my changes to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.bashrc&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vi&lt;/code&gt;, back on the command line I run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source ~/.bashrc&lt;/code&gt; to make the changes to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.bashrc&lt;/code&gt; take effect straight away without needing to log out and back in.&lt;/p&gt;

&lt;p&gt;My command line now looks a little cleaner with very little effort.&lt;/p&gt;

&lt;p&gt;Personalising the Linux command line as I’ve demonstrated is simple and does not impact other users. In &lt;a href=&quot;/general/2026/03/22/linux-personalise-part-2.html&quot;&gt;part 2&lt;/a&gt;, I’ll go further and change the directory listing command, the default “write a file to the screen” command called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cat&lt;/code&gt;, and a couple other things - stay tuned.&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <title>Dockerizing R Markdown/Shiny</title>
      <link href="https://thomasswilliams.github.io/development/r/2026/02/27/dockerizing-r-markdown-shiny.html"/>
      <updated>2026-02-27T01:00:00+00:00</updated>
      <id>https://thomasswilliams.github.io/development/r/2026/02/27/dockerizing-r-markdown-shiny</id>
      <content type="html">&lt;p&gt;I’ve been a fan of R Markdown (and Shiny) for nearly 3 years, and have &lt;a href=&quot;https://thomasswilliams.github.io/categories/?ss360Query=r%20markdown&quot;&gt;written about it here on my blog many times&lt;/a&gt;. R Markdown is my go-to for analysis and reports that need anything more than a simple table.&lt;/p&gt;

&lt;p&gt;Running R Markdown via Docker is a big help with deploying those analysis and reports for others to use.&lt;/p&gt;

&lt;p&gt;While there are alternatives for deploying R Markdown — the most popular being Shiny Server — they add the hassle of matching R and package versions to the machine where the code was developed, may require IT help, and mean sharing disk, CPU, and memory with other apps on the same server. And there’s also licensing requirements &amp;amp; limitations for the free version of Shiny Server.&lt;/p&gt;

&lt;p&gt;R Markdown on Docker avoids some of these issues; a single Docker container = a single app.&lt;/p&gt;

&lt;p&gt;In this post I’ll go over running an R Markdown page in Docker called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debug.Rmd&lt;/code&gt; that lists the R version, as well as installed packages &amp;amp; versions.&lt;/p&gt;

&lt;p&gt;In a new directory, for example “r-markdown-docker-test”, create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debug.Rmd&lt;/code&gt; as per below:&lt;/p&gt;

&lt;div class=&quot;language-r highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;---&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;debug.Rmd&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;html_document&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;runtime&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shiny&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;---&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;``&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;` {r global, echo = FALSE, message = FALSE, warning = FALSE}
# load packages
# sessioninfo: R Session Information
# https://github.com/r-lib/sessioninfo
library(sessioninfo)
# details: R Package to Create Details HTML Tag for Markdown and Package Documentation
# https://github.com/yonicd/details
library(details)
# dplyr for piping with &quot;%&amp;gt;%&quot;
library(dplyr)
`&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;`

`&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;`{r, echo = FALSE, comment = NA}
# initially expanded environment and package versions from sessionInfo()
# requires &quot;sessioninfo&quot; and &quot;details&quot; packages
# adapted from https://cran.r-project.org/web/packages/details/vignettes/sessioninfo.html
sessioninfo::session_info() %&amp;gt;%
  details::details(open = TRUE)
`&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;``&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;note&quot;&gt;
  &lt;p&gt;You can find the complete code on GitHub at &lt;a href=&quot;https://github.com/thomasswilliams/r-markdown-docker-test&quot;&gt;https://github.com/thomasswilliams/r-markdown-docker-test&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debug.Rmd&lt;/code&gt; from RStudio (after installing required packages) results in something like (on an M1 Mac as at Feb 2026):&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/debug-rmd-m1-mac-feb-2026.png&quot; alt=&quot;debug.RMD on M1 Mac&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;bringing-in-docker&quot;&gt;Bringing in Docker&lt;/h2&gt;

&lt;p&gt;Now let’s Dockerize the R Markdown file (or any number of files in a folder), which requires Docker, naturally.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; below:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;pulls the base &lt;a href=&quot;https://rocker-project.org/images/versioned/shiny.html&quot;&gt;Rocker&lt;/a&gt; Shiny Docker image (I experimented with a base R image and installing Shiny - it’s a lot of work, simpler to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rocker/shiny&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;installs required packages (add whatever packages needed for R Markdown files e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DT&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;highcharter&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bslib&lt;/code&gt; etc.)&lt;/li&gt;
  &lt;li&gt;copies R Markdown files from the current directory to the Shiny Server base directory in the container
    &lt;ul&gt;
      &lt;li&gt;since only R Markdown files are copied, I didn’t bother with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.dockerignore&lt;/code&gt; file; suggest adding for R Markdown projects which include separate javascript &amp;amp; CSS files, images etc.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;switches to the “shiny” non-root user&lt;/li&gt;
  &lt;li&gt;runs Shiny&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-dockerfile highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# base R Shiny image, latest&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; rocker/shiny&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# install required R packages (Shiny already present)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;R &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;install.packages(c(&apos;dplyr&apos;, &apos;rmarkdown&apos;, &apos;sessioninfo&apos;, &apos;details&apos;))&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# remove sample files from Shiny Server base directory&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-rf&lt;/span&gt; /srv/shiny-server/

&lt;span class=&quot;c&quot;&gt;# copy R Markdown files from this repo into Shiny Server base directory&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; *.Rmd /srv/shiny-server/&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# make use of in-built &quot;shiny&quot; user in image to run as non-root&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# set permissions on copied files, so &quot;shiny&quot; user can read&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-R&lt;/span&gt; shiny:shiny /srv/shiny-server

&lt;span class=&quot;c&quot;&gt;# switch to the &quot;shiny&quot; non-root user&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# when container runs, following commands run as this user&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;USER&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; shiny&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# expose Shiny port&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;EXPOSE&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; 3838&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# start Shiny Server (default)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CMD&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; [&quot;/usr/bin/shiny-server&quot;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; is saved in the project’s directory, alongside the R Markdown file, build the image from a command line in the same directory as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker build &lt;span class=&quot;nt&quot;&gt;--platform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;linux/amd64 &lt;span class=&quot;nt&quot;&gt;--no-cache&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; r-markdown-docker-test &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The built image can be run with:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker run &lt;span class=&quot;nt&quot;&gt;--platform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;linux/amd64 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; 3838:3838 r-markdown-docker-test
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With the container running, open a web browser to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debug.Rmd&lt;/code&gt; at &lt;a href=&quot;http://localhost:3838/debug.Rmd&quot;&gt;http://localhost:3838/debug.Rmd&lt;/a&gt; which will display something like:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/debug-rmd-docker-feb-2026.png&quot; alt=&quot;debug.RMD on Docker&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Docker can be run on a server too; the built image can be outout to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tar&lt;/code&gt; file based on steps at &lt;a href=&quot;https://www.howtogeek.com/devops/how-to-share-docker-images-with-others/&quot;&gt;https://www.howtogeek.com/devops/how-to-share-docker-images-with-others/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;An advantage with using Docker is being able to preview &lt;u&gt;exactly&lt;/u&gt; what will be deployed, to make sure it works. It will run the same on your machine as any other. I’ve had times where a library version on Shiny Server was different to the development machine which caused a blank page to be shown, not even an error message - thus the original reason for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debug.Rmd&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Hopefully this post gives options when planning to deploy R Markdown files.&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <title>Kerberos and SQL Server in 2026</title>
      <link href="https://thomasswilliams.github.io/sqlserver/2026/02/20/kerberos-and-sql-server-going-further.html"/>
      <updated>2026-02-20T01:00:00+00:00</updated>
      <id>https://thomasswilliams.github.io/sqlserver/2026/02/20/kerberos-and-sql-server-going-further</id>
      <content type="html">&lt;p&gt;I spent a bit of time recently tracking down NTLM Event log warnings relating to SQL Server.&lt;/p&gt;

&lt;p&gt;NTLM is an older Windows authentication protocol, largely superseded by Kerberos.&lt;/p&gt;

&lt;p&gt;Kerberos is used when certain conditions are met; otherwise, Windows authentication silently falls back to NTLM. This is important in 2026 because Microsoft has announced that new versions of Windows desktop and Windows Server will no longer support NTLM.&lt;/p&gt;

&lt;p&gt;I’m not the only DBA following up on NTLM - last week I read a blog post covering NTLM, Kerberos, and Service Principal Names (SPNs) at &lt;a href=&quot;https://www.sqlfingers.com/2026/02/microsoft-is-killing-ntlm-heres-what.html&quot;&gt;https://www.sqlfingers.com/2026/02/microsoft-is-killing-ntlm-heres-what.html&lt;/a&gt; (via &lt;a href=&quot;https://curatedsql.com/2026/02/13/tips-on-a-post-ntlm-future/&quot;&gt;Kevin at Curated SQL&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;To ensure clients connect to SQL Server using Kerberos:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;SQL Server must run under a domain account (the same account on all servers, if using Windows Clusters)&lt;/li&gt;
  &lt;li&gt;connections must use host names, not IP addresses&lt;/li&gt;
  &lt;li&gt;the client driver must support Kerberos&lt;/li&gt;
  &lt;li&gt;if using Availability Groups, two SPNs must be created manually for the Availability Group &lt;em&gt;Listener&lt;/em&gt; like:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;setspn &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt; HOST&lt;span class=&quot;se&quot;&gt;\&amp;lt;&lt;/span&gt;Availability Group Listener&amp;gt;:1433 &amp;lt;SQL Server domain account&amp;gt;
setspn &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt; HOST&lt;span class=&quot;se&quot;&gt;\&amp;lt;&lt;/span&gt;Availability Group Listener&amp;gt;.domain.name:1433 &amp;lt;SQL Server domain account&amp;gt; &lt;span class=&quot;c&quot;&gt;# FQDN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The second SPN creation command is for the Fully-Qualified Domain Name (FQDN) for the SQL Server Listener. You’ll need both, and the commands should be tweaked for your environment (and port). Say the SQL Server domain account is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MYDOMAIN\SqlProd&lt;/code&gt;, and your listener is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SQL-2022-LISTEN&lt;/code&gt;, the commands would look like:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;setspn &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt; HOST&lt;span class=&quot;se&quot;&gt;\S&lt;/span&gt;QL-2022-LISTEN:1433 MYDOMAIN&lt;span class=&quot;se&quot;&gt;\S&lt;/span&gt;qlProd
setspn &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt; HOST&lt;span class=&quot;se&quot;&gt;\S&lt;/span&gt;QL-2022-LISTEN.mydomain:1433 MYDOMAIN&lt;span class=&quot;se&quot;&gt;\S&lt;/span&gt;qlProd &lt;span class=&quot;c&quot;&gt;# FQDN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That covers clients connecting to SQL Server. But what about servers SQL Server connects to?&lt;/p&gt;

&lt;h2 id=&quot;backup&quot;&gt;Backup&lt;/h2&gt;

&lt;p&gt;If you back up databases over the network (and you should), SQL Server may authenticate using NTLM.&lt;/p&gt;

&lt;p&gt;You can see this by enabling NTLM Event logging on your backup server in the registry by adding a DWORD &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AuditReceivingNTLMTraffic&lt;/code&gt; and setting the value to 1 under &lt;strong&gt;HKLM\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0&lt;/strong&gt; as per &lt;a href=&quot;https://dirteam.com/sander/2022/06/15/howto-detect-ntlmv1-authentication/&quot;&gt;https://dirteam.com/sander/2022/06/15/howto-detect-ntlmv1-authentication/&lt;/a&gt;. Then, use Event Viewer and look for warnings under &lt;strong&gt;Microsoft &amp;gt; Windows &amp;gt; NTLM &amp;gt; Operational&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In my case, NTLM was being used because the backup server was referenced via a DNS CNAME. The fix was for a domain admin to manually register two SPNs for the CNAME, under the host:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;setspn &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt; HOST&lt;span class=&quot;se&quot;&gt;\&amp;lt;&lt;/span&gt;CNAME&amp;gt; &amp;lt;host/target&amp;gt;
setspn &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt; HOST&lt;span class=&quot;se&quot;&gt;\&amp;lt;&lt;/span&gt;CNAME&amp;gt;.domain.name &amp;lt;host/target&amp;gt; &lt;span class=&quot;c&quot;&gt;# FQDN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For example, if my CNAME is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backup&lt;/code&gt;, and the server the CNAME is pointing to is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FILESERVER1&lt;/code&gt;, the commands would be:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;setspn &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt; HOST&lt;span class=&quot;se&quot;&gt;\b&lt;/span&gt;ackup fileserver1
setspn &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt; HOST&lt;span class=&quot;se&quot;&gt;\b&lt;/span&gt;ackup.mydomain fileserver1 &lt;span class=&quot;c&quot;&gt;# FQDN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It may take some time for SPN changes to propagate through the domain (in my case, around 25 minutes).&lt;/p&gt;

&lt;p&gt;You can list the SPNs, including the manual ones for CNAMEs, belonging to a host with:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;setspn &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; &amp;lt;host/target&amp;gt; &lt;span class=&quot;c&quot;&gt;# e.g. fileserver1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Alternatively, you can query for the SPN for the DNS CNAME using:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;setspn &lt;span class=&quot;nt&quot;&gt;-Q&lt;/span&gt; HOST&lt;span class=&quot;se&quot;&gt;\&amp;lt;&lt;/span&gt;CNAME&amp;gt;.domain.name &lt;span class=&quot;c&quot;&gt;# e.g. HOST\backup.mydomain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If the backup server changes and the CNAME is repointed, the SPN will need to be removed and recreated to maintain Kerberos authentication.&lt;/p&gt;

&lt;h2 id=&quot;availability-group-file-share-witness&quot;&gt;Availability Group File share witness&lt;/h2&gt;

&lt;p&gt;The other SQL Server–related NTLM warning came from a file share witness used by an Availability Group. Once again, a CNAME was in use, so the fix was the same - create two SPNs for the CNAME, one with the FQDN:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;setspn &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt; HOST&lt;span class=&quot;se&quot;&gt;\&amp;lt;&lt;/span&gt;CNAME&amp;gt; &amp;lt;host/target&amp;gt;
setspn &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt; HOST&lt;span class=&quot;se&quot;&gt;\&amp;lt;&lt;/span&gt;CNAME&amp;gt;.domain.name &amp;lt;host/target&amp;gt; &lt;span class=&quot;c&quot;&gt;# FQDN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With those two issues resolved, there were no further NTLM warnings related to SQL Server.&lt;/p&gt;
</content>
    </entry>
  
</feed>