<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://bridgetownrb.com/" version="2.2.0">Bridgetown</generator><link href="https://www.monospacementor.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.monospacementor.com/" rel="alternate" type="text/html" /><updated>2026-07-15T15:45:38+01:00</updated><id>https://www.monospacementor.com/feed.xml</id><title type="html">Monospace Mentor</title><subtitle>Courses and mentorship for engineering teams of one to ten who have little time to level up.</subtitle><author><name>Jochen Lillich</name></author><entry><title type="html">AI took my job as a coder. Then it made me a better software engineer.</title><link href="https://www.monospacementor.com/learning/ai-made-me-better-software-engineer/" rel="alternate" type="text/html" title="AI took my job as a coder. Then it made me a better software engineer." /><published>2026-06-29T09:10:00+01:00</published><updated>2026-06-29T09:10:00+01:00</updated><id>repo://posts.collection/_posts/2026-06-29-ai-made-me-better-software-engineer.md</id><content type="html" xml:base="https://www.monospacementor.com/learning/ai-made-me-better-software-engineer/">&lt;p&gt;I’ve been doing agent-supported coding for a few weeks now, and it definitely has changed the way I work. It’s not worse, just different. Which is remarkable, given that many programmers are having a disturbingly different experience: LLMs generating bad code, introducing bugs, even destroying data. I also read accounts from people lamenting skill atrophy, loss of job satisfaction, and a feeling of disconnection from their own codebase. It sounds like for these developers, the LLM became the mythical Monkey’s Paw that gives you what you want, and then takes away something dear to your heart.&lt;/p&gt;

&lt;p&gt;I don’t want to find myself in this dark place eventually. I also don’t want to find myself stuck when LLM prices skyrocket, or the whole AI bubble bursts. The reason why I don’t think I will end up stranded is my attitude towards coding, or rather, to software engineering.&lt;/p&gt;

&lt;h2 id=&quot;passive-and-active-guidance&quot;&gt;Passive and active guidance&lt;/h2&gt;

&lt;p&gt;Working with an agent is when good coding standards pay off. I got to enjoy that satisfaction before. It was when we introduced linting, unit and integration tests to our infra code a few years into our operation. We quickly realized how our confidence in the code we deployed was growing, while the amount of nightly emergency fixes was going down. Now, tests and linting rules are the guardrails that prevent agents from introducing regressions and becoming too creative with their new code.&lt;/p&gt;

&lt;p&gt;Even with these guardrails in place, I don’t rely on the agent to find its way alone. My agent sessions look like pair programming, with the agent as the driver and me as the observer. I see other developers switch their attention to other things while the agent does its thing. I follow every step it takes. Not just because I don’t fully trust it. I also keep asking questions about its approach, and intervene to help it find the best solution. Every task results in a merge request that we review thoroughly before we integrate the changes into our codebase. (I just found a neat way to do these reviews in the terminal, about which I’ll write soon.)&lt;/p&gt;

&lt;p&gt;Many years of experience have taught me that this kind of investment into “boring” engineering practices pays dividends. I don’t see why that should change with LLMs.&lt;/p&gt;

&lt;h2 id=&quot;from-programming-to-engineering&quot;&gt;From programming to engineering&lt;/h2&gt;

&lt;p&gt;I don’t care how software is made, I care about what it does in the end. Loris Cro put it well in his article “&lt;a href=&quot;https://kristoff.it/blog/north-star/&quot;&gt;My Software North Star&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Software should be useful to the end user&lt;/li&gt;
  &lt;li&gt;Software should be correct&lt;/li&gt;
  &lt;li&gt;Software should be maintainable and efficient&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But how do you achieve that?&lt;/p&gt;

&lt;p&gt;Before I started to use coding agents regularly, my many responsibilities at work left me little time to practice what I’d call “engineering discipline”. My process was: do a bit of thinking in advance, start coding, review, deploy. It did the job, but there wasn’t much “engineering” to it, and it always felt a bit shallow.&lt;/p&gt;

&lt;p&gt;The software engineering methods I read about in the 80s — SA/SD, Jackson SD, SADT — are long dead, and deservedly so. But their &lt;em&gt;intent&lt;/em&gt; didn’t die. Today’s methods have different names, and they tend to be leaner: discovery instead of requirements analysis, TDD instead of formal verification, ADRs instead of binders full of design docs. The problem is, they’re only “light” relative to 1985, but still not at all light for a team of two. Learning these engineering methods, let alone practising them regularly, always felt like a luxury we couldn’t afford. AI agents changed these economics.&lt;/p&gt;

&lt;h2 id=&quot;get-it-in-writing&quot;&gt;Get it in writing&lt;/h2&gt;

&lt;p&gt;Like everyone, I started out by asking the LLM to implement small new features. (I found it hilarious how much better it was at Rails coding than me.) Soon, I picked up other people’s practice of ending prompts with the question of what else it needed to know. This alone eliminated a lot of backtracking and do-overs, and evolved naturally into having the agent generate a comprehensive plan first, and implement that plan in a second phase.&lt;/p&gt;

&lt;p&gt;Plans require decisions, during both their inception and their implementation. Realizing the advantage of documenting our technological direction, I started dabbling with Architecture Decision Records a few years ago. ADRs are documents that preserve the thoughts behind decisions with significance for the shape of a software. Usually, those are also the kind of decisions that are difficult to revert later. In 16 years at the same company, I learned to appreciate the value of organizational memory. If only writing those documents wouldn’t take so much effort, especially if you’re not writing in your native language! I don’t think I ever wrote more than a handful. Thanks to a coding partner that takes great care of that, I now have a dedicated repository just for ADRs.&lt;/p&gt;

&lt;p&gt;Once I got more proficient at guiding that coding partner, my next step was to break out of the feature-building box. I started involving the agent even before launching new projects. An extensive back-and-forth that I call “crucible sessions” results in clear documentation of a project’s motivations, goals, and most importantly, its scope. Staying focused has always been a struggle for our team. There’s always the temptation of tackling random issues we discover during project work. It’s easy to rationalize the distraction as “a good opportunity”. Starting with a clear definition of a feature and its scope helps us tremendously with shipping it without detours. In the process of refining this workflow, I learned that this kind of document is called a &lt;a href=&quot;https://www.atlassian.com/agile/product-management/requirements&quot;&gt;PRD&lt;/a&gt;. The agent saves them on Linear as project documents.&lt;/p&gt;

&lt;p&gt;You might notice a pattern: all these improvements preserve knowledge and create transparency. This pattern now repeats like a fractal on all levels of my work. I’m writing less code, but I’m creating more value.&lt;/p&gt;

&lt;h2 id=&quot;theres-more-than-just-code&quot;&gt;There’s more than just code&lt;/h2&gt;

&lt;p&gt;Like Mark Betz, “&lt;a href=&quot;https://www.markbetz.net/2026/05/24/i-dont-miss-writing-code/&quot;&gt;I don’t miss writing code&lt;/a&gt;”. I don’t feel disenfranchised by the agent that does it for me.&lt;/p&gt;

&lt;p&gt;It’s probably an identity thing. My job has never been to deliver code, or to provision servers, or to type words into a blog editor. My job is to keep our business on a healthy trajectory, technologically and otherwise. I’m enjoying being able to do that job more effectively than before.&lt;/p&gt;

&lt;p&gt;Did it turn me into an AI Booster? Hell no. I struggle to reconcile the many quandaries around AI usage with my family’s addiction to food and shelter. My message is not “Use AI, it’s great!” It’s: “When you have good software engineering practices, everything works better.” With or without LLMs.&lt;/p&gt;

&lt;p&gt;I don’t believe this new era is “Game Over” for developers. It’s a forcing function to get clear about what we’re building, and to communicate well how we’re doing it. If that resonates with your situation, &lt;a href=&quot;https://monospacementor.com/community/&quot;&gt;The Server Room&lt;/a&gt; is where we can figure it out together.&lt;/p&gt;</content><author><name>Jochen Lillich</name></author><category term="learning" /><category term="genAI" /><category term="SoftwareEngineering" /><summary type="html">Many developers are watching AI agents generate bad code, introduce bugs, and erode their skills. I&apos;m having a different experience.</summary></entry><entry><title type="html">The motion that changed everything</title><link href="https://www.monospacementor.com/learning/the-motion-that-changed-everything/" rel="alternate" type="text/html" title="The motion that changed everything" /><published>2026-06-21T16:42:20+01:00</published><updated>2026-06-21T16:42:20+01:00</updated><id>repo://posts.collection/_posts/2026-06-21-the-motion-that-changed-everything.md</id><content type="html" xml:base="https://www.monospacementor.com/learning/the-motion-that-changed-everything/">&lt;p&gt;I switched to Vim a long ago after using a series of GUI editors like TextMate and Sublime Text. The urge for novelty has me still try out GUI editors sporadically, the most recent one being Zed. But I always come back quickly. I’m doing all my engineering work from the terminal, and by now my tooling is more or less complete and highly optimized for efficiency.&lt;/p&gt;

&lt;p&gt;What brought me to Vim was the thought, “It’s installed on all my Unix and Linux servers, I should have at least some basic proficiency.” What made me stay was its “command grammar”; first and foremost, Vim motions.&lt;/p&gt;

&lt;p&gt;I have to admit that even after decades, I only rarely use the number-based motions. Instead of &lt;code class=&quot;highlighter-rouge&quot;&gt;4dd&lt;/code&gt;, I mark lines for deletion in visual mode. And although I have relative line numbers switched on, I still end up holding &lt;code class=&quot;highlighter-rouge&quot;&gt;k&lt;/code&gt; like an animal.&lt;/p&gt;

&lt;p&gt;What makes Vim great for me are the motions that are based on text. For example, quickly jumping to the next colon with &lt;code class=&quot;highlighter-rouge&quot;&gt;f:&lt;/code&gt;. Or selecting the current word with &lt;code class=&quot;highlighter-rouge&quot;&gt;*&lt;/code&gt; and immediately being able to jump to its next occurrence with &lt;code class=&quot;highlighter-rouge&quot;&gt;n&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Another neat feature isn’t exactly part of Vim motions, but text objects are closely related. It’ll never stop to make me smile when I can edit the contents of a pair of quotes or parentheses as easy as &lt;code class=&quot;highlighter-rouge&quot;&gt;ci)&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;da&quot;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;BTW, if you’re looking for a comprehensive Vim tutorial, try the freely available chapter “&lt;a href=&quot;https://courses.monospacementor.com/linsys-1/mod01-fundamentals/0105-editing/&quot;&gt;Text Editing Fundamentals&lt;/a&gt;”, part of my community course “Basic Linux System Administration”!&lt;/p&gt;

&lt;p&gt;This post was inspired by and written for “&lt;a href=&quot;https://lazybea.rs/vim-carnival-202606/&quot;&gt;Hyde’s Vim Carnival&lt;/a&gt;”.&lt;/p&gt;</content><author><name>Jochen Lillich</name></author><category term="learning" /><category term="Vim" /><summary type="html">Why Vim motions remain the feature that keeps me coming back to Vim after decades of terminal-based work.</summary></entry><entry><title type="html">Fixing Flatpak Obsidian CLI</title><link href="https://www.monospacementor.com/learning/fixing-flatpak-obsidian-cli/" rel="alternate" type="text/html" title="Fixing Flatpak Obsidian CLI" /><published>2026-05-08T21:18:11+01:00</published><updated>2026-05-08T21:18:11+01:00</updated><id>repo://posts.collection/_posts/2026-05-08-fixing-flatpak-obsidian-cli.md</id><content type="html" xml:base="https://www.monospacementor.com/learning/fixing-flatpak-obsidian-cli/">&lt;p&gt;I just tried to use the new CLI tool for Obsidian for the first time. To set it up, enabling a toggle switch in the “General” section of the settings should suffice. But if Obsidian is installed on Linux as a FlatPak, all you get when you execute the command is:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ obsidian
The CLI is unable to find Obsidian. Please make sure Obsidian is running and try again.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The problem is that FlatPak isolates each application for safety reasons. In consequence, the Obsidian socket gets set up, but not at the location where the CLI expects it. This socket is a special file through which the CLI tool and the Obsidian application communicate. That’s why the CLI suspects that Obsidian might not even be running, even if it is.&lt;/p&gt;

