<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Konrad Kądzielawa</title>
    <description>The latest articles on DEV Community by Konrad Kądzielawa (@kkadzielawa).</description>
    <link>https://dev.to/kkadzielawa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1223741%2F3be6d8be-1643-4359-b458-4795630af2c3.jpg</url>
      <title>DEV Community: Konrad Kądzielawa</title>
      <link>https://dev.to/kkadzielawa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kkadzielawa"/>
    <language>en</language>
    <item>
      <title>Extending NetBox with QR Labels: Small Automation That Connects Digital and Physical Infrastructure</title>
      <dc:creator>Konrad Kądzielawa</dc:creator>
      <pubDate>Tue, 07 Jul 2026 10:39:02 +0000</pubDate>
      <link>https://dev.to/kkadzielawa/extending-netbox-with-qr-labels-small-automation-that-connects-digital-and-physical-infrastructure-1n64</link>
      <guid>https://dev.to/kkadzielawa/extending-netbox-with-qr-labels-small-automation-that-connects-digital-and-physical-infrastructure-1n64</guid>
      <description>&lt;p&gt;NetBox is most useful when people actually use it during real work.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but there is a gap between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the source of truth exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;an engineer standing near a rack can quickly open the right object
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;QR labels are a small bridge between those two worlds.&lt;/p&gt;

&lt;p&gt;This article is about a small NetBox-related automation project: generating QR labels for physical infrastructure and making them easy to refresh, browse and use.&lt;/p&gt;

&lt;p&gt;It is not about a big plugin.&lt;/p&gt;

&lt;p&gt;It is about a practical extension that solved an operational problem.&lt;/p&gt;

&lt;p&gt;All names and paths are anonymized. Example paths use &lt;code&gt;/opt/netbox-ops&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;The goal was simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scan a label on a physical object and open the correct NetBox object.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That object could be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;device
rack
PDU
patch panel
power object
other inventory object
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The label should be small, readable and practical.&lt;/p&gt;

&lt;p&gt;The automation should be easy to regenerate when NetBox data changes.&lt;/p&gt;

&lt;p&gt;The generated files should not require manual editing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why QR labels mattered
&lt;/h2&gt;

&lt;p&gt;Without QR labels, the workflow often looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stand near equipment
read hostname or label
open NetBox
search manually
hope the name matches
open object
verify if it is the right one
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With QR labels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scan code
open exact NetBox object
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That saves time.&lt;/p&gt;

&lt;p&gt;More importantly, it reduces ambiguity.&lt;/p&gt;

&lt;p&gt;If physical labels and NetBox objects are connected, engineers are more likely to update the right object and trust the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1: The generator must be repeatable
&lt;/h2&gt;

&lt;p&gt;The QR labels should not be created manually.&lt;/p&gt;

&lt;p&gt;Manual generation creates stale files quickly.&lt;/p&gt;

&lt;p&gt;The better pattern was a generator script.&lt;/p&gt;

&lt;p&gt;A generic structure looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/netbox-ops
├── labels
│   ├── output
│   ├── templates
│   └── reports
└── scripts
    └── generate-labels.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact layout is not important.&lt;/p&gt;

&lt;p&gt;The important part is repeatability.&lt;/p&gt;

&lt;p&gt;The generator should be safe to run many times.&lt;/p&gt;

&lt;p&gt;It should produce predictable output.&lt;/p&gt;

&lt;p&gt;It should not require manual cleanup after each run.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 2: Stale labels are worse than missing labels
&lt;/h2&gt;

&lt;p&gt;A QR label that points to the wrong object is dangerous.&lt;/p&gt;

&lt;p&gt;So the generator had to think about stale output.&lt;/p&gt;

&lt;p&gt;Questions that mattered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What happens if an object was deleted?
What happens if an object was renamed?
What happens if the URL format changes?
What happens if a label was generated last month?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generator needed reports, not only images.&lt;/p&gt;

&lt;p&gt;Useful reports include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;generated labels
skipped objects
missing required fields
objects without labels
labels that may be stale
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A good output is not only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;333 files generated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A better output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;generated: 333
skipped: 12
missing URL: 0
needs review: 8
removed stale files: 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes the automation operationally visible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 3: Serving static output is not always obvious
&lt;/h2&gt;

&lt;p&gt;Generating files is one thing.&lt;/p&gt;

&lt;p&gt;Making them available in a browser is another.&lt;/p&gt;

&lt;p&gt;One issue was that a static index can be handled incorrectly if the web server or path is not configured as expected.&lt;/p&gt;

&lt;p&gt;For example, instead of opening in the browser, the index may download as a file.&lt;/p&gt;

&lt;p&gt;That kind of problem is small but annoying.&lt;/p&gt;

&lt;p&gt;The troubleshooting path was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;check content type
check web server mapping
check file permissions
check directory ownership
check whether the path is served as static content
check whether the browser cached the old behavior
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a generated label portal, the goal is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;open index page
browse labels
download or print if needed
scan QR code
open NetBox object
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the index downloads instead of rendering, the user experience is broken even if generation itself works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 4: Permissions should be boring
&lt;/h2&gt;

&lt;p&gt;Automation that writes files must have clear permissions.&lt;/p&gt;

&lt;p&gt;The generator should not randomly require root for every run.&lt;/p&gt;

&lt;p&gt;The web process should be able to read generated output.&lt;/p&gt;

&lt;p&gt;The operator should be able to regenerate safely.&lt;/p&gt;

&lt;p&gt;A simplified model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;generator user can write output
web process can read output
NetBox token is stored outside the script
logs and reports are readable
secrets are not committed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sounds boring.&lt;/p&gt;

&lt;p&gt;That is the point.&lt;/p&gt;

&lt;p&gt;Operational automation should be boring.&lt;/p&gt;

&lt;p&gt;If permissions are confusing, people stop trusting the automation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 5: Token handling must be simple and safe
&lt;/h2&gt;

&lt;p&gt;The generator needs access to NetBox data.&lt;/p&gt;

&lt;p&gt;That usually means using an API token.&lt;/p&gt;

&lt;p&gt;The mistake would be hardcoding it inside the script.&lt;/p&gt;

&lt;p&gt;A safer pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read token from environment file
restrict file permissions
use least-privilege token if possible
do not print token in logs
do not put token in generated HTML
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="nb"&gt;.&lt;/span&gt; /opt/netbox-ops/.env
&lt;span class="nb"&gt;set&lt;/span&gt; +a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file can contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NETBOX_URL=https://netbox.example.internal
NETBOX_TOKEN=...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The article uses a fake domain and fake path.&lt;/p&gt;

&lt;p&gt;The point is the pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 6: "Regenerate everything" needs guardrails
&lt;/h2&gt;

&lt;p&gt;At first, it is tempting to have one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;regenerate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is fine, but it should be clear what it does.&lt;/p&gt;

&lt;p&gt;A better command set is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status
dry-run
regenerate
clean-stale
install-links
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact names do not matter.&lt;/p&gt;

&lt;p&gt;The useful behavior is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status       -&amp;gt; show current output and last run
dry-run      -&amp;gt; show what would change
regenerate   -&amp;gt; rebuild labels
clean-stale  -&amp;gt; remove labels that no longer match objects
install-links -&amp;gt; expose the generated index where users can access it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This made the automation safer.&lt;/p&gt;

&lt;p&gt;Especially for generated files, I prefer seeing what will change before replacing everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 7: Labels must be designed for real use, not screenshots
&lt;/h2&gt;

&lt;p&gt;A label that looks nice on screen may be bad in the field.&lt;/p&gt;

&lt;p&gt;Real labels must be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;small
readable
scannable
not overloaded
consistent
easy to print
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I avoided putting too much information on the label.&lt;/p&gt;

&lt;p&gt;A practical label needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;short object name
object type or role
QR code
optional location or asset hint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Too much text makes the label worse.&lt;/p&gt;

&lt;p&gt;The QR code should be the primary link.&lt;/p&gt;

&lt;p&gt;The printed text is only a human backup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 8: Physical labels should not bypass NetBox quality
&lt;/h2&gt;

&lt;p&gt;QR labels make bad data visible.&lt;/p&gt;

&lt;p&gt;If an object name is wrong in NetBox, it appears on the label.&lt;/p&gt;

&lt;p&gt;If the object points to the wrong rack or role, the scan leads to bad context.&lt;/p&gt;

&lt;p&gt;So label generation became another validation step.&lt;/p&gt;

&lt;p&gt;The generator could expose issues like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;object has no role
object has unknown status
object has no location
object name is too long for label
object URL could not be generated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means QR labels are not only output.&lt;/p&gt;

&lt;p&gt;They are also a data quality check.&lt;/p&gt;




&lt;h2&gt;
  
  
  Automation workflow
&lt;/h2&gt;

&lt;p&gt;The practical workflow looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Read NetBox objects through API
2. Filter objects that should receive labels
3. Build stable object URLs
4. Generate QR images
5. Render printable labels
6. Generate index page
7. Write reports
8. Optionally remove stale output
9. Serve the index internally
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part was not the technology.&lt;/p&gt;

&lt;p&gt;The important part was that the process could be repeated safely.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The biggest lesson was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A small automation can make NetBox much more useful in the real world.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;QR labels are not a complex concept.&lt;/p&gt;

&lt;p&gt;But to make them operationally useful, the automation needs to handle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repeatability
stale files
permissions
token handling
reports
browser access
label readability
data quality warnings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most useful rule was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generated infrastructure artifacts need the same care as imported infrastructure data.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A QR label is not just an image.&lt;/p&gt;

&lt;p&gt;It is a physical pointer to the source of truth.&lt;/p&gt;

&lt;p&gt;If it is wrong, people lose trust.&lt;/p&gt;

&lt;p&gt;If it is right, NetBox becomes easier to use during real work.&lt;/p&gt;




&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;This QR label workflow is only one example of extending NetBox outside the UI.&lt;/p&gt;

&lt;p&gt;The next steps could include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scheduled regeneration
change detection
automatic reports
integration with internal documentation
label status dashboard
custom actions from NetBox
more validation rules before printing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NetBox becomes most valuable when it stops being only a database and starts becoming part of daily operations.&lt;/p&gt;

&lt;p&gt;QR labels are a small but very practical step in that direction.&lt;/p&gt;

</description>
      <category>netbox</category>
      <category>automation</category>
      <category>qrcode</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>NetBox in Docker: The Migration Problems That Mattered More Than the Install</title>
      <dc:creator>Konrad Kądzielawa</dc:creator>
      <pubDate>Tue, 07 Jul 2026 10:34:52 +0000</pubDate>
      <link>https://dev.to/kkadzielawa/netbox-in-docker-the-migration-problems-that-mattered-more-than-the-install-5hkf</link>
      <guid>https://dev.to/kkadzielawa/netbox-in-docker-the-migration-problems-that-mattered-more-than-the-install-5hkf</guid>
      <description>&lt;p&gt;Installing NetBox is not the hard part.&lt;/p&gt;

&lt;p&gt;The hard part starts when NetBox is no longer an empty application.&lt;/p&gt;

&lt;p&gt;When it already contains valuable infrastructure data, every change becomes more sensitive. A broken test instance is annoying. A broken source of truth is a real operational problem.&lt;/p&gt;

&lt;p&gt;This article is not a clean "how to install NetBox in Docker" guide.&lt;/p&gt;

&lt;p&gt;It is a short case study about the problems that mattered during a real migration-like process: building a safer Docker-based NetBox environment, restoring data, separating application health from automation errors, and avoiding destructive troubleshooting habits.&lt;/p&gt;

&lt;p&gt;All names and paths in this article are anonymized. The examples use generic names such as &lt;code&gt;/opt/netbox-ops&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;The goal was not simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run NetBox in Docker.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real goal was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a safer, reproducible NetBox environment that can be tested,
restored, automated and later used as the base for more advanced workflows.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That difference matters.&lt;/p&gt;

&lt;p&gt;A Dockerized NetBox setup is useful not only because it is easier to start. It is useful because it creates a boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;configuration
data
media
scripts
logs
runtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each part becomes easier to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I did not start directly on production
&lt;/h2&gt;

&lt;p&gt;The most important decision was to avoid learning directly on the main environment.&lt;/p&gt;

&lt;p&gt;Instead of changing the existing NetBox instance, I created a separate proof-of-concept environment.&lt;/p&gt;

&lt;p&gt;The pattern was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;production-like data
separate Docker directory
separate Compose stack
controlled restore
manual verification
only then further automation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified layout looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/netbox-ops
├── compose
├── backups
├── imports
├── media
└── scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact path is not important.&lt;/p&gt;

&lt;p&gt;The important part is that the environment had a clear operational boundary.&lt;/p&gt;

&lt;p&gt;This made it possible to test migration steps, imports and scripts without treating the main NetBox instance as a playground.&lt;/p&gt;

&lt;p&gt;The lesson was simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If the application already contains valuable infrastructure data,
do not test migration logic directly on the only working copy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Problem 1: A running container is not the same as a healthy application
&lt;/h2&gt;

&lt;p&gt;One of the first practical checks was container state.&lt;/p&gt;

&lt;p&gt;Commands like these are obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
docker ps &lt;span class="nt"&gt;-a&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the important part was not running the commands.&lt;/p&gt;

&lt;p&gt;The important part was not trusting only one signal.&lt;/p&gt;

&lt;p&gt;A container can be running while the application is not usable.&lt;/p&gt;

&lt;p&gt;A container can also restart quickly enough that the first look does not show the real failure.&lt;/p&gt;

&lt;p&gt;So I used a few separate checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
docker compose logs netbox &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
docker compose logs postgres &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
docker compose logs redis &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://127.0.0.1:8000/login/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That helped separate several questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is Docker running?
Is the NetBox container running?
Is NetBox actually responding?
Is the database reachable?
Is Redis healthy?
Is the problem inside the app or outside it?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sounds basic, but it prevents a lot of bad troubleshooting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 2: Exited 137 looked like a NetBox issue, but it was not necessarily one
&lt;/h2&gt;

&lt;p&gt;At one point, a container exited with code &lt;code&gt;137&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is one of those errors that can be misleading if you read it only as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NetBox failed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A better first interpretation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The process was killed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Often this points toward memory pressure, OOM killer activity, or a host-level resource problem.&lt;/p&gt;

&lt;p&gt;The diagnostic path was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt;
docker logs &amp;lt;container_name&amp;gt; &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;200
dmesg &lt;span class="nt"&gt;-T&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"killed|oom|out of memory"&lt;/span&gt;
free &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;
docker system &lt;span class="nb"&gt;df&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lesson:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exited 137 is not automatically an application bug.
Check the host before rewriting the application configuration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially important with NetBox because migrations, imports or plugin-related tasks may temporarily use more resources than a quiet application instance.&lt;/p&gt;

&lt;p&gt;If the host is under pressure, restarting containers without checking the reason only hides the real problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 3: Script errors can look scarier than they are
&lt;/h2&gt;

&lt;p&gt;Another issue appeared during custom automation.&lt;/p&gt;

&lt;p&gt;A script returned a database-related error similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;duplicate key value violates unique constraint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, that looks serious.&lt;/p&gt;

&lt;p&gt;But the important question was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Did NetBox fail, or did only the automation script fail?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are different problems.&lt;/p&gt;

&lt;p&gt;The application health check was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
docker compose logs netbox &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://127.0.0.1:8000/login/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If NetBox still responds and the containers are healthy, the problem may be limited to the custom script.&lt;/p&gt;

&lt;p&gt;In this case, the real issue was idempotency.&lt;/p&gt;

&lt;p&gt;The script tried to create objects that already existed.&lt;/p&gt;

&lt;p&gt;The fix was not "repair NetBox".&lt;/p&gt;

&lt;p&gt;The fix was to make automation safer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;check if object exists first
create only when missing
update when needed
skip when unchanged
write a skipped/changed summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This changed the way I treated all import and cleanup scripts.&lt;/p&gt;

&lt;p&gt;A good NetBox script should not assume an empty database.&lt;/p&gt;

&lt;p&gt;It should expect that some objects already exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 4: Do not confuse cleanup with destruction
&lt;/h2&gt;

&lt;p&gt;During Docker troubleshooting, there is always a temptation to run aggressive cleanup commands.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker system prune &lt;span class="nt"&gt;-a&lt;/span&gt;
docker volume prune
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These commands have their place.&lt;/p&gt;

&lt;p&gt;But they should not be a reflex.&lt;/p&gt;

&lt;p&gt;For NetBox, deleting volumes can mean deleting the database or uploaded media if the environment is not designed carefully.&lt;/p&gt;

&lt;p&gt;That is why I prefer this rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before deleting anything, identify where the database, media and backups live.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The safer pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
docker volume &lt;span class="nb"&gt;ls
&lt;/span&gt;docker volume inspect &amp;lt;volume_name&amp;gt;
docker inspect &amp;lt;container_name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then decide.&lt;/p&gt;

&lt;p&gt;For a source-of-truth system, "clean everything and try again" is often the wrong mindset.&lt;/p&gt;

&lt;p&gt;The better mindset is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;observe
identify
back up
change one thing
verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Problem 5: Media restore is easy to forget
&lt;/h2&gt;

&lt;p&gt;When moving a NetBox environment, the database is the obvious part.&lt;/p&gt;

&lt;p&gt;Media files are easier to miss.&lt;/p&gt;

&lt;p&gt;The database may restore correctly, but attachments, uploaded files or generated assets may not appear if media is not restored or mounted correctly.&lt;/p&gt;

&lt;p&gt;That creates a strange situation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NetBox works,
objects exist,
but files or images are missing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is not complicated, but the lesson is important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A NetBox restore is not complete until database and media are both verified.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My basic checklist became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;database restored
superuser/admin access verified
objects visible
media path mounted
attachments/images visible
logs clean enough
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Problem 6: Restart policies must be verified, not assumed
&lt;/h2&gt;

&lt;p&gt;The environment also needed to survive a VM reboot.&lt;/p&gt;

&lt;p&gt;This part is small, but operationally important.&lt;/p&gt;

&lt;p&gt;Docker services should start with the host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;docker.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;containerd.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Containers should have an intentional restart policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But I do not like assuming that Compose settings are actually applied.&lt;/p&gt;

&lt;p&gt;I prefer checking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'{{.HostConfig.RestartPolicy.Name}}'&lt;/span&gt; &amp;lt;container_name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unless-stopped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then test the real scenario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the host comes back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://127.0.0.1:8000/login/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is simple, but it removes guessing.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I avoided documenting on purpose
&lt;/h2&gt;

&lt;p&gt;I did not want this process to turn into another generic NetBox installation tutorial.&lt;/p&gt;

&lt;p&gt;So the focus was not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;install package X
copy file Y
run command Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The more valuable part was the operational behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What happens when a container is killed?
What happens when a script partially fails?
What happens when old data already exists?
What happens when media is missing?
What should not be deleted during troubleshooting?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the problems that appear after the installation guide ends.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The biggest lesson was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker does not automatically make NetBox safe.
But it gives you tools to build a safer operating model.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful parts were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a separate POC environment
clear project directories
persistent data and media
controlled restore
container-level health checks
application-level health checks
idempotent scripts
careful cleanup rules
verified restart policies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important troubleshooting rule was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Always separate application failure from automation failure.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A failed import script does not always mean NetBox is broken.&lt;/p&gt;

&lt;p&gt;A stopped container does not always mean NetBox configuration is wrong.&lt;/p&gt;

&lt;p&gt;A restored database does not always mean the restore is complete.&lt;/p&gt;

&lt;p&gt;Those distinctions matter.&lt;/p&gt;




&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;The Docker environment was only the foundation.&lt;/p&gt;

&lt;p&gt;The harder problem came after that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How to import real infrastructure data into NetBox without creating chaos?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means dealing with DNS exports, IP addresses, hostnames, devices, interfaces, duplicates, naming problems and incomplete information.&lt;/p&gt;

&lt;p&gt;That is the topic of the next part.&lt;/p&gt;

</description>
      <category>netbox</category>
      <category>docker</category>
      <category>devops</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>Building a Local IT Assistant with Ollama, Open WebUI and a Custom Model Router</title>
      <dc:creator>Konrad Kądzielawa</dc:creator>
      <pubDate>Mon, 29 Jun 2026 07:42:42 +0000</pubDate>
      <link>https://dev.to/kkadzielawa/building-a-local-it-assistant-with-ollama-open-webui-and-a-custom-model-router-42dp</link>
      <guid>https://dev.to/kkadzielawa/building-a-local-it-assistant-with-ollama-open-webui-and-a-custom-model-router-42dp</guid>
      <description>&lt;p&gt;Running a local LLM is easy. Turning it into something useful for daily IT administration is a different story.&lt;/p&gt;

&lt;p&gt;This article is not only about installing Ollama and Open WebUI in Docker. There are already many tutorials for that. What I wanted to build was a local assistant that could actually help with SysOps tasks: reading logs, explaining errors, preparing safe Bash commands, writing Ansible playbooks, reviewing YAML files, and helping with Docker-related troubleshooting.&lt;/p&gt;

&lt;p&gt;My goal was to prepare a local AI setup that could later be moved to a dedicated Actina machine and used as a small internal IT assistant.&lt;/p&gt;

&lt;p&gt;The interesting part was not the installation itself.&lt;/p&gt;

&lt;p&gt;The interesting part was what happened after the installation.&lt;/p&gt;