&lt;p&gt;The fix is simple: We put a symbolic link where the CLI expects the socket, and let the link point to where the socket actually is:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ln -sf /run/user/$(id -u)/.flatpak/md.obsidian.Obsidian/xdg-run/.obsidian-cli.sock /run/user/$(id -u)/.obsidian-cli.sock
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With this fix, the CLI is happy:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ obsidian version
1.12.7 (installer 1.12.7)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now I can explore my Obsidian vaults from the shell!&lt;/p&gt;

&lt;p&gt;By the way, you can read &lt;a href=&quot;/wiki/&quot;&gt;my DevOps notes&lt;/a&gt; that I manage in Obsidian right here on my website!&lt;/p&gt;

&lt;p&gt;And if you’d like to learn more about symbolic links and the Linux filesystem in general, check out my course “&lt;a href=&quot;https://monospacementor.com/courses/linsys-1/&quot;&gt;Basic Linux System Administration&lt;/a&gt;”.&lt;/p&gt;</content><author><name>Jochen Lillich</name></author><category term="learning" /><category term="Linux" /><summary type="html">How to fix the Obsidian CLI when the Flatpak package cannot expose the command in the usual way.</summary></entry><entry><title type="html">Going back to the roots for our community chat</title><link href="https://www.monospacementor.com/changelog/going-back-to-the-roots-for-our-community-chat/" rel="alternate" type="text/html" title="Going back to the roots for our community chat" /><published>2026-04-26T16:18:43+01:00</published><updated>2026-04-26T16:18:43+01:00</updated><id>repo://posts.collection/_posts/2026-04-26-going-back-to-the-roots-for-our-community-chat.md</id><content type="html" xml:base="https://www.monospacementor.com/changelog/going-back-to-the-roots-for-our-community-chat/">&lt;p&gt;When Discord announced the introduction of age verification, my first thought was, “That’s what I’ve been preparing for.” I logged into one of my machines and set up an IRC server.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2026/04/well-ive-been-preparing-for-that-my-entire-life.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Like many open-source communities, &lt;a href=&quot;https://monospacementor.com/community/&quot;&gt;The Server Room&lt;/a&gt; has been using Discord as a chat platform for years. But I always felt like I was building on a brittle foundation. I knew that it was only a matter of time until I had to find a more future-proof solution for our real-time conversations.&lt;/p&gt;