&lt;p&gt;I had to solve a few practical problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how to choose a default model,&lt;/li&gt;
&lt;li&gt;how to route coding-related prompts to a better model,&lt;/li&gt;
&lt;li&gt;how to avoid manually switching models in the UI,&lt;/li&gt;
&lt;li&gt;how to make Open WebUI start with the right assistant,&lt;/li&gt;
&lt;li&gt;how to make the whole setup survive a VM restart,&lt;/li&gt;
&lt;li&gt;and how to keep the models and data persistent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is Part 1 of the setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final goal
&lt;/h2&gt;

&lt;p&gt;The final idea was simple:&lt;/p&gt;

&lt;p&gt;I wanted to open Open WebUI, select one assistant, and use it for IT administration.&lt;/p&gt;

&lt;p&gt;Under the hood, however, I wanted different models to be used depending on the task.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;General chat, logs, CSV analysis, explanations  -&amp;gt; qwen3:8b
Bash, Python, Docker, Ansible, YAML, errors     -&amp;gt; qwen2.5-coder:7b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So instead of manually switching between models, I created a custom Pipe Function in Open WebUI.&lt;/p&gt;

&lt;p&gt;From the user perspective, it looked like one model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AUTO Local IT Assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But internally it worked as a simple model router.&lt;/p&gt;




&lt;h2&gt;
  
  
  Environment overview
&lt;/h2&gt;

&lt;p&gt;The local setup was based on Docker Compose.&lt;/p&gt;

&lt;p&gt;The project directory looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/home/kkadzielawa/ai-local
├── docker-compose.yml
├── .env
└── data
    ├── ollama
    └── open-webui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main containers were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ollama
open-webui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local models were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qwen3:8b
qwen2.5-coder:7b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qwen3:8b           -&amp;gt; general local IT assistant
qwen2.5-coder:7b  -&amp;gt; coding, scripting, Docker, Ansible, YAML
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first, I thought that setting a default model would be enough.&lt;/p&gt;

&lt;p&gt;It was not.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1: A default model is not enough
&lt;/h2&gt;

&lt;p&gt;The first approach was to configure Open WebUI with a default model.&lt;/p&gt;

&lt;p&gt;In Docker Compose, this can be done using environment variables like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DEFAULT_MODELS=qwen3:8b&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DEFAULT_PINNED_MODELS=qwen3:8b,qwen2.5-coder:7b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works, but only solves one problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Which model should be selected by default?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not solve the more important problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Which model is best for this specific prompt?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, this prompt is probably fine for a general model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Explain what should be checked before upgrading a Debian server in production.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this one should go to a coding-oriented model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write an Ansible playbook that checks hostname, uptime, OS version and free disk space.
Read-only tasks only. Do not modify the system.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did not want to remember which model to select every time.&lt;/p&gt;

&lt;p&gt;I wanted Open WebUI to make that decision automatically.&lt;/p&gt;

&lt;p&gt;So I needed a custom layer between the user prompt and the actual model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 2: Tools, Pipelines or Functions?
&lt;/h2&gt;

&lt;p&gt;This was one of the first confusing parts.&lt;/p&gt;

&lt;p&gt;Open WebUI has several places that may look similar at first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Workspace -&amp;gt; Tools
Settings  -&amp;gt; Pipelines
Admin     -&amp;gt; Functions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first assumption was that this kind of logic should be created as a tool or pipeline.&lt;/p&gt;

&lt;p&gt;But that was not the right direction.&lt;/p&gt;

&lt;p&gt;A model router is not a normal tool called by the model during a conversation. It is not something like a calculator, web search or custom API tool.&lt;/p&gt;

&lt;p&gt;What I needed was something that behaves like a model in the UI, receives the user request, decides what to do with it, and then forwards it to the selected real model.&lt;/p&gt;

&lt;p&gt;The right place was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Admin Settings
-&amp;gt; Functions
-&amp;gt; New Function
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the right type of function was a Pipe Function.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 3: Open WebUI showed an Example Filter, but I needed a Pipe
&lt;/h2&gt;

&lt;p&gt;When creating a new function, Open WebUI may show an example based on a filter.&lt;/p&gt;

&lt;p&gt;That can be misleading.&lt;/p&gt;

&lt;p&gt;For this use case, a filter was not what I needed.&lt;/p&gt;

&lt;p&gt;A filter can modify input or output, but I wanted to create something that appears as a selectable model and routes the request to another model.&lt;/p&gt;

&lt;p&gt;So the function had to be based on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Pipe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was an important detail.&lt;/p&gt;

&lt;p&gt;The custom function was named:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AUTO Local IT Assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function had its own configurable valves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Default Model: qwen3:8b
Coder Model: qwen2.5-coder:7b
Show Selected Model: false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allowed me to change the underlying models later without rewriting the whole function.&lt;/p&gt;




&lt;h2&gt;
  
  
  The routing logic
&lt;/h2&gt;

&lt;p&gt;The first version of the router was intentionally simple.&lt;/p&gt;

&lt;p&gt;It checked the user message and looked for keywords or patterns suggesting that the prompt was related to coding, scripting or infrastructure automation.&lt;/p&gt;

&lt;p&gt;Examples of keywords:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ansible
playbook
yaml
bash
python
powershell
docker compose
terraform
traceback
exception
stack trace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples of patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker ...
systemctl
journalctl
apt install
def function_name(...)
class Something
#!/bin/bash
- hosts:
tasks:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the prompt matched coding or automation-related patterns, the function routed it to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qwen2.5-coder:7b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise, it used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qwen3:8b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not an advanced agent.&lt;/p&gt;

&lt;p&gt;It is a simple and predictable router.&lt;/p&gt;

&lt;p&gt;And that was exactly what I wanted at this stage.&lt;/p&gt;

&lt;p&gt;For a local IT assistant, predictable behavior is often better than clever behavior that is hard to debug.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adding a safety-oriented system prompt
&lt;/h2&gt;

&lt;p&gt;Because this assistant is meant for IT administration, I did not want it to randomly suggest destructive commands.&lt;/p&gt;

&lt;p&gt;So I added a system prompt with practical safety rules.&lt;/p&gt;

&lt;p&gt;The assistant should:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- answer in Polish,
- classify commands as read-only, system-changing or risky,
- suggest diagnostics before making production changes,
- avoid destructive commands without backups,
- explain Bash, Python and Ansible snippets,
- prefer safe troubleshooting steps first.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a very small addition, but it changes the behavior a lot.&lt;/p&gt;

&lt;p&gt;For example, instead of immediately suggesting a dangerous command, the assistant should first propose read-only diagnostics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;
free &lt;span class="nt"&gt;-m&lt;/span&gt;
&lt;span class="nb"&gt;uptime
&lt;/span&gt;systemctl status docker
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; docker &lt;span class="nt"&gt;--no-pager&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For me, this is one of the most important parts of building a local SysOps assistant.&lt;/p&gt;

&lt;p&gt;The model should not only answer.&lt;/p&gt;

&lt;p&gt;It should answer in a way that matches how administrators actually work with production systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 4: The router existed, but Open WebUI still showed qwen3
&lt;/h2&gt;

&lt;p&gt;After creating the Pipe Function, I wanted Open WebUI to start with the router selected by default.&lt;/p&gt;

&lt;p&gt;So I changed the Docker Compose environment variables to something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DEFAULT_MODELS=auto-local-it-assistant&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DEFAULT_PINNED_MODELS=auto-local-it-assistant,qwen3:8b,qwen2.5-coder:7b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first, it looked like it did not work.&lt;/p&gt;

&lt;p&gt;Open WebUI still showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qwen3:8b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was confusing, because the configuration seemed correct.&lt;/p&gt;

&lt;p&gt;The reason was simple but easy to miss:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open WebUI can remember the model selected in an existing chat or user session.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the fix was not another Docker Compose change.&lt;/p&gt;

&lt;p&gt;The fix was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Hard refresh the browser with CTRL + F5
2. Start a new chat
3. Search for "auto" in the model selector
4. Select AUTO Local IT Assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the router worked as expected.&lt;/p&gt;

&lt;p&gt;The lesson here was important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DEFAULT_MODELS affects the default state,
but existing chats or user sessions may still remember the old model.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a very practical troubleshooting point.&lt;/p&gt;

&lt;p&gt;Without knowing this, it is easy to waste time restarting containers, changing YAML files or assuming the Pipe Function is broken.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 5: Containers must start after VM reboot
&lt;/h2&gt;

&lt;p&gt;Another issue was more operational.&lt;/p&gt;

&lt;p&gt;This setup was running on a VM, and I did not want the local assistant to disappear after a reboot.&lt;/p&gt;

&lt;p&gt;The solution had two parts.&lt;/p&gt;

&lt;p&gt;First, Docker and containerd should start with the system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;docker.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;containerd.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, the containers should have a proper restart policy.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama/ollama:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/ollama:/root/.ollama&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;open-webui&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/open-webui/open-webui:main&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;open-webui&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/open-webui:/app/backend/data&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;restart: unless-stopped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;restart: always
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because I prefer this behavior for a local admin tool.&lt;/p&gt;

&lt;p&gt;If I manually stop the container, I usually mean it. I do not want Docker to bring it back immediately against my decision.&lt;/p&gt;

&lt;p&gt;To verify the restart policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'{{.HostConfig.RestartPolicy.Name}}'&lt;/span&gt; ollama
docker inspect &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'{{.HostConfig.RestartPolicy.Name}}'&lt;/span&gt; open-webui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unless-stopped
unless-stopped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the real test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the VM comes back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both containers should be running again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 6: Models and data must survive container recreation
&lt;/h2&gt;

&lt;p&gt;Another thing that matters a lot with Ollama and Open WebUI is persistence.&lt;/p&gt;

&lt;p&gt;I did not want downloaded models or Open WebUI data to live only inside containers.&lt;/p&gt;

&lt;p&gt;That is why I used local directories mounted into the containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/ollama:/root/.ollama&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/open-webui:/app/backend/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives me a clean project structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/home/kkadzielawa/ai-local
├── docker-compose.yml
├── .env
└── data
    ├── ollama
    └── open-webui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also makes the setup easier to back up or move to another machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-czf&lt;/span&gt; ai-local-backup-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;.tar.gz ai-local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was important because the environment was prepared with a future migration in mind.&lt;/p&gt;

&lt;p&gt;The VM was only the first step.&lt;/p&gt;

&lt;p&gt;The target was a dedicated machine for local AI-assisted administration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing the assistant
&lt;/h2&gt;

&lt;p&gt;I tested the router with a few different prompts.&lt;/p&gt;

&lt;p&gt;A general SysOps question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Explain what should be checked before upgrading a Debian server in production.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qwen3:8b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Bash-related prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write a Bash script that checks CPU, RAM, disks, IP addresses and whether Docker is running.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qwen2.5-coder:7b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An Ansible prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write an Ansible playbook that checks hostname, uptime, OS version and free disk space.
Read-only tasks only.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qwen2.5-coder:7b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A YAML troubleshooting prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check this docker-compose.yml and explain why Open WebUI cannot reach Ollama.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qwen2.5-coder:7b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A log analysis prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Analyze this log and tell me what looks suspicious.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qwen3:8b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this stage, the routing does not need to be perfect.&lt;/p&gt;

&lt;p&gt;It needs to be predictable and useful.&lt;/p&gt;

&lt;p&gt;That is enough for Part 1.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from this setup is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Local AI is not only about running a model.
It is about building the workflow around the model.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ollama and Open WebUI give you a very good starting point.&lt;/p&gt;

&lt;p&gt;But the real value starts when you customize the environment for your own use case.&lt;/p&gt;

&lt;p&gt;In my case, the most important improvements were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- one visible assistant in the UI,
- automatic routing between a general model and a coding model,
- a safety-oriented system prompt,
- persistent model and Open WebUI data,
- container autostart after VM reboot,
- a structure that can be moved to another machine later.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most useful parts were not the obvious installation steps.&lt;/p&gt;

&lt;p&gt;The most useful parts were the small issues found along the way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- a model router is not a normal Tool,
- the correct Open WebUI feature is a Function,
- the function should be a Pipe, not a Filter,
- Open WebUI may remember the previous model in an existing chat,
- DEFAULT_MODELS may look broken until a new chat is created,
- restart policies should be verified, not assumed,
- persistent volumes are mandatory if the setup is meant to survive recreation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the details that usually do not appear in a clean installation guide.&lt;/p&gt;

&lt;p&gt;But these are exactly the details that matter when building something usable.&lt;/p&gt;




&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;This is only the first part of the project.&lt;/p&gt;

&lt;p&gt;The current setup gives me a local IT assistant with simple model routing.&lt;/p&gt;

&lt;p&gt;But there are many possible next steps.&lt;/p&gt;

&lt;p&gt;The most obvious one is adding a vision model.&lt;/p&gt;

&lt;p&gt;That would allow the assistant to handle screenshots, UI errors, diagrams or monitoring views.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;screenshot or image -&amp;gt; vision model
code or automation  -&amp;gt; coder model
general IT question -&amp;gt; general model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another important step is RAG.&lt;/p&gt;

&lt;p&gt;I want the assistant to work with local documentation, internal notes, procedures, PDFs, CSV exports and troubleshooting guides.&lt;/p&gt;

&lt;p&gt;That would require adding an embedding model and a local knowledge base.&lt;/p&gt;

&lt;p&gt;Possible future improvements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Add a vision model for screenshots and UI troubleshooting
2. Add a local knowledge base with RAG
3. Add an embedding model for document search
4. Improve the routing logic
5. Add monitoring for Ollama and Open WebUI
6. Add backups for models and Open WebUI data
7. Add reverse proxy and HTTPS
8. Restrict access to VPN or internal LAN only
9. Build more specialized assistants for specific IT tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So this is not a finished platform.&lt;/p&gt;

&lt;p&gt;It is the base layer.&lt;/p&gt;

&lt;p&gt;The first working version of a local SysOps assistant.&lt;/p&gt;

&lt;p&gt;And for me, this is the most interesting part of local AI: not just running a model, but slowly turning it into a practical internal tool.&lt;/p&gt;

</description>
      <category>pgaichallenge</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>ai</category>
    </item>
    <item>
      <title>Post-Mortem: Diagnosing and Fixing Ceph Quorum Breakdown (Time Drift) in a Hybrid Proxmox VE Cluster</title>
      <dc:creator>Konrad Kądzielawa</dc:creator>
      <pubDate>Wed, 25 Mar 2026 21:42:53 +0000</pubDate>
      <link>https://dev.to/kkadzielawa/post-mortem-diagnosing-and-fixing-ceph-quorum-breakdown-time-drift-in-a-hybrid-proxmox-ve-cluster-3d3d</link>
      <guid>https://dev.to/kkadzielawa/post-mortem-diagnosing-and-fixing-ceph-quorum-breakdown-time-drift-in-a-hybrid-proxmox-ve-cluster-3d3d</guid>
      <description>&lt;p&gt;&lt;strong&gt;Key Takeaways &amp;amp; Conclusions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Root Cause:&lt;/strong&gt; A Time Drift between the physical Proxmox host and nested/external virtual machines exceeded Ceph's strict &lt;strong&gt;0.05s&lt;/strong&gt; tolerance limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Impact:&lt;/strong&gt; The time mismatch caused a continuous authorization loop (&lt;code&gt;electing&lt;/code&gt;/&lt;code&gt;probing&lt;/code&gt;) in the local Ceph Monitor (MON), corrupting its local RocksDB database and triggering a cascading failure of the dependent &lt;strong&gt;OSD.0&lt;/strong&gt; daemon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolution:&lt;/strong&gt; Enforced rigorous time synchronization via &lt;strong&gt;Chrony&lt;/strong&gt; and &lt;strong&gt;QEMU Guest Agent&lt;/strong&gt;, followed by a destructive purge and rebuild of the corrupted MON daemon using the surviving quorum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification:&lt;/strong&gt; Chaos Engineering tests (&lt;code&gt;rados bench&lt;/code&gt;) confirmed that with a &lt;strong&gt;min_size = 2&lt;/strong&gt; replication policy, the cluster successfully survives a hard node power-off without dropping client I/O.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. The Architecture and The Incident
&lt;/h2&gt;