&lt;p&gt;When Discord announced the introduction of age verification, potentially requiring official identification and photographic evidence, that time had come. Many of our community members are working in (or adjacent to) cybersecurity. There was no way they’d be happy to provide deeply personal information to an opaque business (or to its even more &lt;a href=&quot;https://www.malwarebytes.com/blog/news/2026/02/age-verification-vendor-persona-left-frontend-exposed&quot;&gt;questionable business partners&lt;/a&gt;) just to have a chat.&lt;/p&gt;

&lt;p&gt;Even though Discord backed off their initial plan, I’m happy that I made the change.&lt;/p&gt;

&lt;h2 id=&quot;the-problem-with-building-on-someone-elses-land&quot;&gt;The Problem With Building on Someone Else’s Land&lt;/h2&gt;

&lt;p&gt;The barrier to entry to Discord is low, for server operators, and even more so for community members. In comparison to other chat platforms, its less ephemeral “forum” channels that retain messages for later reading seemed ideal for a community platform that is all about sharing and preserving knowledge. But there’s a catch. (There’s always a catch.)&lt;/p&gt;

&lt;p&gt;Discord is a business, businesses need to make money, and it was clear from the start that it won’t offer its service for free without any downsides forever. Discord looks like the ideal candidate for the Enshittification effect - a sudden push for monetization, harming its users once it reaches a certain level of popularity and funds starts running out.&lt;/p&gt;

&lt;p&gt;The TSR community is at the heart of my whole operation. Not only is my teaching work funded by membership fees, it’s based on connections between humans. Members don’t just study my course materials in isolation, they help each other grow. I can’t build this community on a product that can take a nosedive at any time. Joan Westenberg wrote about this kind of dependency in “&lt;a href=&quot;https://joanwestenberg.medium.com/the-creator-economy-trap-why-building-on-someone-elses-platform-is-a-dead-end-732b93f1a016&quot;&gt;The creator economy trap&lt;/a&gt;”. I’ve seen communities switch platforms for all kinds of reasons, and every time, they lost members in the process. For a fledgling community like mine, this could be an extinction-level event. Owning your community platform not only impacts cost and control, it also is the prerequisite for continuity.&lt;/p&gt;

&lt;p&gt;For that reason, I set up a forum using the popular open-source software Discourse (the similarity in names is so confusing) in January 2024. Other than Discord, the forum gave me confidence that the useful conversations in my community would be preserved and easy to retrieve at any time. Within a short time, the forum became the centre of our community’s activities. With this part in the bag, real-time chat needed a reliable solution, too.&lt;/p&gt;

&lt;h2 id=&quot;why-irc-though&quot;&gt;Why IRC, Though?&lt;/h2&gt;

&lt;p&gt;While Discord dominates the market of chat solutions, there are alternatives. When you look for open-source solutions, you’ll find &lt;a href=&quot;https://matrix.org/&quot;&gt;Matrix&lt;/a&gt;, Mattermost, &lt;a href=&quot;https://zulip.com/&quot;&gt;Zulip&lt;/a&gt;, and Discord clones like &lt;a href=&quot;https://stoat.chat/&quot;&gt;Stoat&lt;/a&gt;. And even among proprietary group chat services, there are well-liked options such as Signal. Why on earth did I choose the oldest chat platform of them all?&lt;/p&gt;

&lt;p&gt;One reason is certainly that I’m even older than IRC, and I used it daily in the early years of me having access to the internet. The experience of being able to chat with people in distant countries was magical, and it informed my understanding of the internet as a “global village”.&lt;/p&gt;

&lt;p&gt;While I did receive a gentle mocking as “the old sysadmin” when I mentioned IRC as a chat alternative, there was little pushback. People rather seemed intrigued that IRC might still have its place in internet-based communication.&lt;/p&gt;

&lt;p&gt;As a first experiment, I created a channel on Libera.Chat. Additionally, I set up a bridge between it and our main Discord channel, et voila: people were able to chat from their IRC client applications. This only took me an hour, and most of that went into the initial bridge configuration.&lt;/p&gt;

&lt;p&gt;Another reason for my choice was that setting up an IRC server is easy. With a modern implementation like &lt;a href=&quot;https://github.com/ergochat/ergo&quot;&gt;Ergo&lt;/a&gt;, you can get started within an hour. You don’t have to deploy a web application backed by multiple database services, there’s no need for a complex Docker stack.&lt;/p&gt;

&lt;p&gt;Then, IRC clients are available for every popular desktop, tablet and phone OS. In addition to the GUI apps, there are (of course!) well-maintained terminal-based ones as well. I’m using &lt;a href=&quot;https://weechat.org/&quot;&gt;weechat&lt;/a&gt;, for example.&lt;/p&gt;

&lt;p&gt;Finally, IRC is still actively developed. The latest protocol revision IRCv3 brings quality-of-life improvements such as being able to retrieve chat history when connecting. With earlier IRC versions, you’ll either miss what happened in chat while you were offline, or you’ll have to set up a client application (also known as a “bouncer”) that keeps a continuous server connection. Ergo has chat history enabled by default, and modern clients such as &lt;a href=&quot;https://github.com/squidowl/halloy&quot;&gt;Halloy&lt;/a&gt; take advantage of it.&lt;/p&gt;

&lt;p&gt;And as a bonus, I just found out that the IRCv3 project has a &lt;a href=&quot;https://ircv3.net/conduct&quot;&gt;Code of Conduct&lt;/a&gt;, which is a good sign that they’re a reliable part of the online community.&lt;/p&gt;

&lt;p&gt;All this, together with lively IRC communities like Libera.Chat, proves that IRC is still going strong after four decades. What’s not to like?&lt;/p&gt;

&lt;h2 id=&quot;the-technical-details&quot;&gt;The technical details&lt;/h2&gt;

&lt;p&gt;Getting the IRC server started on one of my virtual private servers didn’t take longer than it took setting up our Discord server back in the day. Launching a Discord server takes a few mouse clicks. Installing &lt;a href=&quot;https://github.com/ergochat/ergo&quot;&gt;Ergo&lt;/a&gt; from the Fedora package repository took a few keystrokes.&lt;/p&gt;

&lt;p&gt;I admit that I took a detour setting up TLS that cost me another hour. First, I tried to put the IRC server behind the Nginx Proxy Manager that I already had running for serving websites, but I couldn’t get the Gamja web interface to connect successfully. From what I found online, IRC requires a few esoteric proxy settings that are easier to configure when you can modify the nginx configuration directly, instead of having to use the limited options of the NginxPM web interface. In the end, I configured Ergo to open a TLS port itself, and installed Certbot to handle the certificate renewal with Let’s Encrypt.&lt;/p&gt;

&lt;p&gt;Installing &lt;a href=&quot;https://codeberg.org/emersion/gamja&quot;&gt;Gamja&lt;/a&gt; was a quick job, and here I was able to successfully employ NginxPM. Thanks to Gamja, community members who don’t want to install a local IRC client have the option to access chat from their web browser.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/42wim/matterbridge&quot;&gt;Matterbridge&lt;/a&gt; is a nifty self-hosted service that can cross-post messages from one chat service to another. That way, there’s no split between the people adopting IRC and the ones still stuck on Discord. Discord and IRC are only two items in the long list of services that Matterbridge supports. Since I had already been using it to bridge our channel on Libera.Chat, I just had to modify the connection details to switch to our own IRC server. Then, I added bridges for two more channels, just to have the most important ones covered.&lt;/p&gt;

&lt;p&gt;I used the Discord API and a few bots to build admin workflows, posting of news and social media messages, and a few other fun features. IRC has been using bots for the same purposes for much longer. With &lt;a href=&quot;https://limnoria.net/&quot;&gt;Limnoria&lt;/a&gt;, I was able to replace all the features I wanted to have from the start. The list of available Limnoria plugins is long and provides a lot of useful (or simply fun) features. Here are some of the plugins I set up myself:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;SedRegex&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;allows you to correct a previous message using a quick&lt;/p&gt;

    &lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;s/what you typed/what you meant to type/&lt;/code&gt;&lt;/p&gt;

    &lt;p&gt;. (Did you know that Skype had this built in?)&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;With&lt;/p&gt;

    &lt;p&gt;&lt;strong&gt;Later&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;, you can leave offline members a message that they’ll see when they post their first message after returning.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The&lt;/p&gt;

    &lt;p&gt;&lt;strong&gt;Karma&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;plugin keeps tally of up- and downvotes for a member or another thing. A neat way to show appreciation or annoyance.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The 3rd-party&lt;/p&gt;

    &lt;p&gt;&lt;a href=&quot;https://github.com/jlu5/Limnoria-Grapnel&quot;&gt;&lt;strong&gt;Grapnel hooks&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;plugin installs a web API that I can use to post messages from the outside, mainly from automations I build on&lt;/p&gt;

    &lt;p&gt;&lt;a href=&quot;https://make.com&quot;&gt;Make.com&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In total, I spent less than a workday on getting our new community chat platform to a sufficiently usable state. That’s perfectly okay, don’t you think?&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;With little effort, I was able to set up an online chat platform for my DevOps community that gives members a choice of either the convenience of Discord, the flexibility of their own IRC client, or the ease of an IRC web interface. In case I have to ditch Discord after all, there’ll be a well-established alternative chat service in place already. It’s nice to be able to focus on our conversations without worries.&lt;/p&gt;

&lt;p&gt;And you’re welcome to join them, either on &lt;a href=&quot;https://irc.monospacementor.com&quot;&gt;IRC&lt;/a&gt; or on &lt;a href=&quot;https://discord.gg/y36CtcuS2a&quot;&gt;Discord&lt;/a&gt;!&lt;/p&gt;</content><author><name>Jochen Lillich</name></author><category term="changelog" /><summary type="html">Why Discord age verification pushed The Server Room community chat back toward IRC.</summary></entry><entry><title type="html">The systemd age attestation controversy</title><link href="https://www.monospacementor.com/commentary/the-systemd-age-attestation-controversy/" rel="alternate" type="text/html" title="The systemd age attestation controversy" /><published>2026-04-01T13:29:03+01:00</published><updated>2026-04-01T13:29:03+01:00</updated><id>repo://posts.collection/_posts/2026-04-01-the-systemd-age-attestation-controversy.md</id><content type="html" xml:base="https://www.monospacementor.com/commentary/the-systemd-age-attestation-controversy/">&lt;p&gt;In yesterday’s &lt;a href=&quot;https://monospacementor.com/community/&quot;&gt;Office Hour&lt;/a&gt;, we discussed systemd, especially systemd timers. Then the question was raised why there is so much backlash against systemd in some circles. Here’s a short summary of my answer:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;systemd is becoming kind of an “OS within the OS”. That this runs counter to the Unix philosophy’s “small programs working together” rubs people, especially the more puristic ones, the wrong way.&lt;/li&gt;
  &lt;li&gt;systemd author Lennart Pöttering has strong opinions, and some people seem to think that they have to offer even stronger counter-arguments, to put it lightly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Technical differences can and should be debated. We have a wealth of talented developers in the open-source community. By putting their heads together, they have created, and will keep creating, remarkable pieces of software.&lt;/p&gt;

&lt;p&gt;What should never, ever, happen, is that developers get death threats for their decisions. That actually happened after Dylan M. Taylor submitted a &lt;a href=&quot;https://github.com/systemd/systemd/pull/40954&quot;&gt;pull request&lt;/a&gt; to add a field to store (NB: and only store) a user’s birthdate in systemd’s JSON user records. Here’s a &lt;a href=&quot;https://lwn.net/SubscriberLink/1064706/b46aa24028c7455b/&quot;&gt;gift link&lt;/a&gt; to an LWN article that lays out the whole story well.&lt;/p&gt;

&lt;p&gt;I find behaviour as described in the article absolutely unacceptable. In Open Source, we’re a community, and mob mentality has no place in it.&lt;/p&gt;</content><author><name>Jochen Lillich</name></author><category term="commentary" /><category term="Linux" /><category term="OpenSourceSoftware" /><summary type="html">In Open Source, we’re a community, and mob mentality, as was displayed in response to a recent systemd change, has no place in it.</summary></entry><entry><title type="html">Bringing Linux Administration to Everyone: Free Online Course Starting Soon</title><link href="https://www.monospacementor.com/learning/free-linux-sysadmin-course/" rel="alternate" type="text/html" title="Bringing Linux Administration to Everyone: Free Online Course Starting Soon" /><published>2025-09-02T11:58:30+01:00</published><updated>2025-09-02T11:58:30+01:00</updated><id>repo://posts.collection/_posts/2025-09-02-free-linux-sysadmin-course.md</id><content type="html" xml:base="https://www.monospacementor.com/learning/free-linux-sysadmin-course/">&lt;p&gt;I had planned to start the cohort-based course “Basic Linux System Administration” later this week. When this cohort unfortunately didn’t fill up, I realized this was the perfect opportunity to do something I’ve wanted to try for a while: offer my Linux System Administration course completely free as a livestreaming experience.&lt;/p&gt;

&lt;p&gt;Starting this month, I’ll be delivering my complete &lt;strong&gt;“Basic Linux System Administration”&lt;/strong&gt; course through weekly live lectures on my streaming channels. This isn’t just a recording of classroom sessions—it’s a fully interactive learning experience designed specifically for the online format.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2025/09/a-close-up-shot-of-a-hand-holding-a-penguin-sticker-against-a-blurred-outdoor-background.-11035358-1024x683.jpg&quot; alt=&quot;A close-up shot of a hand holding a penguin sticker against a blurred outdoor background.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;what-youll-get&quot;&gt;What You’ll Get&lt;/h2&gt;

&lt;p&gt;The course covers everything you need to get a solid foundation for a system administration or DevOps role. We’ll start with the fundamentals—installing Linux, mastering the command line, and understanding how Linux actually works—then progress through practical skills like user management, process control, networking, and security.&lt;/p&gt;

&lt;p&gt;Each week includes live Q&amp;amp;A sessions where you can get direct answers to your questions, plus assessment quizzes to verify your understanding. The hands-on labs use real Linux systems, so you’ll gain practical experience with the same tools used in production environments.&lt;/p&gt;

&lt;h2 id=&quot;why-im-doing-this&quot;&gt;Why I’m Doing This&lt;/h2&gt;

&lt;p&gt;After decades in software development and managing Linux systems, I’ve seen how transformative these skills can be for someone’s career. The demand for Linux expertise continues to grow, but quality education shouldn’t be locked behind high course fees.&lt;/p&gt;

&lt;p&gt;This streaming format lets me reach people who might not otherwise access this training—whether due to location, schedule constraints, or budget limitations. Plus, I’m genuinely excited to experiment with live online education and build a learning community around practical Linux skills.&lt;/p&gt;

&lt;h2 id=&quot;get-started&quot;&gt;Get Started&lt;/h2&gt;

&lt;p&gt;All the details are on my &lt;a href=&quot;https://monospacementor.com/courses/linsys-1-basic-linux-administration-stream/&quot;&gt;course page&lt;/a&gt;. You can sign up there to receive notifications about lecture schedules and other important updates.&lt;/p&gt;

&lt;p&gt;The first session covers Linux fundamentals and walks you through your first Fedora installation. Whether you’re entirely new to Linux or have some experience but want to formalize your knowledge, this course will give you the solid foundation you need for a successful career in system administration.&lt;/p&gt;

&lt;p&gt;Let’s build something great together. See you in the stream!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;—Jochen, The Monospace Mentor&lt;/em&gt;&lt;/p&gt;</content><author><name>Jochen Lillich</name></author><category term="learning" /><category term="Career" /><category term="Course" /><category term="Learning" /><category term="Linux" /><category term="Skills" /><summary type="html">Why an unfilled Linux administration cohort became a chance to offer the course as a free livestreaming experience.</summary></entry><entry><title type="html">A homelab is a skill booster</title><link href="https://www.monospacementor.com/learning/a-homelab-is-a-skill-booster/" rel="alternate" type="text/html" title="A homelab is a skill booster" /><published>2025-08-09T12:50:04+01:00</published><updated>2025-08-09T12:50:04+01:00</updated><id>repo://posts.collection/_posts/2025-08-09-a-homelab-is-a-skill-booster.md</id><content type="html" xml:base="https://www.monospacementor.com/learning/a-homelab-is-a-skill-booster/">&lt;p&gt;In her article &lt;a href=&quot;https://www.paigerduty.com/sre-biggest-problem/&quot;&gt;“What’s the biggest unsolved problem within Site Reliability Engineering?”&lt;/a&gt;, Paige Cruz identifies a critical challenge facing our industry: how do we pass the hard-won knowledge of veteran system administrators down to a new generation that has never worked outside cloud infrastructure?&lt;/p&gt;

&lt;p&gt;Cruz makes a compelling case that this knowledge transfer gap represents SRE’s biggest unsolved problem. However, I take issue with one of her conclusions. She argues that “It’s also not acceptable or sustainable or accessible in my opinion to continue to expect people set up home-labs and experiment themselves. We don’t ask veterinarians to practice surgery in their free time at home and I don’t think we should expect that from future SRE.”&lt;/p&gt;

&lt;p&gt;I respectfully disagree. A home lab represents the best investment a future SRE can make to secure their career growth.&lt;/p&gt;

&lt;h2 id=&quot;the-home-lab-advantage&quot;&gt;The home lab advantage&lt;/h2&gt;

&lt;p&gt;The veterinarian comparison doesn’t hold up under scrutiny. Veterinarians need live animals, surgical equipment, and sterile environments – resources that are neither practical nor ethical to maintain at home. Particle physicists need billion-dollar accelerators. But what does a sysadmin or SRE need to experiment with infrastructure? A few decommissioned Lenovo ThinkCentre boxes and a network switch.&lt;/p&gt;

&lt;p&gt;The barrier to entry is remarkably low, and the learning potential is enormous.&lt;/p&gt;

&lt;p&gt;Scientific research consistently shows that skill development requires more than passive consumption of learning materials. Growing expertise demands practical exploration – running into problems, analyzing failures, and working through solutions. A home lab provides exactly this environment, free from the constraints and pressures of production systems.&lt;/p&gt;

&lt;p&gt;You don’t need a data centre in your basement. One or a few small networked PCs will serve you well. These machines consume minimal power and don’t need to run around the clock. Fire them up when you want to experiment with a new service mesh, test disaster recovery procedures, or break something just to see what happens.&lt;/p&gt;

&lt;p&gt;For those who can’t or don’t want to maintain physical hardware, alternatives exist. “Pubmox” – the shared Proxmox Virtual Environment I maintain for members of “&lt;a href=&quot;https://monospacementor.com/community/&quot;&gt;The Server Room&lt;/a&gt;” – offers free access to virtual infrastructure for hands-on learning.&lt;/p&gt;

&lt;h2 id=&quot;the-learning-laboratory&quot;&gt;The learning laboratory&lt;/h2&gt;

&lt;p&gt;A home lab creates space for the kind of curiosity-driven exploration that builds deep technical intuition. When you’re troubleshooting a networking issue at 2 AM in your home lab, you’re not worried about affecting customers or violating change management policies. You can take risks, make mistakes, and learn from the consequences.&lt;/p&gt;

&lt;p&gt;This environment encourages the experimental mindset that separates great SREs from those who merely follow runbooks. You learn to ask “what if” questions and then actually test your hypotheses.&lt;/p&gt;

&lt;h2 id=&quot;knowledge-transfer-requires-community&quot;&gt;Knowledge transfer requires community&lt;/h2&gt;

&lt;p&gt;While I advocate strongly for home labs as skill boosters, they’re not the complete answer to Cruz’s original question about knowledge preservation. The solution to transferring veteran sysadmin wisdom doesn’t lie in solitary exploration – it requires collaborative knowledge transfer.&lt;/p&gt;

&lt;p&gt;Home labs excel at building technical skills, but they can’t replace the mentorship and context that comes from working alongside experienced practitioners. The real magic happens when hands-on experimentation combines with guided learning from those who’ve solved these problems before.&lt;/p&gt;

&lt;p&gt;This is why I’m launching “&lt;a href=&quot;https://monospacementor.com/courses/linsys-1/&quot;&gt;Basic Linux System Administration&lt;/a&gt;” – an instructor-led course backed by “The Server Room” community. Participants get free access to Pubmox, allowing them to practice what they learn while benefiting from my decades of system administration experience.&lt;/p&gt;

&lt;p&gt;It’s the best of both worlds: the freedom to experiment and break things, combined with the guidance to understand why systems fail and how to make them more reliable.&lt;/p&gt;

&lt;p&gt;A home lab won’t solve SRE’s knowledge transfer problem on its own. But for individual practitioners looking to deepen their skills and advance their careers, it remains an invaluable investment. The combination of practical experimentation and collaborative learning creates the foundation for the next generation of systems experts our industry desperately needs.&lt;/p&gt;</content><author><name>Jochen Lillich</name></author><category term="learning" /><category term="Learning" /><category term="Linux" /><category term="Skills" /><summary type="html">Why a homelab can help new engineers build the practical operational instincts that cloud-only work often skips.</summary></entry><entry><title type="html">Linux Shell for Beginners: The ls command</title><link href="https://www.monospacementor.com/learning/linux-shell-for-beginners-the-ls-command/" rel="alternate" type="text/html" title="Linux Shell for Beginners: The ls command" /><published>2025-05-21T09:06:13+01:00</published><updated>2025-05-21T09:06:13+01:00</updated><id>repo://posts.collection/_posts/2025-05-21-linux-shell-for-beginners-the-ls-command.md</id><content type="html" xml:base="https://www.monospacementor.com/learning/linux-shell-for-beginners-the-ls-command/">&lt;div style=&quot;position:relative;padding-top:56.25%;&quot;&gt;&lt;iframe src=&quot;https://player.mediadelivery.net/embed/259935/92acda52-0079-4bfb-8954-f96c64b8f729?autoplay=false&amp;amp;loop=false&amp;amp;muted=false&amp;amp;preload=true&amp;amp;responsive=true&quot; loading=&quot;lazy&quot; style=&quot;border:0;position:absolute;top:0;height:100%;width:100%;&quot; allow=&quot;accelerometer;gyroscope;autoplay;encrypted-media;picture-in-picture;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;

&lt;p&gt;Hello DevOps friends! In this tutorial, we’re going to take a look at the &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;There is no Unix or Linux user who doesn’t use it on a daily basis, so I’ll teach you what you need to know.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; command is how you see what’s on your Linux disks. Type &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; in your terminal, and it shows you all the files and folders right there. It’s usually one of the first commands you learn. Pretty simple, but super useful for finding your way around. This does not sound spectacular, right, not something to make a tutorial about?&lt;/p&gt;

&lt;p&gt;Well, I’ve been using Linux since 1993. Would you believe that in preparation of this tutorial, I still learned something new about &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt;? So, even if you are a veteran Linux user, you might take away one or two interesting titbits from this.&lt;/p&gt;

&lt;p&gt;And at the end, I’ll introduce you to a command that takes a fresh and even more powerful approach to what &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; has been doing for decades.&lt;/p&gt;

&lt;p&gt;So let’s dive right in!&lt;/p&gt;

&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;

&lt;p&gt;Because it’s so essential, the &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; command is included in the Core Utilities package that gets installed right when you set up your Linux distribution of choice.&lt;/p&gt;

&lt;p&gt;This is how &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; works by default: When you execute it on its own, it will list the files and folders in your current working directory.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/57yc5qPLNkQgQSpCurPrpjnim.js&quot; id=&quot;asciicast-57yc5qPLNkQgQSpCurPrpjnim&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;Hidden files, as the name implies, will not be shown. On Linux, all files whose names start with a dot are treated as hidden files.&lt;/p&gt;

&lt;p&gt;As you can see, the output of the command is sorted alphabetically, and it’s not immediately obvious which names are files and which are directories. We can improve this by using command options.&lt;/p&gt;

&lt;p&gt;Options are additional arguments that you add after the command name. They change its behaviour, and they all start with a dash.&lt;/p&gt;

&lt;h2 id=&quot;long-listing-format&quot;&gt;Long listing format&lt;/h2&gt;

&lt;p&gt;The option &lt;code class=&quot;highlighter-rouge&quot;&gt;-l&lt;/code&gt; requests the “long listing format”. This format lists each file and directory on its own line with a whole lot of detail information. I’ll cover those details later.&lt;/p&gt;

&lt;p&gt;Let’s see what the long listing format looks like.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/2whUNcUwf1B1dr4J7IizezdxJ.js&quot; id=&quot;asciicast-2whUNcUwf1B1dr4J7IizezdxJ&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;That’s quite a bit more information than before! But apart from the file name and the modification date, it’s not quite obvious what all of this means. I’ll point it out for you.&lt;/p&gt;

&lt;p&gt;Every line in the long format starts with a series of letters and dashes. It represents the file type and its access permissions. We’ll get to them in a minute. The solitary number that follows is the number of links related to the file. You won’t need this information often. The next two items designate the owners of the file. Every file can be assigned to one user and to one user group, and they are both listed here. Finally, there’s the file size and the last modification time before the file name is printed in the last column.&lt;/p&gt;

&lt;p&gt;Let’s tease apart what’s hidden in the permissions pattern at the start of the line!&lt;/p&gt;

&lt;h2 id=&quot;file-permissions&quot;&gt;File permissions&lt;/h2&gt;

&lt;p&gt;The file permissions field is a combination of characters, and it consists of several groups, each of which has its own meaning. The first single character signifies the file type. A &lt;code class=&quot;highlighter-rouge&quot;&gt;d&lt;/code&gt; stands for “directory”, while regular data files are represented with a dash &lt;code class=&quot;highlighter-rouge&quot;&gt;-&lt;/code&gt;. There are other types as well, but they’re not relevant at this stage.&lt;/p&gt;

&lt;p&gt;Following the file type character, there are three groups or sections of three characters each. They display the file permissions. The first section shows the permissions of the owner user, the second one the permissions of the owner group, and the third section the permissions of everyone else. This last section is often referred to as “others”.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2025/05/permissions-1024x576.png&quot; alt=&quot;Breakdown of the file permissions field&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We can see that each of these sections is composed of the letters ‘r’, ‘w’, ‘x’, and the dash ‘-‘. What do they mean?&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2025/05/file_permissions-1024x576.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The ‘r’ stands for ‘read’ permission, which allows users to read the contents of the file or directory.&lt;/p&gt;

&lt;p&gt;Similarly, the ‘w’ allows users to make changes. In case of a directory, making changes to the contents of it means creating and deleting files inside it.&lt;/p&gt;

&lt;p&gt;The ‘x’ permission on a file makes it executable. With this permission, you can execute it from the Linux shell like any other shell command. In the case of a directory, the ‘x’ permission has a slightly different meaning. It allows users to enter the directory and thus make it their working directory.&lt;/p&gt;

&lt;p&gt;If one of these permissions is not given for a specific section, a dash ‘-‘ takes its place.&lt;/p&gt;

&lt;p&gt;These three permissions can be combined freely, but only a few of these combinations are commonly used. For files, you usually see &lt;code class=&quot;highlighter-rouge&quot;&gt;r--&lt;/code&gt; if a file is read-only, or &lt;code class=&quot;highlighter-rouge&quot;&gt;rw-&lt;/code&gt; if write permission is given. For executable files, the &lt;code class=&quot;highlighter-rouge&quot;&gt;x&lt;/code&gt; pops up in the third position. It’s similar for directories. Usually, the &lt;code class=&quot;highlighter-rouge&quot;&gt;x&lt;/code&gt; permission goes hand in hand with read rights, so the two common combinations are &lt;code class=&quot;highlighter-rouge&quot;&gt;r-x&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;rwx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;(Internally, these combinations are represented as octal numbers, but that’s something I’ll cover in detail in my Linux courses, not here.)&lt;/p&gt;

&lt;h2 id=&quot;hidden-files&quot;&gt;Hidden files&lt;/h2&gt;

&lt;p&gt;In the early days of Unix, the operating system developers noticed that more and more system and configuration files were accumulating in their home directories. Some of these files even contained sensitive information, for example, login credentials to some FTP server. To minimize exposure and clutter of the directory listing, the fathers of Unix devised a simple solution: files whose names started with a dot would be treated as hidden files and not displayed by &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; if not explicitly requested.&lt;/p&gt;

&lt;p&gt;This explicit request has the form of the &lt;code class=&quot;highlighter-rouge&quot;&gt;-a&lt;/code&gt; option.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/LWMAhYKN5glv82zrczO2xtDZo.js&quot; id=&quot;asciicast-LWMAhYKN5glv82zrczO2xtDZo&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;On closer inspection, you will notice that among the files this option reveals are always two special directories whose names only consist of dots. The “single dot” directory always represents the directory it’s located in. It allows the user to explicitly reference the current working directory. Similarly, the “double dot” directory stands for the parent of the current working directory. It makes it easy to reference or even switch to the directory one level higher in the directory tree.&lt;/p&gt;

&lt;h2 id=&quot;human-readable-file-sizes&quot;&gt;Human-readable file sizes&lt;/h2&gt;

&lt;p&gt;Among other things, the long listing format also shows the file size. However, it shows it in bytes, and for large files, that’s impractical. “Wait, is that number in the million or in the ten million bytes range?” With the &lt;code class=&quot;highlighter-rouge&quot;&gt;-h&lt;/code&gt; option, you get file sizes in human-readable form, with useful units like MB or GB.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/4oClLLb3rUeOljCPQ1121437i.js&quot; id=&quot;asciicast-4oClLLb3rUeOljCPQ1121437i&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;This option only works in conjunction with the &lt;code class=&quot;highlighter-rouge&quot;&gt;-l&lt;/code&gt; option because otherwise, there’s no file size to display.&lt;/p&gt;

&lt;p&gt;As I mentioned earlier, you can pass multiple options to &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; as separate arguments, or you can combine them into a single multi-option. In both cases, the order in which you pass in the options will not matter. &lt;code class=&quot;highlighter-rouge&quot;&gt;ls -lh&lt;/code&gt; works identically to &lt;code class=&quot;highlighter-rouge&quot;&gt;ls -hl&lt;/code&gt;, for example.&lt;/p&gt;

&lt;h2 id=&quot;sorting&quot;&gt;Sorting&lt;/h2&gt;

&lt;p&gt;The default sort order by name is useful when you’re looking for a specific file. However, other administration tasks will require a different order. &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; has a few options in store.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/0ur9zIl8vsKI7VQY7ctgjH1oH.js&quot; id=&quot;asciicast-0ur9zIl8vsKI7VQY7ctgjH1oH&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;With &lt;code class=&quot;highlighter-rouge&quot;&gt;-t&lt;/code&gt; as in “time”, &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; will sort the file list by modification time, starting with the newest file and ending with the file that hasn’t been touched in the longest time.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/kqFpn0BsMJ7AJ87E68MIUoH94.js&quot; id=&quot;asciicast-kqFpn0BsMJ7AJ87E68MIUoH94&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;When you’re looking for candidates to delete to free up storage space, the &lt;code class=&quot;highlighter-rouge&quot;&gt;-S&lt;/code&gt; with a capital S comes in handy. It’ll list files by size in descending order.&lt;/p&gt;

&lt;p&gt;Speaking of order, you can always reverse the sort order you picked by adding the &lt;code class=&quot;highlighter-rouge&quot;&gt;-r&lt;/code&gt; option. For example, &lt;code class=&quot;highlighter-rouge&quot;&gt;ls -lr&lt;/code&gt; will give you a listing in long format in inverse alphabetical order.&lt;/p&gt;

&lt;h2 id=&quot;recursive-listing&quot;&gt;Recursive listing&lt;/h2&gt;

&lt;p&gt;Sometimes, you need to see the contents not only of your current working directory but also its subdirectories. Maybe you’re looking for a specific file that’s in there somewhere. Maybe you just want to see how much stuff there is.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/HHXQrGcGbFBW6noXVmg7RpdEF.js&quot; id=&quot;asciicast-HHXQrGcGbFBW6noXVmg7RpdEF&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;-R&lt;/code&gt; option with a capital R will help you with that. The R stands for “recursive” because &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; will go down into the first subdirectory and list its contents, then further down until it reaches the bottom, and then will do the same with any other subdirectory until it has traversed them all. Depending on the size of the filesystem hierarchy you’re in, this can take some time and yield quite a lot of output.&lt;/p&gt;

&lt;h2 id=&quot;output-in-colour&quot;&gt;Output in colour&lt;/h2&gt;

&lt;p&gt;At the beginning of this tutorial, I highlighted the fact that with the default output of &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt;, it’s hard to tell which names are files and which are directories. Colour coding makes it easier. To enable it, use the special option &lt;code class=&quot;highlighter-rouge&quot;&gt;--color=auto&lt;/code&gt;. (Note the American spelling with just an “o”.)&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/UbTY6JFUvweOBZbckO8Z3MXOl.js&quot; id=&quot;asciicast-UbTY6JFUvweOBZbckO8Z3MXOl&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;You can even change the default colour scheme, but I’ll leave that as a research exercise for you.&lt;/p&gt;

&lt;h2 id=&quot;listing-specific-files-and-directories&quot;&gt;Listing specific files and directories&lt;/h2&gt;

&lt;script src=&quot;https://asciinema.org/a/J4LvlGMownL7AIVdOR0tdkjKR.js&quot; id=&quot;asciicast-J4LvlGMownL7AIVdOR0tdkjKR&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; command accepts not only options as its arguments but also file and directory names. When you pass in the name of a directory, &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; will list its contents. In the case of a file name, you’ll see its details. Of course, if you don’t also pass in the &lt;code class=&quot;highlighter-rouge&quot;&gt;-l&lt;/code&gt; option, the only detail you’ll see is the file’s name; that’s not very useful.&lt;/p&gt;

&lt;h2 id=&quot;wildcards&quot;&gt;Wildcards&lt;/h2&gt;

&lt;p&gt;Not always can we be totally specific in naming a file or directory. Maybe we only remember the beginning of the name. Or maybe we just want the details of files of a specific type. This is where wildcards come in, or as they’re commonly named, “globbing characters”. They’re characters with a special meaning that act as placeholders for a larger set of possibilities.&lt;/p&gt;

&lt;p&gt;The most simple globbing characters are the asterisk and the question mark. The latter stands for any single character, the former even encompasses any number of arbitrary characters. Note that this number may be zero.&lt;/p&gt;

&lt;p&gt;For example, &lt;code class=&quot;highlighter-rouge&quot;&gt;ls a*&lt;/code&gt; will list all files whose name &lt;strong&gt;is&lt;/strong&gt; “a” or starts with “a”.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/V9XSgw4GEhyFJVn5YsihY3Ugl.js&quot; id=&quot;asciicast-V9XSgw4GEhyFJVn5YsihY3Ugl&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;ls a?&lt;/code&gt; will show you any file whose name has two characters, the first of which is an “a”.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/kD3D0IUD5ynEFBTDkyHJ7K1bF.js&quot; id=&quot;asciicast-kD3D0IUD5ynEFBTDkyHJ7K1bF&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;You can, of course, combine both wildcard characters. The output of the command &lt;code class=&quot;highlighter-rouge&quot;&gt;ls *.??&lt;/code&gt; will be a list of any files that have an extension consisting of two characters, for example &lt;code class=&quot;highlighter-rouge&quot;&gt;abstract.md&lt;/code&gt;.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/qe8whTrL9ORKW2DdB0uwfJlbf.js&quot; id=&quot;asciicast-qe8whTrL9ORKW2DdB0uwfJlbf&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;If we don’t just want to allow any character but a specific set, we can pass it to &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; in square brackets. &lt;code class=&quot;highlighter-rouge&quot;&gt;ls a[12]&lt;/code&gt; will only match the file names &lt;code class=&quot;highlighter-rouge&quot;&gt;a1&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;a2&lt;/code&gt;.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/qlsUFqNx2UljJNEhOVuWXnvmT.js&quot; id=&quot;asciicast-qlsUFqNx2UljJNEhOVuWXnvmT&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;Character sets can also be specified as ranges. For example, the set &lt;code class=&quot;highlighter-rouge&quot;&gt;0-5&lt;/code&gt; in square brackets will match any digit between 0 and 5. However, as you can see, there isn’t an &lt;code class=&quot;highlighter-rouge&quot;&gt;a4&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;a5&lt;/code&gt;.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/t4HGxvhuo9fVV0hD0FhEzrErL.js&quot; id=&quot;asciicast-t4HGxvhuo9fVV0hD0FhEzrErL&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;And by the way, this “globbing” is actually not built into the &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; command. It’s a feature of the shell from which you execute &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt;. But that’s a topic for my Linux and shell scripting courses.&lt;/p&gt;

&lt;p&gt;As you already know, the command &lt;code class=&quot;highlighter-rouge&quot;&gt;ls -l&lt;/code&gt; followed by a file name will display detail information on the file. However, with a directory name, it will list the details of all the files inside the directory. This can be confusing when you’re working with wildcards because it’s hard to predict if there are matching subdirectories. To make sure that you’ll get filesystem details on both files and directories, you add the &lt;code class=&quot;highlighter-rouge&quot;&gt;-d&lt;/code&gt; option.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/vKZDzgxDQp517bOgOZsnHwSOr.js&quot; id=&quot;asciicast-vKZDzgxDQp517bOgOZsnHwSOr&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;As you can see, the first command listed the details of the contents of matching directories, while the second only showed their own details.&lt;/p&gt;

&lt;h2 id=&quot;quiz&quot;&gt;Quiz&lt;/h2&gt;

&lt;p&gt;Let’s do a quick quiz! Take a look at this combination of options. Can you tell how &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; is going to behave?&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2025/05/quiz-1024x576.png&quot; alt=&quot;What does this do? &amp;quot;ls -lhtr /var/log/&amp;quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This command will list all the contents of the directory &lt;code class=&quot;highlighter-rouge&quot;&gt;/var/log&lt;/code&gt;; you can tell that it’s a directory because the path argument ends with a slash. The listing will be in long format with human-readable file sizes. And since it’s sorted by time in reverse order, the files that were changed most recently will be at the bottom.&lt;/p&gt;

&lt;h2 id=&quot;advanced-options&quot;&gt;Advanced options&lt;/h2&gt;

&lt;p&gt;There are a number of advanced options that you won’t use as often but are useful to know.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/xuCJHZpezgtptQZ95p0oyta5C.js&quot; id=&quot;asciicast-xuCJHZpezgtptQZ95p0oyta5C&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;If you want to know the inode related to a file, use the &lt;code class=&quot;highlighter-rouge&quot;&gt;-i&lt;/code&gt; option. If you don’t know what an inode is, don’t worry about it. I’ll cover the topic in my Linux courses.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/7YvN55Ov9ak6wR2c0ub2NvVGf.js&quot; id=&quot;asciicast-7YvN55Ov9ak6wR2c0ub2NvVGf&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;The option &lt;code class=&quot;highlighter-rouge&quot;&gt;-F&lt;/code&gt; with a capital F appends indicator symbols after each file name to make discerning its type easier. It adds a slash (&lt;code class=&quot;highlighter-rouge&quot;&gt;/&lt;/code&gt;) for directories, an asterisk (&lt;code class=&quot;highlighter-rouge&quot;&gt;*&lt;/code&gt;) after each file that is executable, and an at sign (&lt;code class=&quot;highlighter-rouge&quot;&gt;@&lt;/code&gt;) after each symbolic link. There are other symbols for lesser-used file types that I’ll omit here.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/xDylKO6lBnzCnClXaAMl4hrfD.js&quot; id=&quot;asciicast-xDylKO6lBnzCnClXaAMl4hrfD&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;There is also an option that doesn’t have a single-letter form but is useful nonetheless. If you want to see directories listed separately from regular files, try &lt;code class=&quot;highlighter-rouge&quot;&gt;--group-directories-first&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;aliases&quot;&gt;Aliases&lt;/h2&gt;

&lt;p&gt;Once you work in a Linux shell more often, you’ll notice quickly that you’re using &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; very regularly with certain sets of options. Therefore, it’s useful to define shell aliases for them so you can execute them more easily using a shorthand command.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2025/05/aliases1-1024x576.png&quot; alt=&quot;Basic ls aliases:
alias ll=&amp;quot;ls -l&amp;quot;
alias la=&amp;quot;ls -la&amp;quot;
alias lt=&amp;quot;ls -lt&amp;quot;
alias lh=&amp;quot;ls -lh&amp;quot;
alias ltr=&amp;quot;ls -ltr&amp;quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Some of these aliases are so common that they might be already part of the default configuration on your Linux distribution. If not, I recommend you add them, since, for example, typing &lt;code class=&quot;highlighter-rouge&quot;&gt;ltr&lt;/code&gt; is much quicker than typing &lt;code class=&quot;highlighter-rouge&quot;&gt;ls -ltr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2025/05/aliases2-1024x576.png&quot; alt=&quot;More Advanced Aliases:
alias lsd=&apos;ls -ld&apos;
alias lsz=&apos;ls -lhS&apos;
alias lsx=&apos;ls -lX&apos;
alias recent=&apos;ls -lt | head&apos;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Here are a few more suggestions for useful aliases.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;lsd&lt;/code&gt; gives you the details of not only files but also directories that you name in the arguments.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;lsz&lt;/code&gt; alias is easy to remember; it focuses on file size, after all.&lt;/p&gt;

&lt;p&gt;In the &lt;code class=&quot;highlighter-rouge&quot;&gt;lsx&lt;/code&gt; alias, you’ll find an option that I haven’t covered up to now. The &lt;code class=&quot;highlighter-rouge&quot;&gt;-X&lt;/code&gt; option with a capital X does something very useful: It groups files with the same extension. For example, if you have a directory full of different file formats, &lt;code class=&quot;highlighter-rouge&quot;&gt;lsx&lt;/code&gt; will give you a quick overview.&lt;/p&gt;

&lt;h2 id=&quot;alternatives&quot;&gt;Alternatives&lt;/h2&gt;

&lt;p&gt;As you can see, &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; can do a lot of things. But it’s by far not the only command you can use to explore your storage systems. Let’s look at a few companions, or even replacements!&lt;/p&gt;

&lt;p&gt;With &lt;code class=&quot;highlighter-rouge&quot;&gt;tree&lt;/code&gt;, you can get a quick overview of the directory hierarchy. In practice, you might start with a quick invocation of &lt;code class=&quot;highlighter-rouge&quot;&gt;tree&lt;/code&gt;, and then use &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; to zoom in on one or more specific items. Since it’s not installed by default on my machine, I have to install it first.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/f0GuCluB26aw1Aww06B5Bue4v.js&quot; id=&quot;asciicast-f0GuCluB26aw1Aww06B5Bue4v&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;I guess it’s not hard to guess what the &lt;code class=&quot;highlighter-rouge&quot;&gt;find&lt;/code&gt; command does.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/X6WUsWPwoscmZfFctQi2eMOji.js&quot; id=&quot;asciicast-X6WUsWPwoscmZfFctQi2eMOji&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;And it does it well! Instead of trying to scan the output of &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; to find the needle in the haystack of your filesystem, you can tell &lt;code class=&quot;highlighter-rouge&quot;&gt;find&lt;/code&gt; exactly what you’re looking for, and it’ll go and search it for you. I’ll cover the &lt;code class=&quot;highlighter-rouge&quot;&gt;find&lt;/code&gt; command in its own tutorial.&lt;/p&gt;

&lt;p&gt;Finally, there are also modern implementations that cover the functionality of &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; &lt;em&gt;and then some&lt;/em&gt;. &lt;code class=&quot;highlighter-rouge&quot;&gt;lsd&lt;/code&gt; is one of them. Note that this is not the shell alias I mentioned earlier. It’s a command that you have to install yourself.&lt;/p&gt;

&lt;p&gt;The same goes for &lt;code class=&quot;highlighter-rouge&quot;&gt;exa&lt;/code&gt;, and I’d like to give you a few more details on this useful add-on command.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;exa&lt;/code&gt; is a modern alternative to &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; with enhanced features. It supports all of the common &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; options. Additionally, it comes with useful features enabled by default. For example, it can use colours and file type icons to make its output much easier to scan.&lt;/p&gt;

&lt;p&gt;Like many modern takes on traditional Unix commands, &lt;code class=&quot;highlighter-rouge&quot;&gt;exa&lt;/code&gt; is written in Rust, so you don’t have to worry that its bells and whistles come at the expense of performance.&lt;/p&gt;

&lt;p&gt;You can install &lt;code class=&quot;highlighter-rouge&quot;&gt;exa&lt;/code&gt; from Github, but by now, it’s found its way into many of the common Linux distributions. On my Debian system, I can simply install it via &lt;code class=&quot;highlighter-rouge&quot;&gt;apt install exa&lt;/code&gt;.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/NXFuIIISvZOM0tJ99wriTr5GY.js&quot; id=&quot;asciicast-NXFuIIISvZOM0tJ99wriTr5GY&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;By the way, that’s the reason why I’m still covering &lt;code class=&quot;highlighter-rouge&quot;&gt;exa&lt;/code&gt; here, although unfortunately, it’s not maintained any more. The community has already filled the gap with a fork named &lt;code class=&quot;highlighter-rouge&quot;&gt;eza&lt;/code&gt; with a “zed”, but its installation isn’t quite as straightforward yet.&lt;/p&gt;

&lt;p&gt;Let’s look at a few examples of what &lt;code class=&quot;highlighter-rouge&quot;&gt;exa&lt;/code&gt; does better than &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt;.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/NTrfOaeiTZFjiGaV1gtJxSt0w.js&quot; id=&quot;asciicast-NTrfOaeiTZFjiGaV1gtJxSt0w&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;exa&lt;/code&gt; has its own long output format, of course. But it includes additional information that &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; doesn’t provide. In this example, it outputs a list of files that has an explanatory header at the top and, in addition to the familiar details, includes each file’s inode number. As DevOps people, you will especially appreciate that it also shows each file’s current Git status.&lt;/p&gt;

&lt;p&gt;I mentioned the &lt;code class=&quot;highlighter-rouge&quot;&gt;tree&lt;/code&gt; command earlier. &lt;code class=&quot;highlighter-rouge&quot;&gt;exa&lt;/code&gt; can replace it, too.&lt;/p&gt;

&lt;script src=&quot;https://asciinema.org/a/3yveoMTSPmITRSIGhYZwEHMe8.js&quot; id=&quot;asciicast-3yveoMTSPmITRSIGhYZwEHMe8&quot; async=&quot;true&quot;&gt;&lt;/script&gt;

&lt;p&gt;In this example, it traverses the directory hierarchy up to two levels down, and display all its files with their details as a neatly formatted tree.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;All right! That’s the &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; command. It’s an essential command on every Unix and Linux system. Its options allow you to customize its output. Check out the system manual page for &lt;code class=&quot;highlighter-rouge&quot;&gt;ls&lt;/code&gt; to learn more. With shell redirection and pipes, you can process this output further. Check out my Linux courses on how this works exactly.&lt;/p&gt;

&lt;p&gt;And with modern re-implementations like &lt;code class=&quot;highlighter-rouge&quot;&gt;eza&lt;/code&gt;, you get additional features such as file type icons, tree display and colour shading by file size.&lt;/p&gt;

&lt;p&gt;That’s it for this time; I hope that it was useful for you. For more DevOps learning content, check out my website &lt;code class=&quot;highlighter-rouge&quot;&gt;monospacementor.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you’d like to grow your skills quickly with the support of me and other DevOps practitioners, consider becoming a member of The Server Room, my online community. Your membership makes it possible for me to do more teaching in the open source community. For details on all the learning content and community events waiting for you, visit &lt;code class=&quot;highlighter-rouge&quot;&gt;theserverroom.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our community forum is the ideal place to ask any questions you might have.&lt;/p&gt;

&lt;p&gt;Until next time! Take care.&lt;/p&gt;</content><author><name>Jochen Lillich</name></author><category term="learning" /><category term="Linux" /><category term="Shell" /><summary type="html">A beginner-friendly walkthrough of the Linux `ls` command and how to read directory listings.</summary></entry><entry><title type="html">A community that becomes your cohort of mentors</title><link href="https://www.monospacementor.com/learning/a-community-that-becomes-your-cohort-of-mentors/" rel="alternate" type="text/html" title="A community that becomes your cohort of mentors" /><published>2025-05-06T12:57:42+01:00</published><updated>2025-05-06T12:57:42+01:00</updated><id>repo://posts.collection/_posts/2025-05-06-a-community-that-becomes-your-cohort-of-mentors.md</id><content type="html" xml:base="https://www.monospacementor.com/learning/a-community-that-becomes-your-cohort-of-mentors/">&lt;p&gt;I do offer &lt;a href=&quot;https://monospacementor.com/engineering-mentorship/&quot;&gt;mentorship and coaching&lt;/a&gt;, but I’m well aware that 1:1 coaching isn’t for everyone. That’s why I was nodding heavily while I read Jacob Evans’s article “&lt;a href=&quot;https://dev.to/jacobmgevans/mentorship-to-me-fg7&quot;&gt;Mentorship To Me&lt;/a&gt;” earlier today. In it, he explains that he learned from a lot of people, but most weren’t “people who signed up for the job”. What he means is, they didn’t call themselves “mentors”. They simply chose to share their knowledge online, and were happy to take others along on their own learning journey.&lt;/p&gt;

&lt;p&gt;Jacob appreciates that he can tap many sources to feed his personal growth:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Being part of Cloudflare, T3 ecosystem, Theo’s, Kent C Dodd’s, and the various dev communities I’m in has taught me that the best mentorship is often ambient, it’s absorbing knowledge from many sources rather than one guru.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is exactly why I founded &lt;a href=&quot;https://monospacementor.com/community/&quot;&gt;The Server Room&lt;/a&gt;, my DevOps community of practice, two years ago. I’m on our community forum and our chat every day to help community members grow. But this community is different to my Office Hour live streams, where people come to ask questions and I try to give them useful answers. Our community has a far more complex and beneficial structure of knowledge flow and peer support.&lt;/p&gt;

&lt;p&gt;The following list contains just a few examples of the interactions happening in The Server Room. None of them require anyone wearing a “mentor” label.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;On the forum, a community member shares their growth goals. Others not only give helpful pointers, but also follow up later to hold them accountable.&lt;/li&gt;
  &lt;li&gt;During a “Hot Seat” session, a community member presents different options they’re considering as a solution to a problem. The other participants give them feedback based on their own experience.&lt;/li&gt;
  &lt;li&gt;In chat, a community member shares their latest learnings, and others congratulate them on their progress. They also suggest possible next steps to tackle.&lt;/li&gt;
  &lt;li&gt;A community member writes a forum post about the fun they’re having with a certain technology. Someone replies that it gave them the confidence to finally give it a try themselves.&lt;/li&gt;
  &lt;li&gt;During a community workshop, a small sidenote turns out to be the missing puzzle piece that a participant had been searching for weeks.&lt;/li&gt;
  &lt;li&gt;A community member describes a conflict they’re having at work. Others who had similar experiences provide them with both emotional support and actionable advice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When people join The Server Room, they don’t sign up to a support forum where some lone veteran engineer answers their questions. They join a community of peers from which they can get help, and in turn are expected to make their own contributions themselves. This way, we create the proverbial rising tide that floats all boats.&lt;/p&gt;

&lt;p&gt;Jacob put it really well in his conclusion (emphasis mine):&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;So yeah, traditional mentorship has its place, but &lt;strong&gt;don’t sleep on the power of community-based learning and peer networks&lt;/strong&gt;. Sometimes the best guidance comes from the people right next to you in the trenches.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If this insight resonates with you, as it did with me, please consider joining &lt;a href=&quot;https://monospacementor.com/community/&quot;&gt;The Server Room&lt;/a&gt;!&lt;/p&gt;</content><author><name>Jochen Lillich</name></author><category term="learning" /><category term="Community" /><category term="Learning" /><summary type="html">While traditional mentorship has its value, sometimes a group of helpful peers can be a great way to grow. The Server Room is such a community.</summary></entry><entry><title type="html">Self-Hosting Web Applications Using Podman</title><link href="https://www.monospacementor.com/learning/self-hosting-web-applications-using-podman/" rel="alternate" type="text/html" title="Self-Hosting Web Applications Using Podman" /><published>2025-02-25T15:52:36+00:00</published><updated>2025-02-25T15:52:36+00:00</updated><id>repo://posts.collection/_posts/2025-02-25-self-hosting-web-applications-using-podman.md</id><content type="html" xml:base="https://www.monospacementor.com/learning/self-hosting-web-applications-using-podman/">&lt;p&gt;Container-based deployment is a neat way to self-host an application. However, I always try not to go overboard with complexity. That’s why I won’t immediately join the chorus of Kubernetes disciples when members of my DevOps community ask me how to deploy container workloads. Searching for viable alternatives, I recently explored the much simpler alternative of Podman and Quadlet during a few of my &lt;a href=&quot;/live-stream&quot;&gt;live streams&lt;/a&gt;. In this post, I will tell you about the results:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;why I use a URL-shortener&lt;/li&gt;
  &lt;li&gt;why I host it myself&lt;/li&gt;
  &lt;li&gt;how to run Kutt in a Podman pod&lt;/li&gt;
  &lt;li&gt;how to expose it to the internet using Nginx Proxy Manager&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;how to manage it all using&lt;/p&gt;

    &lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;systemd&lt;/code&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;how to safely store secrets in Podman&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Are you with me? Let’s go!&lt;/p&gt;

&lt;h2 id=&quot;why-i-use-a-url-shortener&quot;&gt;Why I use a URL shortener&lt;/h2&gt;

&lt;p&gt;In my blog posts, course materials, and in chats, I use links to other useful information all the time. But their URLs are not always easy to type or remember, neither for me nor for my audience. A URL-shortener is a useful service that allows me to construct URLs that are much easier to memorize and type. Usually, they consist of one or a few simple words, prepended with a short domain name. The service then redirects visitors of these short URLs to the actual content. It’s a quality-of-life improvement for everyone.&lt;/p&gt;

&lt;h2 id=&quot;why-i-run-it-myself&quot;&gt;Why I run it myself&lt;/h2&gt;

&lt;p&gt;URL-shorteners are not new. They started to be widely used when Twitter popularized micro-posts as a new type of social media. Its constraint with the 280-character limit discouraged users from including long URLs. Google’s introduction of &lt;a href=&quot;https://en.wikipedia.org/wiki/UTM_parameters&quot;&gt;UTM Tracking&lt;/a&gt; via query parameters made this issue even worse for marketers. URL-shorteners like bit.ly offered a workaround.&lt;/p&gt;

&lt;p&gt;So why do I not simply use one of the existing URL shorteners? After all, I usually try to resist the &lt;a href=&quot;https://en.wikipedia.org/wiki/Not_invented_here&quot;&gt;NIH Syndrome&lt;/a&gt;, and rather make running services I won’t monetize Someone Else’s Problem™.&lt;/p&gt;

&lt;p&gt;Well, turns out that those other people increasingly tend to &lt;em&gt;make it my problem&lt;/em&gt; again. The two most common ways they do so is &lt;em&gt;The Sunset&lt;/em&gt; and &lt;em&gt;Platform Decay&lt;/em&gt; (aka Enshittification). Last year, Google announced the shutdown of their popular URL shortener goog.le, which will cause a tremendous amount of “link rot”, which is a term describing links in existing web documents turning invalid and unusable. And just the other day, bit.ly informed its users that links created with its free plan will soon stop sending an immediate redirect and instead show an interstitial page with ads.&lt;/p&gt;

&lt;p&gt;In this day and age, we can rely on corporate web products less and less, which is why we have to take matters into our own hands. “&lt;strong&gt;Self-hosting&lt;/strong&gt;” is the way to go if we want to stay in control of how our web content is distributed.&lt;/p&gt;

&lt;h2 id=&quot;run-your-own-url-shortener-with-kutt&quot;&gt;Run your own URL shortener with Kutt&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/thedevs-network/kutt&quot;&gt;Kutt&lt;/a&gt;&lt;/strong&gt; is a great open source URL-shortening service that you can host yourself. You can use it to define your own shortcuts, and you can also modify an existing shortcut’s destination URL. So not only does the longevity of your URLs with Kutt depend solely on how long you’re willing to run your own service, it’s also extended by your ability to update them when their target URLs need to change.&lt;/p&gt;

&lt;p&gt;Running Kutt is straightforward by using its container image. I’m using &lt;strong&gt;Podman&lt;/strong&gt; to run it on a cheap virtual private server (VPS) on Hetzner Cloud. I chose Podman because it requires the least amount of effort and resources of all the container deployment solutions I’ve used so far.&lt;/p&gt;

&lt;p&gt;In addition to Kutt, we also need a proxy server that exposes the service on the web safely with TLS encryption. We’re going to use the popular &lt;a href=&quot;https://nginxproxymanager.com/&quot;&gt;Nginx Proxy Manager&lt;/a&gt; for this purpose. It comes with a nice web UI that’ll make it easy to add more services at a later time.&lt;/p&gt;

&lt;h2 id=&quot;setting-up-podman&quot;&gt;Setting up Podman&lt;/h2&gt;

&lt;p&gt;By now, most current Linux distributions provide Podman out of the box. You will probably be able to install it simply using your distribution’s package manager. I installed my VPS with Fedora because it provides very recent versions of the tool.&lt;/p&gt;

&lt;h2 id=&quot;a-kutt-in-a-pod&quot;&gt;A Kutt in a pod&lt;/h2&gt;

&lt;p&gt;For the deployment of Kutt, I found a well-built &lt;a href=&quot;https://code.dbt3ch.com/CJXNRrcr&quot;&gt;Compose file&lt;/a&gt; by YouTuber DB Tech. Compose files are a neat way to define a set of containers that together form an application. In this case, we have containers for the Kutt web application and for the Redis database it uses for caching. Here is the full Compose file. It’s in YAML format and defines two “services”, &lt;code class=&quot;highlighter-rouge&quot;&gt;server&lt;/code&gt; (the Kutt web application), and &lt;code class=&quot;highlighter-rouge&quot;&gt;redis&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;services:
  server:
    image: kutt/kutt:latest
    volumes:
      - db_data_sqlite:/var/lib/kutt
      - custom:/kutt/custom
    environment:
      DB_FILENAME: &quot;/var/lib/kutt/data.sqlite&quot;
      REDIS_ENABLED: &apos;true&apos;
      REDIS_HOST: redis
      REDIS_PORT: 6379
      JWT_SECRET: 0F5B2J0CcoaFIDehV14ngwd6Lp2YbyJM7YeFVkILB1 #random string of characters
      DEFAULT_DOMAIN: yourwebsite.com #no http or https
    ports:
      - 3000:3000
    depends_on:
      redis:
        condition: service_started
  redis:
    image: redis:alpine
    restart: always
    expose:
      - 6379
volumes:
  db_data_sqlite:
  custom:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I copied this definition to a file named &lt;code class=&quot;highlighter-rouge&quot;&gt;compose.yml&lt;/code&gt; in a project directory I called &lt;code class=&quot;highlighter-rouge&quot;&gt;kutt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Inside the original file, I had to make a slight alteration to line 9 with the &lt;code class=&quot;highlighter-rouge&quot;&gt;REDIS_ENABLED&lt;/code&gt; setting because of an &lt;a href=&quot;https://github.com/containers/podman-compose/issues/1022&quot;&gt;issue&lt;/a&gt; that requires boolean values to be quoted when using Podman.&lt;/p&gt;

&lt;p&gt;Podman’s &lt;code class=&quot;highlighter-rouge&quot;&gt;compose&lt;/code&gt; sub-command supports Compose files by invoking either &lt;code class=&quot;highlighter-rouge&quot;&gt;docker-compose&lt;/code&gt;, if it’s installed, or &lt;code class=&quot;highlighter-rouge&quot;&gt;podman-compose&lt;/code&gt;. We’ll use the latter because it supports “pods”. Podman can group the service containers of an application in a so-called &lt;strong&gt;pod&lt;/strong&gt;, just like its bigger cousin &lt;strong&gt;Kubernetes&lt;/strong&gt;. Containers inside a pod can talk to each other via &lt;code class=&quot;highlighter-rouge&quot;&gt;localhost&lt;/code&gt;, which simplifies many things. (And complicates others, as we’ll see later.)&lt;/p&gt;

&lt;p&gt;First, let’s make sure we’ll be using &lt;code class=&quot;highlighter-rouge&quot;&gt;podman-compose&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;export PODMAN_COMPOSE_PROVIDER=/usr/bin/podman-compose
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now we can start the pod:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;podman compose --in-pod true up -d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Because I named the project directory &lt;code class=&quot;highlighter-rouge&quot;&gt;kutt&lt;/code&gt;, Podman automatically created a pod called &lt;code class=&quot;highlighter-rouge&quot;&gt;pod_kutt&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ podman pod ls
POD ID        NAME        STATUS      CREATED     INFRA ID    # OF CONTAINERS
94387ff1aabd  pod_kutt    Running     2 minutes ago              2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Inside this pod, Podman launched the two service containers, &lt;code class=&quot;highlighter-rouge&quot;&gt;kutt_redis_1&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;kutt_server_1&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ podman compose ps
CONTAINER ID  IMAGE                           COMMAND               CREATED        STATUS        PORTS                   NAMES
2bdf802c7739  docker.io/library/redis:alpine  redis-server          2 minutes ago  Up 2 minutes  6379/tcp                kutt_redis_1
8863e07584ae  docker.io/kutt/kutt:latest      /bin/sh -c npm ru...  2 minutes ago  Up 2 minutes  0.0.0.0:3000-&amp;gt;3000/tcp  kutt_server_1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;At this point, I was able to access Kutt via &lt;code class=&quot;highlighter-rouge&quot;&gt;http://localhost:3000&lt;/code&gt; from both my browser and the command line:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ curl http://localhost:3000
Found. Redirecting to /create-admin
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Success!&lt;/p&gt;

&lt;h2 id=&quot;adding-nginx-proxy-manager&quot;&gt;Adding Nginx Proxy Manager&lt;/h2&gt;

&lt;p&gt;For deploying Nginx Proxy Manager, I followed the same approach. I took the &lt;a href=&quot;https://nginxproxymanager.com/setup/&quot;&gt;Compose file&lt;/a&gt; from the project website, copied it to its own subdirectory, and spun it up within a new pod.&lt;/p&gt;

&lt;p&gt;However, I first had to take care of two requirements: The Compose file expects two subdirectories as storage volumes, and it needs to be able to bind the web application to the host’s ports 80 and 443, which requires privileges a normal user doesn’t have.&lt;/p&gt;

&lt;p&gt;The subdirectories are easy to create:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mkdir data letsencrypt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To allow the port allocation, we need root privileges:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo sh -c &quot;echo 0 &amp;gt; /proc/sys/net/ipv4/ip_unprivileged_port_start&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now we can deploy Nginx Proxy Manager:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;podman compose --in-pod true up -d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Let’s test the web access:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ curl http://localhost
...
&amp;lt;h1&amp;gt;Congratulations!&amp;lt;/h1&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Another success! But we’re not going to set up our URL-shortener just yet. Let’s first hand over management to &lt;code class=&quot;highlighter-rouge&quot;&gt;systemd&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;integrating-containers-into-systemd-with-quadlet&quot;&gt;Integrating containers into systemd with Quadlet&lt;/h2&gt;

&lt;p&gt;While the deployment with &lt;code class=&quot;highlighter-rouge&quot;&gt;podman compose&lt;/code&gt; worked, we don’t want to have to manually issue commands every time we restart our host machine. From a system administration standpoint, an application that’s deployed in containers ideally should be handled just like any other service. It should be managed by &lt;code class=&quot;highlighter-rouge&quot;&gt;systemd&lt;/code&gt;, which will allow you to do common tasks like &lt;code class=&quot;highlighter-rouge&quot;&gt;systemctl start kutt&lt;/code&gt; to start the service, and &lt;code class=&quot;highlighter-rouge&quot;&gt;systemctl enable kutt&lt;/code&gt; to get it started automatically at boot time. And fortunately, Podman has this integration with &lt;code class=&quot;highlighter-rouge&quot;&gt;systemd&lt;/code&gt; in the form of Quadlet.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;systemd&lt;/code&gt; uses so-called &lt;strong&gt;unit files&lt;/strong&gt; to define the system services it has to manage. With Quadlet, we get a few new types of unit files that interface directly with Podman to run container-based infrastructure. Quadlet files live in &lt;code class=&quot;highlighter-rouge&quot;&gt;/etc/containers/systemd&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each Quadlet file in turn references a service unit file that can, among other things, run a single container or a whole pod. In summary, we will need a pod unit definition for each service, and a Quadlet file to have it managed by &lt;code class=&quot;highlighter-rouge&quot;&gt;systemd&lt;/code&gt;. Since we want to deploy both Kutt and Nginx Proxy Manager, we will need four files in total.&lt;/p&gt;

&lt;p&gt;However, Quadlet doesn’t use Compose files as service units. Instead, it reads pod manifests, a different YAML format used by the resource orchestration software &lt;strong&gt;Kubernetes&lt;/strong&gt;. Fortunately, Podman makes it easy to generate such a file right from an already running pod. We just need its name or ID.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ podman pod ls
POD ID        NAME         STATUS      CREATED         INFRA ID    # OF CONTAINERS
9414b40c01c7  pod_nginxpm  Running     11 minutes ago              1
94387ff1aabd  pod_kutt     Running     18 minutes ago              2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Let’s generate the pod definition file for Kutt:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;podman kube generate pod_kutt | sudo tee /etc/containers/systemd/kutt.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And for Nginx Proxy Manager:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;podman kube generate pod_nginxpm | sudo tee /etc/containers/systemd/nginxpm.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;making-everything-pretty&quot;&gt;Making everything pretty&lt;/h2&gt;

&lt;p&gt;These pod definition files will work as generated by Podman, but I found the automatic naming less than ideal. First, I removed the initial comments and annotations, and shortened the pod and container names. I also assigned uniform volume names, and made sure their associated physical volume claims (PVC), which is how Kubernetes provides storage space for volumes, followed Kubernetes naming rules.&lt;/p&gt;

&lt;p&gt;Here’s my &lt;code class=&quot;highlighter-rouge&quot;&gt;kutt.yml&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;---
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: &quot;2025-02-20T15:53:07Z&quot;
  labels:
    app: kutt
  name: kutt
spec:
  containers:
  - args:
    - redis-server
    image: docker.io/library/redis:alpine
    name: redis1
    volumeMounts:
    - mountPath: /data
      name: kutt-redis-pvc
  - args:
    - /bin/sh
    - -c
    - npm run migrate &amp;amp;&amp;amp; npm start
    env:
    - name: REDIS_HOST
      value: redis
    - name: JWT_SECRET
      value: 0F5B2J0CcoaFIDehV14ngwd6Lp2YbyJM7YeFVkILB1
    - name: DEFAULT_DOMAIN
      value: mspc.sh
    - name: DB_FILENAME
      value: /var/lib/kutt/data.sqlite
    - name: REDIS_ENABLED
      value: &quot;true&quot;
    - name: REDIS_PORT
      value: &quot;6379&quot;
    image: docker.io/kutt/kutt:latest
    name: server
    ports:
    - containerPort: 3000
      hostPort: 8000
      hostIP: 127.0.0.1
    volumeMounts:
    - mountPath: /var/lib/kutt
      name: kutt-sqlite-pvc
    - mountPath: /kutt/custom
      name: kutt-custom-pvc
  volumes:
  - name: kutt-redis-pvc
    persistentVolumeClaim:
      claimName: kutt-redis
  - name: kutt-sqlite-pvc
    persistentVolumeClaim:
      claimName: kutt-sqlite
  - name: kutt-custom-pvc
    persistentVolumeClaim:
      claimName: kutt-custom
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I set the value for &lt;code class=&quot;highlighter-rouge&quot;&gt;DEFAULT_DOMAIN&lt;/code&gt; (line 28) to “mspc.sh”; a URL shortener should have a short domain name, after all. I also changed the host port mapping to port 8000 because I think it’s a better fit for HTTP-based services.&lt;/p&gt;

&lt;p&gt;Speaking of port mapping, you can see that I also added &lt;code class=&quot;highlighter-rouge&quot;&gt;hostIP: 127.0.0.1&lt;/code&gt; in line 40. This connects the web application to the host’s &lt;code class=&quot;highlighter-rouge&quot;&gt;localhost&lt;/code&gt; interface, but not to its external NIC. This way, it will not be reachable from the internet; we want to let Nginx Proxy Manager take care of that.&lt;/p&gt;

&lt;p&gt;For the Nginx Proxy Manager pod, I did the same clean-up, and also replaced the directory-based volumes with physical volume claims:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;---
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: &quot;2025-02-20T15:59:42Z&quot;
  labels:
    app: nginxpm
  name: nginxpm
spec:
  containers:
  - image: docker.io/jc21/nginx-proxy-manager:latest
    name: nginxpm
    ports:
    - containerPort: 80
      hostPort: 80
    - containerPort: 81
      hostPort: 81
    - containerPort: 443
      hostPort: 443
    volumeMounts:
    - mountPath: /data
      name: nginxpm-data-pvc
    - mountPath: /etc/letsencrypt
      name: nginxpm-letsencrypt-pvc
  volumes:
  - name: nginxpm-data-pvc
    persistentVolumeClaim:
      claimName: nginxpm-data
  - name: nginxpm-letsencrypt-pvc
    persistentVolumeClaim:
      claimName: nginxpm-letsencrypt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;At this point, we can get rid of the Compose deployments we used to get here. Simply execute &lt;code class=&quot;highlighter-rouge&quot;&gt;podman compose down&lt;/code&gt; for both Kutt and Nginx Proxy Manager, and remove the volume directories as well.&lt;/p&gt;

&lt;p&gt;To launch the two services using our newly created pod definition files, we use the command &lt;code class=&quot;highlighter-rouge&quot;&gt;podman kube play&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ sudo podman kube play /etc/containers/systemd/kutt.yml
...
Pod:
465c57697148a9a737bf966b272a01b785d2674da8de990d0095093d34546128
Containers:
c84fdb5d07d7f16de2a79c606910bbe825fd7f09dcd76a542e02e824fc00f4e9
430eb4c1adb85096727f43dd159187e748d3babf59bab830ebd94e2d6ec2c36b
$ sudo podman kube play /etc/containers/systemd/nginxpm.yml
...
Pod:
b2d82d01d13fa4dae26bd177d2a3b88f6084907238ecb98c54150466c4c9f207
Container:
8e21f38468807eec2d0024c8b8f49724c8a8f97f0a23ee311f85ef0ee192c84d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As you can tell from the output, Podman created a pod and two containers for Kutt, and another pod and a single container for Nginx Proxy Manager; just as expected.&lt;/p&gt;

&lt;h2 id=&quot;turning-a-pod-into-a-systemd-unit&quot;&gt;Turning a pod into a systemd unit&lt;/h2&gt;

&lt;p&gt;The final step is to hand over management of these pods to &lt;code class=&quot;highlighter-rouge&quot;&gt;systemd&lt;/code&gt;. All we need for this are two Quadlet files. Since they reference pod definitions, their names both end in &lt;code class=&quot;highlighter-rouge&quot;&gt;.kube&lt;/code&gt;. I had to create both files as &lt;code class=&quot;highlighter-rouge&quot;&gt;root&lt;/code&gt; because the &lt;code class=&quot;highlighter-rouge&quot;&gt;/etc&lt;/code&gt; directory is superuser territory.&lt;/p&gt;

&lt;p&gt;For Kutt, there’s &lt;code class=&quot;highlighter-rouge&quot;&gt;/etc/containers/systemd/kutt.kube&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[Unit]
Description=The kutt URL shortener
Wants=network-online.target
After=network-online.target

[Kube]
Yaml=kutt.yml

[Service]
TimeoutStartSec=900

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Similarly, we have &lt;code class=&quot;highlighter-rouge&quot;&gt;/etc/containers/systemd/nginxpm.kube&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[Unit]
Description=Nginx Proxy Manager
Wants=network-online.target
After=network-online.target

[Kube]
Yaml=nginxpm.yml
Network=host

[Service]
TimeoutStartSec=900

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This second file has a tiny but important difference to the first. It prevents the pod from being isolated in its own container network; instead, it is bound to the host machine’s network interfaces. This is necessary because, as I explained above, the port 8000 that is connected with Kutt is bound exclusively to the host’s &lt;code class=&quot;highlighter-rouge&quot;&gt;localhost&lt;/code&gt; interface. Without the &lt;code class=&quot;highlighter-rouge&quot;&gt;Network=host&lt;/code&gt; setting in line 8, the Nginx Proxy Manager pod would get its own &lt;code class=&quot;highlighter-rouge&quot;&gt;localhost&lt;/code&gt; interface, and this one wouldn’t know anything about Kutt.&lt;/p&gt;

&lt;p&gt;Here’s a diagram that visualizes how the &lt;code class=&quot;highlighter-rouge&quot;&gt;kutt&lt;/code&gt; pod has its own pod-internal localhost interface that the &lt;code class=&quot;highlighter-rouge&quot;&gt;kutt&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;redis&lt;/code&gt; containers use to communicate. Only port 8000 gets forwarded from the Podman host into this pod. The &lt;code class=&quot;highlighter-rouge&quot;&gt;nginxpm&lt;/code&gt; pod, on the other hand, is connected directly to the host’s network interfaces.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2025/02/kutt-pod-network.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This allows us to set up a proxy host in Nginx Proxy Manager that forwards incoming requests to the Kutt container in the other pod.&lt;/p&gt;

&lt;h2 id=&quot;putting-it-all-together&quot;&gt;Putting it all together&lt;/h2&gt;

&lt;p&gt;Now that we have both Quadlet files and pod manifests in place, we can let &lt;code class=&quot;highlighter-rouge&quot;&gt;systemd&lt;/code&gt; take over:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ sudo systemctl daemon-reload
$ sudo systemctl restart kutt
$ sudo systemctl restart nginxpm
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As the final installation step after doing a quick initial setup of Nginx Proxy Manager by visiting my host machine in the browser, I set up a proxy host for the Kutt container bound to &lt;code class=&quot;highlighter-rouge&quot;&gt;localhost:8000&lt;/code&gt; with my desired domain name. Since I also enabled SSL support, everyone can now safely access my URL-shortener.&lt;/p&gt;

&lt;div class=&quot;wp-block-kadence-column kadence-column3141_6e8c42-e9&quot;&gt;&lt;div class=&quot;kt-inside-inner-col&quot;&gt;
&lt;figure class=&quot;wp-block-image size-full has-custom-border&quot; style=&quot;margin-top:0;margin-right:0;margin-bottom:0;margin-left:0&quot;&gt;&lt;img src=&quot;/images/posts/2025/02/kutt-nginxpm-settings.png&quot; alt=&quot;&quot; class=&quot;has-border-color has-theme-palette-6-border-color wp-image-3105&quot; style=&quot;border-width:3px&quot; /&gt;&lt;/figure&gt;
&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;handling-secrets-properly&quot;&gt;Handling secrets properly&lt;/h2&gt;

&lt;p&gt;As a bonus, we can make a small security improvement. One thing I didn’t like about the Kutt manifest is that it contained &lt;code class=&quot;highlighter-rouge&quot;&gt;JWT_SECRET&lt;/code&gt; in plain text (just like the original Compose file did). I don’t regard this as good operations practice, given that Podman can store secrets in a safer way.&lt;/p&gt;

&lt;p&gt;To do so, I created a Kubernetes &lt;code class=&quot;highlighter-rouge&quot;&gt;secret&lt;/code&gt; definition file named &lt;code class=&quot;highlighter-rouge&quot;&gt;secret.yml&lt;/code&gt; containing a long, randomly generated &lt;code class=&quot;highlighter-rouge&quot;&gt;jwtSecret&lt;/code&gt; in line 3:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;apiVersion: v1
data:
  jwtSecret: ZWlUbzl0aG9vZmFoR2gyUXVh5nb0g2YWhkb2hhZXoK
kind: Secret
metadata:
  creationTimestamp: null
  name: kutt-jwt-secret
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After I had given the file’s contents into Podman’s safety, it wasn’t needed any more.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ podman kube play secret.yml
Secrets:
3bfa1f97029464c220cd699ea
$ rm secret.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In &lt;code class=&quot;highlighter-rouge&quot;&gt;kutt.yml&lt;/code&gt;, I then replaced the hard-coded value of &lt;code class=&quot;highlighter-rouge&quot;&gt;JWT_SECRET&lt;/code&gt; with a reference to the key &lt;code class=&quot;highlighter-rouge&quot;&gt;jwtSecret&lt;/code&gt; of the &lt;code class=&quot;highlighter-rouge&quot;&gt;kutt-jwt-secret&lt;/code&gt;. The resulting &lt;code class=&quot;highlighter-rouge&quot;&gt;env&lt;/code&gt; section now looks like this:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    env:
    - name: REDIS_HOST
      value: redis
    - name: JWT_SECRET
      valueFrom:
        secretKeyRef:
          name: kutt-jwt-secret
          key: jwtSecret
    - name: DEFAULT_DOMAIN
      value: mspc.sh
    - name: DB_FILENAME
      value: /var/lib/kutt/data.sqlite
    - name: REDIS_ENABLED
      value: &quot;true&quot;
    - name: REDIS_PORT
      value: &quot;6379&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s much better than exposing the value in an unencrypted YAML file.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;In this article, I explained all the steps that I went through to run my own URL-shortener using Podman. I laid out how to deploy the necessary applications using Compose files, how to derive pod manifests from the running containers, and finally, how to integrate them with &lt;code class=&quot;highlighter-rouge&quot;&gt;systemd&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you’d like to use Podman for self-hosting your own container-based applications, I hope this will be useful!&lt;/p&gt;</content><author><name>Jochen Lillich</name></author><category term="learning" /><category term="Deployment" /><category term="Linux" /><category term="no" /><category term="Podman" /><category term="random" /><summary type="html">A practical look at self-hosting web applications with Podman and Quadlet instead of reaching straight for Kubernetes.</summary></entry></feed>