&lt;p&gt;Building a Ceph cluster typically requires dedicated bare-metal servers. My home lab topology, however, is a hybrid 3-node setup designed to simulate a full cluster using limited hardware:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;admin&lt;/strong&gt; (Physical Proxmox host, IP: &lt;code&gt;192.168.0.250&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;admin-02&lt;/strong&gt; (Nested VM 103 on the main host, IP: &lt;code&gt;192.168.0.251&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;admin-03&lt;/strong&gt; (External VM running on a separate Ubuntu desktop, IP: &lt;code&gt;192.168.0.252&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The incident began when the cluster state dropped to &lt;code&gt;HEALTH_WARN&lt;/code&gt;. Despite the underlying Corosync cluster and network layer (ICMP) functioning perfectly across the &lt;code&gt;192.168.0.0/24&lt;/code&gt; subnet, the main physical node (&lt;code&gt;admin&lt;/code&gt;) dropped out of the Ceph quorum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@admin:~# ceph &lt;span class="nt"&gt;-s&lt;/span&gt;
  cluster:
    &lt;span class="nb"&gt;id&lt;/span&gt;:     25f2d0a2-d64b-42a2-b93d-99a560571e0e
    health: HEALTH_WARN
            1/3 mons down, quorum admin-02,admin-03
            1 osds down
            1 host &lt;span class="o"&gt;(&lt;/span&gt;1 osds&lt;span class="o"&gt;)&lt;/span&gt; down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Debugging &amp;amp; Log Analysis
&lt;/h2&gt;

&lt;p&gt;To find the root cause, I inspected the &lt;code&gt;systemd&lt;/code&gt; journals for the failed Object Storage Daemon (&lt;code&gt;osd.0&lt;/code&gt;) located on the physical &lt;code&gt;admin&lt;/code&gt; host.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@admin:~# journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; ceph-osd@0.service &lt;span class="nt"&gt;-n&lt;/span&gt; 50 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
Mar 25 21:08:21 admin ceph-osd[3147]: failed to fetch mon config &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;--no-mon-config&lt;/span&gt; to skip&lt;span class="o"&gt;)&lt;/span&gt;
Mar 25 21:08:21 admin systemd[1]: ceph-osd@0.service: Main process exited, &lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;exited, &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1/FAILURE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OSD drive itself was physically healthy, but it crashed on startup because it could not retrieve the &lt;a href="https://www.google.com/search?q=https://docs.ceph.com/en/latest/architecture/%23cluster-map" rel="noopener noreferrer"&gt;Cluster Map&lt;/a&gt; from the local Monitor (&lt;code&gt;ceph-mon&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Next, I checked the local Monitor service on the &lt;code&gt;admin&lt;/code&gt; node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@admin:~# journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; ceph-mon@admin.service &lt;span class="nt"&gt;-n&lt;/span&gt; 30 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
Mar 25 21:49:55 admin ceph-mon[1377]: 2026-03-25T21:49:55.526+0100 7c00b3b756c0 &lt;span class="nt"&gt;-1&lt;/span&gt; mon.admin@0&lt;span class="o"&gt;(&lt;/span&gt;probing&lt;span class="o"&gt;)&lt;/span&gt; e3 get_health_metrics reporting 1 slow ops, oldest is auth&lt;span class="o"&gt;(&lt;/span&gt;proto 0 30 bytes epoch 0&lt;span class="o"&gt;)&lt;/span&gt;
Mar 25 21:50:50 admin ceph-mon[1377]: 2026-03-25T21:50:50.528+0100 7c00b3b756c0 &lt;span class="nt"&gt;-1&lt;/span&gt; mon.admin@0&lt;span class="o"&gt;(&lt;/span&gt;electing&lt;span class="o"&gt;)&lt;/span&gt; e3 get_health_metrics reporting 1 slow ops, oldest is auth&lt;span class="o"&gt;(&lt;/span&gt;proto 0 30 bytes epoch 0&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logs revealed the local MON was stuck in an endless &lt;code&gt;electing&lt;/code&gt;/&lt;code&gt;probing&lt;/code&gt; state, constantly dropping cryptographic authorization packets (&lt;code&gt;auth slow ops&lt;/code&gt;). According to &lt;a href="https://www.google.com/search?q=https://docs.ceph.com/en/latest/rados/configuration/mon-config-ref/%23clock" rel="noopener noreferrer"&gt;Ceph's official clock synchronization documentation&lt;/a&gt;, Monitors require clocks to be synchronized within &lt;strong&gt;0.05 seconds&lt;/strong&gt;. Because &lt;code&gt;admin-02&lt;/code&gt; and &lt;code&gt;admin-03&lt;/code&gt; were virtual machines, their CPU clock cycles drifted away from the physical host's hardware clock, effectively locking the &lt;code&gt;admin&lt;/code&gt; node out of the quorum and corrupting its metadata database.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Action Plan: Fixing Time Drift &amp;amp; Rebuilding
&lt;/h2&gt;

&lt;p&gt;To stabilize the cluster, I had to enforce strict timekeeping on the virtual nodes and reconstruct the corrupted Monitor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table: Remediation Steps per Node&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Node&lt;/th&gt;
&lt;th&gt;Ceph Role&lt;/th&gt;
&lt;th&gt;Root Cause&lt;/th&gt;
&lt;th&gt;Implemented Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;admin&lt;/strong&gt; (Physical)&lt;/td&gt;
&lt;td&gt;MON, OSD.0&lt;/td&gt;
&lt;td&gt;Corrupted RocksDB&lt;/td&gt;
&lt;td&gt;Destroyed and recreated local MON daemon.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;admin-02&lt;/strong&gt; (VM &lt;strong&gt;103&lt;/strong&gt;)&lt;/td&gt;
&lt;td&gt;MON, OSD.1&lt;/td&gt;
&lt;td&gt;Hypervisor Time Drift&lt;/td&gt;
&lt;td&gt;Installed &lt;strong&gt;QEMU Guest Agent&lt;/strong&gt; for hardware sync.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;admin-03&lt;/strong&gt; (Ext. VM)&lt;/td&gt;
&lt;td&gt;MON, OSD.2&lt;/td&gt;
&lt;td&gt;OS-level Time Drift&lt;/td&gt;
&lt;td&gt;Deployed &lt;strong&gt;Chrony&lt;/strong&gt; to force strict NTP sync.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 1: Enforcing Strict NTP on the External VM (&lt;code&gt;admin-03&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Standard &lt;code&gt;systemd-timesyncd&lt;/code&gt; is too sluggish for Ceph. I replaced it with &lt;code&gt;chrony&lt;/code&gt; and forced an immediate clock step on the external Ubuntu VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@admin-03:~# &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;chrony &lt;span class="nt"&gt;-y&lt;/span&gt;
root@admin-03:~# &lt;span class="nb"&gt;sudo &lt;/span&gt;chronyc makestep
root@admin-03:~# chronyc tracking
Reference ID    : C292FB72 &lt;span class="o"&gt;(&lt;/span&gt;host-194-146-251-114.virtuaoperator.pl&lt;span class="o"&gt;)&lt;/span&gt;
Stratum         : 2
System &lt;span class="nb"&gt;time&lt;/span&gt;     : 0.000000000 seconds slow of NTP &lt;span class="nb"&gt;time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output confirmed a &lt;strong&gt;0.000000000 seconds&lt;/strong&gt; deviation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Enabling QEMU Guest Agent (&lt;code&gt;admin-02&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;For the nested Proxmox node, time had to be injected directly from the physical hypervisor. I enabled the QEMU Guest Agent flag, performed a hard power cycle of VM &lt;strong&gt;103&lt;/strong&gt;, and verified the communication channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Executed on the physical host (admin)&lt;/span&gt;
root@admin:~# qm &lt;span class="nb"&gt;set &lt;/span&gt;103 &lt;span class="nt"&gt;-agent&lt;/span&gt; 1
root@admin:~# qm shutdown 103
root@admin:~# qm start 103
root@admin:~# qm agent 103 ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Reconstructing the Corrupted Monitor (&lt;code&gt;admin&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;With the time drift neutralized, I had to fix the broken &lt;code&gt;ceph-mon&lt;/code&gt; daemon on the physical host. Attempting to repair a broken RocksDB is often futile. Since the other two nodes maintained a healthy quorum and an intact Cluster Map, I simply purged the broken node and recreated it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@admin:~# pveceph mon destroy admin
root@admin:~# pveceph mon create
root@admin:~# systemctl restart ceph-osd@0.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Immediately after the restart, &lt;code&gt;osd.0&lt;/code&gt; successfully authenticated with the fresh Monitor, and the cluster began the peering process, eventually returning to &lt;strong&gt;HEALTH_OK&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Verification: Chaos Engineering
&lt;/h2&gt;

&lt;p&gt;You can't trust a storage cluster until you break it while it's under load. To prove the fix worked, I initiated a continuous benchmark write on the &lt;code&gt;ceph-storage&lt;/code&gt; pool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@admin:~# rados bench &lt;span class="nt"&gt;-p&lt;/span&gt; ceph-storage 120 write &lt;span class="nt"&gt;--no-cleanup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the benchmark was running at &lt;strong&gt;~55 MB/s&lt;/strong&gt;, I executed a brutal &lt;code&gt;sudo systemctl poweroff&lt;/code&gt; on the external &lt;strong&gt;admin-03&lt;/strong&gt; node.&lt;/p&gt;

&lt;p&gt;Here is what happened in the benchmark logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  38       16       419       403   42.4168         0            -     1.12533
  39       16       419       403   41.3292         0            -     1.12533
2026-03-25T22:31:25.378550+0100 min lat: 0.334379 max lat: 3.14551 avg lat: 1.12533
  40       16       419       403   40.2959         0            -     1.12533
  41       16       419       403   39.3131         0            -     1.12533
  42       16       439       423   40.2816         8     0.399663      1.5659
  43       16       470       454   42.2282       124     0.593991      1.5049
  44       16       489       473   42.9956        76     0.755278     1.47177
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Result:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Seconds 38-41:&lt;/strong&gt; The moment the node died, the cluster momentarily paused client I/O. Throughput dropped to &lt;strong&gt;0 MB/s&lt;/strong&gt;, and latency spiked.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Second 42+:&lt;/strong&gt; The CRUSH map recalculated the topology. Because the pool was configured with &lt;strong&gt;min_size = 2&lt;/strong&gt;, the two surviving nodes authorized the writes. The cluster automatically resumed operations, quickly catching up and hitting speeds of &lt;strong&gt;124 MB/s&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once the test concluded, I cleaned up the benchmark data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@admin:~# rados &lt;span class="nt"&gt;-p&lt;/span&gt; ceph-storage cleanup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conclusion:&lt;/strong&gt; Time synchronization is the absolute lifeblood of Ceph. Never trust default virtual machine clocks in a production or simulated lab environment. By implementing &lt;code&gt;chrony&lt;/code&gt; and QEMU hardware sync, the cluster is now fully immune to node-level failures.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ceph</category>
      <category>proxmox</category>
    </item>
    <item>
      <title>Proxmox &amp; Ceph in a Home Lab: Building a Cluster from a Single Drive &amp; Surviving the CLI Battle</title>
      <dc:creator>Konrad Kądzielawa</dc:creator>
      <pubDate>Mon, 23 Mar 2026 19:58:21 +0000</pubDate>
      <link>https://dev.to/kkadzielawa/proxmox-ceph-in-a-home-lab-building-a-cluster-from-a-single-drive-surviving-the-cli-battle-4f6a</link>
      <guid>https://dev.to/kkadzielawa/proxmox-ceph-in-a-home-lab-building-a-cluster-from-a-single-drive-surviving-the-cli-battle-4f6a</guid>
      <description>&lt;p&gt;Building a &lt;a href="https://pve.proxmox.com/pve-docs/chapter-pveceph.html" rel="noopener noreferrer"&gt;Ceph cluster on Proxmox VE&lt;/a&gt; in a home lab is a fantastic proving ground. The catch is that Ceph is inherently a powerful Enterprise-grade system – and by default, it demands &lt;a href="https://docs.ceph.com/en/latest/start/hardware-recommendations/" rel="noopener noreferrer"&gt;entire, raw physical drives (raw block devices)&lt;/a&gt; for its OSD (Object Storage Daemon) daemons.&lt;/p&gt;

&lt;p&gt;But what if your "IT Bunker" only has one physical server with a single extra 111 GB SSD, and you want to simulate a full-fledged 3-node cluster? My architecture looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;admin&lt;/strong&gt; (Physical Proxmox host)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;admin-02&lt;/strong&gt; (Nested VM on the main server)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;admin-03&lt;/strong&gt; (VM... running remotely on a separate desktop PC)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the story of how I "cheated" Ceph's strict mechanics, sliced a single drive into pieces, bypassed hypervisor GUI errors, and ultimately achieved the coveted &lt;code&gt;HEALTH_OK&lt;/code&gt; status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge 1: Handing an LVM Partition over to Ceph
&lt;/h2&gt;

&lt;p&gt;My plan was to split the physical &lt;code&gt;sdc&lt;/code&gt; (111.8 GB) drive on the main server into three 35 GB logical volumes using &lt;a href="https://wiki.debian.org/LVM" rel="noopener noreferrer"&gt;LVM (Logical Volume Manager)&lt;/a&gt;. Here is the &lt;code&gt;lsblk&lt;/code&gt; output from the main host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sdc                            111.8G LVM2_member disk 
├─vg_ceph_lab-ceph_admin          35G             lvm  
└─vg_ceph_lab-ceph_admin02        35G             lvm  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first node (&lt;code&gt;admin&lt;/code&gt;) took its piece without a hitch. The trouble started when I tried to add an OSD for the second node (&lt;code&gt;admin-02&lt;/code&gt;) via the main host's CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@admin:~# pveceph osd create /dev/vg_ceph_lab/ceph_admin
unable to get device info &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s1"&gt;'/dev/dm-6'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt; Proxmox and Ceph hate shared LVM paths and standard partitions when creating an OSD. Since &lt;code&gt;admin-02&lt;/code&gt; is a virtual machine, I leveraged the built-in &lt;a href="https://www.google.com/search?q=%5Bhttps://pve.proxmox.com/pve-docs/qm.1.html%5D(https://pve.proxmox.com/pve-docs/qm.1.html)" rel="noopener noreferrer"&gt;QEMU/KVM Virtual Machine Manager (&lt;code&gt;qm&lt;/code&gt;) tool&lt;/a&gt;. Instead of fighting Ceph daemons on the host, I attached the virtual volume directly into the &lt;code&gt;admin-02&lt;/code&gt; VM, bypassing the filesystem layer entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;qm &lt;span class="nb"&gt;set &lt;/span&gt;103 &lt;span class="nt"&gt;-scsi2&lt;/span&gt; /dev/vg_ceph_lab/ceph_admin02
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the &lt;code&gt;admin-02&lt;/code&gt; VM, the OS saw new, "physical", and raw block hardware. A quick click on &lt;em&gt;Create OSD&lt;/em&gt; in the node's Proxmox GUI, and the drive initialized perfectly!&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge 2: An External Host and a Rebellious virt-manager
&lt;/h2&gt;

&lt;p&gt;That left the third node: &lt;code&gt;admin-03&lt;/code&gt;. The problem? This VM physically resided on my Linux Desktop PC across the room. The &lt;code&gt;qm set&lt;/code&gt; command was useless here because the sliced LVM volume was on the server in the rack.&lt;/p&gt;

&lt;p&gt;In a cluster architecture, Ceph doesn't care where the physical storage comes from. I decided to ditch the server's LVM for this node and create a new virtual disk locally on my PC. But trying to add a disk via the &lt;a href="https://virt-manager.org/" rel="noopener noreferrer"&gt;virt-manager GUI&lt;/a&gt; threw a hardware incompatibility error: &lt;code&gt;pc-q35-noble&lt;/code&gt;. The graphical interface completely refused to cooperate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt; When the GUI throws weird exceptions, the terminal is your best friend. I fell back on native tools from the &lt;a href="https://libvirt.org/docs.html" rel="noopener noreferrer"&gt;libvirt&lt;/a&gt; and QEMU stack.&lt;/p&gt;

&lt;p&gt;First, I manually generated the disk file using &lt;a href="https://www.google.com/search?q=%5Bhttps://qemu-project.gitlab.io/qemu/tools/qemu-img.html%5D(https://qemu-project.gitlab.io/qemu/tools/qemu-img.html)" rel="noopener noreferrer"&gt;&lt;code&gt;qemu-img&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;qemu-img create &lt;span class="nt"&gt;-f&lt;/span&gt; qcow2 /var/lib/libvirt/images/admin-03-ceph.qcow2 35G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I hard-attached the generated image to the offline VM configuration using &lt;code&gt;virsh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;virsh attach-disk Proxmox-Lab /var/lib/libvirt/images/admin-03-ceph.qcow2 vdb &lt;span class="nt"&gt;--config&lt;/span&gt; &lt;span class="nt"&gt;--subdriver&lt;/span&gt; qcow2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upon booting &lt;code&gt;admin-03&lt;/code&gt;, its internal Proxmox system recognized the &lt;code&gt;/dev/vdb&lt;/code&gt; drive. Adding the third OSD in the Proxmox GUI worked flawlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finale: Pools, Replication, and Green Status
&lt;/h2&gt;

&lt;p&gt;After adding the third OSD, I had a "Frankenstein" cluster: drives supplied by raw LVM from a physical server mixed with a remote &lt;code&gt;qcow2&lt;/code&gt; virtual disk. Did Ceph care? Not at all.&lt;/p&gt;

&lt;p&gt;The final step was to create a &lt;a href="https://docs.ceph.com/en/latest/rados/operations/pools/" rel="noopener noreferrer"&gt;Ceph Pool&lt;/a&gt;—the logical organizational layer where the actual data lands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Size: 3&lt;/strong&gt; (Every data block is replicated in 3 copies).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Min. Size: 2&lt;/strong&gt; (The cluster allows I/O writes as long as at least 2 out of 3 nodes are active).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CLI verification (on the main node) showed exactly what I had been fighting for all evening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@admin:~# ceph &lt;span class="nt"&gt;-s&lt;/span&gt;
  cluster:
    &lt;span class="nb"&gt;id&lt;/span&gt;:     25f2d0a2-d64b-42a2-b93d-99a560571e0e
    health: HEALTH_OK

  services:
    mon: 3 daemons, quorum admin,admin-02,admin-03 &lt;span class="o"&gt;(&lt;/span&gt;age 6m&lt;span class="o"&gt;)&lt;/span&gt;
    mgr: admin&lt;span class="o"&gt;(&lt;/span&gt;active, since 60m&lt;span class="o"&gt;)&lt;/span&gt;, standbys: admin-02, admin-03
    osd: 3 osds: 3 up &lt;span class="o"&gt;(&lt;/span&gt;since 6m&lt;span class="o"&gt;)&lt;/span&gt;, 3 &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;since 6m&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's check the &lt;a href="https://docs.ceph.com/en/latest/rados/operations/crush-map/" rel="noopener noreferrer"&gt;CRUSH (Controlled Replication Under Scalable Hashing)&lt;/a&gt; algorithm tree using the &lt;code&gt;ceph osd tree&lt;/code&gt; command to confirm the cluster topology:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@admin:~# ceph osd tree
ID  CLASS WEIGHT   TYPE NAME         STATUS  REWEIGHT  PRI-AFF
&lt;span class="nt"&gt;-1&lt;/span&gt;         0.10258 root default                      
&lt;span class="nt"&gt;-3&lt;/span&gt;         0.03419     host admin                     
 0   hdd  0.03419         osd.0          up   1.00000  1.00000
&lt;span class="nt"&gt;-5&lt;/span&gt;         0.03419     host admin-02                  
 1   hdd  0.03419         osd.1          up   1.00000  1.00000
&lt;span class="nt"&gt;-7&lt;/span&gt;         0.03419     host admin-03                  
 2   hdd  0.03419         osd.2          up   1.00000  1.00000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, Ceph's intelligent engine sees my crafted nodes as separate physical &lt;code&gt;hosts&lt;/code&gt; and correctly distributes the traffic, providing me with a single, unified &lt;code&gt;rbd&lt;/code&gt; (RADOS Block Device) storage pool labeled &lt;code&gt;ceph-storage&lt;/code&gt; in Proxmox.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Home lab experiments quickly expose the gap between dry documentation theory and actual implementations in simulated environments. This deployment taught me two key lessons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Understanding how to pass storage resources through the hypervisor (like the &lt;code&gt;qm set&lt;/code&gt; tool in PVE) is a game changer when Ceph insists on block access and you only have LVM partitions available.&lt;/li&gt;
&lt;li&gt; When GUI software (like desktop &lt;code&gt;virt-manager&lt;/code&gt; in my case) throws motherboard configuration errors, going back to the basics and pure CLI (&lt;code&gt;virsh&lt;/code&gt;, &lt;code&gt;qemu-img&lt;/code&gt;) lets you bypass those issues in just a few quick commands.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With a fully functioning, green cluster, I am ready for the next level: Chaos Engineering. I plan to disconnect drives while VMs are running just to see if my Ceph setup is truly as resilient as the documentation promises!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ceph</category>
      <category>proxmox</category>
    </item>
    <item>
      <title>DNS Troubleshooting on Windows: When the Internet Works but the Intranet Doesn’t</title>
      <dc:creator>Konrad Kądzielawa</dc:creator>
      <pubDate>Fri, 20 Mar 2026 12:10:01 +0000</pubDate>
      <link>https://dev.to/kkadzielawa/dns-troubleshooting-on-windows-when-the-internet-works-but-the-intranet-doesnt-1459</link>
      <guid>https://dev.to/kkadzielawa/dns-troubleshooting-on-windows-when-the-internet-works-but-the-intranet-doesnt-1459</guid>
      <description>&lt;p&gt;One of the more frustrating problems in IT is when a user says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“The internet works, but this one internal site doesn’t.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At first, it sounds simple. Public websites load fine. Email works. Teams works. But the internal app still refuses to open, or it loads painfully slowly.&lt;/p&gt;

&lt;p&gt;In situations like that, the application itself is not always the real problem. Quite often, the issue is one layer lower: DNS.&lt;/p&gt;

&lt;p&gt;That is also why I think DNS is worth understanding in a practical way, not just as a protocol you read about once and then forget. In day-to-day admin work, name resolution problems show up more often than people expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  A very typical scenario
&lt;/h2&gt;

&lt;p&gt;Let’s take a neutral example.&lt;/p&gt;

&lt;p&gt;A user tries to open:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;http://intranet-app01/portal&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;What they see is something like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the page does not open at all, or takes too long to load,&lt;/li&gt;
&lt;li&gt;public websites still work,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nslookup intranet-app01&lt;/code&gt; returns an IP address,&lt;/li&gt;
&lt;li&gt;but the internal application still behaves as if something is wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, it is very tempting to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“If &lt;code&gt;nslookup&lt;/code&gt; works, DNS is fine.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I used to think that too in simpler cases. But in real environments, that conclusion is often too optimistic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;nslookup&lt;/code&gt; helps — but does not always tell the full story
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/nslookup" rel="noopener noreferrer"&gt;&lt;code&gt;nslookup&lt;/code&gt;&lt;/a&gt; is still a useful tool. It is quick, simple, and good for checking what a DNS server returns for a given name.&lt;/p&gt;

&lt;p&gt;The catch is that a successful &lt;code&gt;nslookup&lt;/code&gt; result does &lt;strong&gt;not always mean the client is resolving names cleanly in real use&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Microsoft’s own &lt;a href="https://learn.microsoft.com/en-us/troubleshoot/windows-client/networking/troubleshoot-dns-client-resolution-issues" rel="noopener noreferrer"&gt;DNS client troubleshooting guide&lt;/a&gt; shows why this matters. If a machine has multiple DNS servers configured and some of them are unreachable, Windows may still eventually resolve the name — but only after retries and timeouts. From the user’s point of view, that can look like “the app is slow” or “the site is broken,” even though DNS eventually returns an answer.&lt;/p&gt;

&lt;p&gt;So when I troubleshoot DNS now, I try not to stop at:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Does the name resolve?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How does it resolve, from where, and how long does it take?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How Windows actually resolves a name
&lt;/h2&gt;

&lt;p&gt;One detail I find especially useful from Microsoft’s &lt;a href="https://learn.microsoft.com/en-us/troubleshoot/windows-client/networking/troubleshoot-dns-client-resolution-issues" rel="noopener noreferrer"&gt;troubleshooting article&lt;/a&gt; is that Windows does not immediately send every query to a DNS server.&lt;/p&gt;

&lt;p&gt;The client typically goes through these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;check the local DNS cache&lt;/li&gt;
&lt;li&gt;check the &lt;code&gt;Hosts&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;query the DNS server&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That means a “correct” answer is not always coming from the place you think it is.&lt;/p&gt;

&lt;p&gt;Sometimes the client is using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an old cached entry,&lt;/li&gt;
&lt;li&gt;a negative cache result,&lt;/li&gt;
&lt;li&gt;or a static entry in the &lt;code&gt;Hosts&lt;/code&gt; file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So even when name resolution appears to work, it is still worth asking whether you are looking at live DNS behavior or just local state.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple workflow I usually follow
&lt;/h2&gt;

&lt;p&gt;When an internal hostname behaves strangely, I try not to jump between random fixes. I usually go through a short set of checks in the same order.&lt;/p&gt;

&lt;p&gt;It does not solve every case, but it helps narrow things down quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Check the client’s DNS configuration
&lt;/h3&gt;

&lt;p&gt;I usually start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;ipconfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/all&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives me a quick view of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which DNS servers are configured,&lt;/li&gt;
&lt;li&gt;in what order,&lt;/li&gt;
&lt;li&gt;and whether anything looks off right away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I see multiple DNS servers, I want to know whether all of them are still reachable. Microsoft notes in its &lt;a href="https://learn.microsoft.com/en-us/troubleshoot/windows-client/networking/troubleshoot-dns-client-resolution-issues" rel="noopener noreferrer"&gt;DNS troubleshooting guide&lt;/a&gt; that unreachable DNS servers can introduce noticeable delays because the client keeps trying before moving on.&lt;/p&gt;

&lt;p&gt;That is one of those things that can make an application feel broken even though the underlying name eventually resolves.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Compare the default result with a specific DNS server
&lt;/h3&gt;

&lt;p&gt;Next, I usually run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;nslookup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;intranet-app01&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;nslookup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;intranet-app01.corp.example&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;192.0.2.53&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command tells me what happens with the machine’s default DNS configuration.&lt;/p&gt;

&lt;p&gt;The second helps remove some ambiguity, because I am querying a specific DNS server directly.&lt;/p&gt;

&lt;p&gt;According to the official &lt;a href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/nslookup" rel="noopener noreferrer"&gt;&lt;code&gt;nslookup&lt;/code&gt; documentation&lt;/a&gt;, you can provide the DNS server explicitly in non-interactive mode, which makes this a useful comparison step.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Test the same name with &lt;code&gt;Resolve-DnsName&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;After that, I often repeat the test in PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Resolve-DnsName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;intranet-app01.corp.example&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Resolve-DnsName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;intranet-app01.corp.example&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;192.0.2.53&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Microsoft’s &lt;a href="https://learn.microsoft.com/en-us/powershell/module/dnsclient/resolve-dnsname?view=windowsserver2025-ps" rel="noopener noreferrer"&gt;&lt;code&gt;Resolve-DnsName&lt;/code&gt; documentation&lt;/a&gt; describes it as functionally similar to &lt;code&gt;nslookup&lt;/code&gt;, and I find it really useful when I want clearer, more structured output.&lt;/p&gt;

&lt;p&gt;It is especially helpful when I want to compare answers from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the default client path,&lt;/li&gt;
&lt;li&gt;and a specific DNS server I actually trust for that zone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This does not automatically fix anything, of course, but it helps answer a more precise question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Is the issue really DNS, or is it the path the client is taking to get the answer?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4. Inspect the local DNS cache
&lt;/h3&gt;

&lt;p&gt;If the results still feel inconsistent, I check the local resolver cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;ipconfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/displaydns&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Microsoft’s &lt;a href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/ipconfig" rel="noopener noreferrer"&gt;&lt;code&gt;ipconfig&lt;/code&gt; reference&lt;/a&gt; explains that &lt;code&gt;/displaydns&lt;/code&gt; shows the DNS resolver cache, including entries loaded from the local &lt;code&gt;Hosts&lt;/code&gt; file and recently resolved records.&lt;/p&gt;

&lt;p&gt;I like this step because it helps explain a lot of confusing cases.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;maybe the client is not querying DNS at all anymore,&lt;/li&gt;
&lt;li&gt;maybe it is holding on to an old result,&lt;/li&gt;
&lt;li&gt;maybe there is a local entry that makes the test misleading.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Flush the cache if I suspect stale local state
&lt;/h3&gt;

&lt;p&gt;If I think the cache might be part of the problem, I clear it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;ipconfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/flushdns&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, this is documented by Microsoft in the &lt;a href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/ipconfig" rel="noopener noreferrer"&gt;&lt;code&gt;ipconfig&lt;/code&gt; command reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I would not treat this as a magic fix, because it is not. But it is a reasonable troubleshooting step when I want to remove stale local state before testing again.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Check whether the short name is the real issue
&lt;/h3&gt;

&lt;p&gt;This is probably one of the most common things behind “it works for some people but not for others.”&lt;/p&gt;

&lt;p&gt;Users often type:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;http://intranet-app01&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;instead of:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;http://intranet-app01.corp.example&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That difference matters more than it seems.&lt;/p&gt;

&lt;p&gt;Microsoft’s &lt;a href="https://learn.microsoft.com/en-us/troubleshoot/windows-client/networking/troubleshoot-dns-client-resolution-issues" rel="noopener noreferrer"&gt;DNS troubleshooting guide&lt;/a&gt; includes examples showing how the DNS suffix search list can affect resolution time and behavior. If the suffix list is long, badly ordered, or just not aligned with the environment, short-name resolution can become slow or inconsistent.&lt;/p&gt;

&lt;p&gt;So sometimes the problem is not that DNS is “down.”&lt;/p&gt;

&lt;p&gt;Sometimes the problem is simply that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the short name is ambiguous,&lt;/li&gt;
&lt;li&gt;the client is trying the wrong suffixes first,&lt;/li&gt;
&lt;li&gt;or the right answer only shows up after too many failed attempts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What usually turns out to be wrong
&lt;/h2&gt;

&lt;p&gt;In many environments, DNS is not completely broken.&lt;/p&gt;

&lt;p&gt;It is just &lt;strong&gt;slow, inconsistent, or slightly misleading&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In practice, I have found that the most common causes are things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the primary DNS server is unreachable,&lt;/li&gt;
&lt;li&gt;DNS servers are configured in a bad order,&lt;/li&gt;
&lt;li&gt;the client is using a cached result,&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;Hosts&lt;/code&gt; file contains an override,&lt;/li&gt;
&lt;li&gt;users rely on short names instead of FQDNs,&lt;/li&gt;
&lt;li&gt;the DNS suffix search list is too long or poorly ordered,&lt;/li&gt;
&lt;li&gt;or the application is simply less tolerant of delays than a browser is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why I try not to stop at “&lt;code&gt;nslookup&lt;/code&gt; works.”&lt;/p&gt;

&lt;p&gt;That statement may be true and still not explain the user’s experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I actually want to answer
&lt;/h2&gt;

&lt;p&gt;When I troubleshoot something like this, I am not really asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Does the name resolve?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I am usually asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How is the client resolving it, what is it talking to first, and where is the delay happening?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That small shift in thinking helps a lot.&lt;/p&gt;

&lt;p&gt;It turns troubleshooting from random guessing into a more predictable workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick checklist I like to keep in mind
&lt;/h2&gt;

&lt;p&gt;Here is the short version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. ipconfig /all
2. nslookup shortname
3. nslookup FQDN &amp;lt;specific_DNS_server&amp;gt;
4. Resolve-DnsName -Name FQDN
5. Resolve-DnsName -Name FQDN -Server &amp;lt;specific_DNS_server&amp;gt;
6. ipconfig /displaydns
7. ipconfig /flushdns
8. test the FQDN instead of the short name
9. review DNS server order and suffix search list
10. only then move deeper into the application itself
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;A lot of DNS issues do not look like DNS issues at first.&lt;/p&gt;

&lt;p&gt;They look like browser problems, application issues, random network instability, or “something weird on this one computer.”&lt;/p&gt;

&lt;p&gt;But sometimes the underlying cause is much simpler than that:&lt;/p&gt;

&lt;p&gt;the client is asking the wrong place, in the wrong order, for too long.&lt;/p&gt;

&lt;p&gt;That is why I try to troubleshoot DNS methodically instead of jumping straight into random fixes. It saves time, and it usually makes the actual problem easier to explain — both to yourself and to the user who just wants the internal site to work again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;If you want to dig a bit deeper, these official Microsoft references are worth bookmarking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/nslookup" rel="noopener noreferrer"&gt;&lt;code&gt;nslookup&lt;/code&gt; command reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/powershell/module/dnsclient/resolve-dnsname?view=windowsserver2025-ps" rel="noopener noreferrer"&gt;&lt;code&gt;Resolve-DnsName&lt;/code&gt; PowerShell documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/ipconfig" rel="noopener noreferrer"&gt;&lt;code&gt;ipconfig&lt;/code&gt; command reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/troubleshoot/windows-client/networking/troubleshoot-dns-client-resolution-issues" rel="noopener noreferrer"&gt;Troubleshoot DNS client name resolution issues&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>dns</category>
      <category>microsoft</category>
      <category>networking</category>
    </item>
    <item>
      <title>Building a Zero-Maintenance SysOps Knowledge Base with React and the Dev.to API</title>
      <dc:creator>Konrad Kądzielawa</dc:creator>
      <pubDate>Wed, 18 Feb 2026 12:25:24 +0000</pubDate>
      <link>https://dev.to/kkadzielawa/building-a-zero-maintenance-sysops-knowledge-base-with-react-and-the-devto-api-36lp</link>
      <guid>https://dev.to/kkadzielawa/building-a-zero-maintenance-sysops-knowledge-base-with-react-and-the-devto-api-36lp</guid>
      <description>&lt;p&gt;After spending nearly a decade navigating different areas of IT—from building complex e-commerce frontends to managing the underlying Linux servers and infrastructure—I decided it was time to completely rebuild my personal portfolio.&lt;/p&gt;

&lt;p&gt;My old website was full of typical "frontend fireworks": heavy animations, flashy graphics, and complex layouts. But as my career naturally shifted towards IT Systems Administration and infrastructure management, that style no longer resonated with my daily work. I wanted a space that felt like home to a SysOps engineer: raw, minimalist, and terminal-inspired.&lt;/p&gt;

&lt;p&gt;However, as an admin, I had a strict set of requirements for this new platform:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero maintenance:&lt;/strong&gt; I don't want to deal with database backups or traditional CMS panels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No local Markdown files:&lt;/strong&gt; Pushing a commit to GitHub just to fix a typo in a blog post is an overkill that breaks the writing flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single Source of Truth:&lt;/strong&gt; I want to write technical notes once and have them automatically distribute to my community profile and my personal website.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The solution? &lt;strong&gt;Using the Dev.to API as a free, headless CMS for a React Single Page Application.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is a technical breakdown of how I designed this architecture&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Fetching the Data (The API Layer)
&lt;/h2&gt;

&lt;p&gt;Dev.to provides a fantastic, mostly undocumented perk for developers: a public, read-only API that returns your articles in JSON format. &lt;/p&gt;

&lt;p&gt;Instead of hardcoding data, my React app fetches the article list directly from my Dev.to profile upon initialization. To prevent unnecessary network requests and ensure a 0ms load time when navigating back and forth, I implemented a simple in-memory cache outside the component lifecycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Global cache for instant UI rendering&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cachedArticles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DevToArticle&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MainAdmin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setArticles&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DevToArticle&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cachedArticles&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cachedArticles&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cachedArticles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[https://dev.to/api/articles?username=kkadzielawa](https://dev.to/api/articles?username=kkadzielawa)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setArticles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;cachedArticles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Cache the response&lt;/span&gt;
          &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="c1"&gt;// ... render logic&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Dynamic Tag Filtering
&lt;/h2&gt;

&lt;p&gt;Since the Dev.to API returns an array of tags (&lt;code&gt;tag_list&lt;/code&gt;) for each article, I didn't need to build a custom taxonomy system. I simply extract all unique tags dynamically from the fetched payload and generate a terminal-like filter menu &lt;code&gt;[ #linux ]&lt;/code&gt;, &lt;code&gt;[ #docker ]&lt;/code&gt;, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The "Optimistic UI" Trick
&lt;/h2&gt;

&lt;p&gt;One downside of the &lt;code&gt;/articles&lt;/code&gt; list endpoint is that it doesn't return the full Markdown body of the posts. To get the content, you have to make a second request to &lt;code&gt;/articles/{id}&lt;/code&gt; when the user clicks on a specific note.&lt;/p&gt;

&lt;p&gt;To prevent the annoying "layout shift" and loading spinners, I used React Router's &lt;code&gt;state&lt;/code&gt; to pass the title of the article from the main list directly to the reader component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; 
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`/note/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; 
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// Pass the title forward!&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thanks to this, the note viewer immediately renders the title and structure, while the actual Markdown body stream loads discreetly in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Rendering Markdown with Syntax Highlighting
&lt;/h2&gt;

&lt;p&gt;Once the API returns the &lt;code&gt;body_markdown&lt;/code&gt;, it needs to be safely parsed into HTML. Since this is a SysOps blog, code snippets must look flawless. I combined &lt;code&gt;react-markdown&lt;/code&gt; with &lt;code&gt;react-syntax-highlighter&lt;/code&gt; using the VS Code Dark Plus theme for a native IDE feel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ReactMarkdown&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-markdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Prism&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;SyntaxHighlighter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-syntax-highlighter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;vscDarkPlus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-syntax-highlighter/dist/esm/styles/prism&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Inside the render function:&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReactMarkdown&lt;/span&gt;
  &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/language-&lt;/span&gt;&lt;span class="se"&gt;(\w&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;inline&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SyntaxHighlighter&lt;/span&gt;
          &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;vscDarkPlus&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;PreTag&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"div"&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SyntaxHighlighter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;code&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"inline-code"&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;code&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ReactMarkdown&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By treating Dev.to as a completely touchless, headless CMS, I achieved a perfect &lt;strong&gt;Separation of Concerns&lt;/strong&gt;. Dev.to handles the database, image hosting, SEO, and the writing experience. My React application acts purely as a customized, terminal-themed presentation layer.&lt;/p&gt;

&lt;p&gt;My personal portfolio updates automatically. That's the exact kind of automation every IT Admin loves.&lt;/p&gt;

&lt;p&gt;Now, whenever I want to drop some technical meat (like a new Linux configuration or securing a server environment), I just hit "Publish" on Dev.to, and it's instantly live.&lt;/p&gt;

&lt;p&gt;Check out the live result at &lt;a href="https://kadzielawa.dev" rel="noopener noreferrer"&gt;kadzielawa.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Server-Side Rendering (SSR) vs. Client-Side Rendering (CSR): The Fascinating World of Page Rendering</title>
      <dc:creator>Konrad Kądzielawa</dc:creator>
      <pubDate>Fri, 01 Dec 2023 15:28:31 +0000</pubDate>
      <link>https://dev.to/kkadzielawa/server-side-rendering-ssr-vs-client-side-rendering-csr-the-fascinating-world-of-page-rendering-2fba</link>
      <guid>https://dev.to/kkadzielawa/server-side-rendering-ssr-vs-client-side-rendering-csr-the-fascinating-world-of-page-rendering-2fba</guid>
      <description>&lt;p&gt;The choice between Server-Side Rendering (SSR) and Client-Side Rendering (CSR) is not just a technological decision. It's like choosing between classic art and modern cinema - each has its unique characteristics and hidden treasures to discover.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server-Side Rendering (SSR): Classical Elegance
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Advantages of SSR:&lt;/strong&gt;&lt;br&gt;
SEO Magic: With SSR, your website is like a well-written book, easy to find and read by search engines.&lt;br&gt;
Fast First Impression: SSR delivers content quickly, like an express courier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages of SSR:&lt;/strong&gt;&lt;br&gt;
Heavier Server Load: SSR is a bit like working full-time for the server - it requires continuous attention and resources.&lt;/p&gt;

&lt;p&gt;SSR is like visiting an elegant, classical art gallery, where each artwork is carefully prepared and presented with attention to detail. In SSR, every website is like a finely crafted painting, ready to be admired immediately upon entry. This approach combines solidity and reliability, offering users and search engines complete, ready-to-consume content. As a result, SSR pages are like well-tailored suits: stylish, professional, and always in good taste, perfectly suited for the world of SEO and making a fast first impression.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.google.com/search?q=Server-side+rendering" rel="noopener noreferrer"&gt;Discover the secrets of SSR.&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Client-Side Rendering (CSR): Modern Theater of Interaction
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Advantages of CSR:&lt;/strong&gt;&lt;br&gt;
Mastery of Interaction: CSR is like a director who adapts the scene to the audience's mood - dynamic and full of surprises.&lt;br&gt;
Server Resource Savings: It's like remote work for the server - fewer loads, more flexibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages of CSR:&lt;/strong&gt;&lt;br&gt;
SEO Puzzle: For search engines, CSR pages can sometimes be like a complicated puzzle.&lt;br&gt;
CSR is nothing but entering a modern theater of interaction, where each scene is created live, in response to the viewer's actions. CSR is like a dynamic, improvised performance in which the website's content unfolds and evolves in real-time in the user's browser. It's a world where interactivity and adaptability play the lead roles, and the website is constantly in motion, like a crowd in a bustling city center. CSR brings what is most modern and exciting in web development – a site that not only responds but dances to the user's rhythm.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.google.com/search?q=client+side+rendering" rel="noopener noreferrer"&gt;Immerse yourself in the world of CSR.&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Blending Dimensions: Hybrid Rendering Techniques
&lt;/h2&gt;

&lt;p&gt;Using hybrid rendering methods is like the art of culinary fusion, where the chef combines traditional ingredients with modern cooking techniques to create surprising and complex dishes. In the world of web development, hybrid approaches blend the solidity and reliability of SSR with the flexibility and dynamism of CSR, creating unique "dishes" in the digital space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Innovation in technology:&lt;/strong&gt; Tools like Next.js for React are redefining the frameworks in which developers create web experiences. By combining the fast loading and SEO optimization offered by SSR with the reactivity and rich interactions of CSR, hybrid approaches create platforms that are both efficient and engaging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interesting IT fact:&lt;/strong&gt; Using hybrid approaches in web design is like balancing between two worlds. Developers can decide which elements of the website are best rendered on the server-side and which on the client-side, similar to how a chef decides when to combine classic techniques with modern ones for the best effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion:&lt;/strong&gt; Hybrid rendering techniques open up new possibilities in web development, allowing for the creation of websites that combine the best features of both worlds. Just as in culinary art, where the combination of old and new techniques creates amazing dishes, in web design, combining SSR and CSR creates unforgettable digital experiences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;br&gt;
The choice between SSR and CSR is not just a technical decision. It's a choice between two different ways of storytelling on the internet - one old and proven, and the other new and exciting. Understanding their unique features and benefits allows for the creation of better, more engaging websites.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Programming in 2033 – Vision of the Future of Software Development</title>
      <dc:creator>Konrad Kądzielawa</dc:creator>
      <pubDate>Fri, 01 Dec 2023 15:20:47 +0000</pubDate>
      <link>https://dev.to/kkadzielawa/programming-in-2033-vision-of-the-future-of-software-development-2bbb</link>
      <guid>https://dev.to/kkadzielawa/programming-in-2033-vision-of-the-future-of-software-development-2bbb</guid>
      <description>&lt;p&gt;In the last decade, the world of technology has undergone a metamorphosis, and software development has become a part of an exciting future. In 2033, the work of programmers is full of extraordinary experiences, seeming like something out of fantasy from years past, which we could only dream of not too long ago. Let's contemplate what their reality looks like... let's delve into this future, discovering the latest innovations and futuristic trends in technology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code-Generating Algorithms:&lt;/strong&gt;&lt;br&gt;
In the year 2033, programmers have become supervisors and process engineers, having long delegated some tasks to advanced code-generating algorithms. Thanks to advanced tools, programmers exclusively use a thought interface to communicate with artificial intelligence systems that create and optimize code according to their intentions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#Example: A programmer thinks about a function to add two numbers.&lt;br&gt;
Text = "Create a function that adds two numbers."&lt;br&gt;
AI.create_code(text)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.blog/2023-04-14-how-generative-ai-is-changing-the-way-developers-work/" rel="noopener noreferrer"&gt;Discover how AI is changing programming.&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The World of Cybersecurity:&lt;/strong&gt;&lt;br&gt;
In an era full of advanced hackers and AI, the importance of cybersecurity has reached its zenith. Programmers are now cyber-wizards, using tools based on neural algorithms to defend our digital worlds against constantly evolving threats.&lt;/p&gt;

&lt;p&gt;Cybersecurity in 2033 is not just about fighting threats; it's also about the art of predicting potential attacks through predictive analysis.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cybriant.com/top-cyber-security-websites-of-2022/" rel="noopener noreferrer"&gt;Explore the latest trends in cybersecurity.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Ubiquity of Clouds and Quantum Computing:&lt;/strong&gt;&lt;br&gt;
A key element of a programmer's work in 2033 is cloud computing and solutions based on quantum computing. Programmers use such computers to solve problems that were once thought impossible to solve in a traditional way. Serverless and microservices solutions have become the norm, requiring programmers to have the skills to manage complex, distributed systems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bluexp.netapp.com/blog/cvo-blg-the-future-of-cloud-computing-5-trends-you-must-know-about" rel="noopener noreferrer"&gt;Get to know the future of the cloud.&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Virtual and Augmented Reality in Everyday Work:&lt;/strong&gt;&lt;br&gt;
Programmers in 2033 use VR and AR technologies not only for design but also as their daily work tools. Using thought interfaces, they design three-dimensional software models that can be "touched" and edited in virtual space.&lt;/p&gt;

&lt;p&gt;In the world of VR, programmers work within their projects, moving through virtual spaces to solve problems and test applications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.toptal.com/designers/ui/vr-ar-design-guide" rel="noopener noreferrer"&gt;Immerse yourself in the world of VR/AR for programmers.&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;The Blossoming of Soft Skills:&lt;/strong&gt;&lt;br&gt;
In an era where machines handle technical details, soft skills have become a key asset for programmers. Communication, creativity, and the ability to work in teams are essential for developing innovative solutions.&lt;/p&gt;

&lt;p&gt;Programmers participate in simulations of customer interactions to refine their soft skills and understand user needs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.comptia.org/career-change/exploring-it/skills-for-it" rel="noopener noreferrer"&gt;Learn more about soft skills in IT.&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Never-ending Education:&lt;/strong&gt;&lt;br&gt;
In 2033, learning knows no bounds. Programmers are eternal students, using interactive learning interfaces that enable them to quickly grasp the latest technologies and concepts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#Example: An interactive AI-based learning interface.&lt;br&gt;
AI.learn("New_Technology")&lt;br&gt;
AI.connect_with_mentor("Name Surname")&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://teambuilding.com/blog/online-learning-platforms" rel="noopener noreferrer"&gt;Check out the best educational platforms for programmers.&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Conclusion:&lt;/strong&gt;&lt;br&gt;
The work of programmers in 2033 is an exciting journey into the future, full of interactions with intelligent algorithms, quantum computing, virtual worlds, and continuous learning. It's a world where humans and machines collaborate to create innovative solutions that transform the face of technological reality. Artificial intelligence, cloud solutions, VR/AR, and cybersecurity play crucial roles. This evolution of the profession demonstrates how adaptability and the ability to learn are essential in a dynamically changing world of technology.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
