<?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>Forem: Adam Leskis</title>
    <description>The latest articles on Forem by Adam Leskis (@lpmi13).</description>
    <link>https://forem.com/lpmi13</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F466674%2F21c099a4-62e2-4070-af25-2b11a5fe559d.jpeg</url>
      <title>Forem: Adam Leskis</title>
      <link>https://forem.com/lpmi13</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/lpmi13"/>
    <language>en</language>
    <item>
      <title>From Active Learning to Deliberate Practice: an iximiuz Labs case study</title>
      <dc:creator>Adam Leskis</dc:creator>
      <pubDate>Tue, 24 Mar 2026 08:59:53 +0000</pubDate>
      <link>https://forem.com/lpmi13/from-active-learning-to-deliberate-practice-an-iximiuz-labs-case-study-2jmo</link>
      <guid>https://forem.com/lpmi13/from-active-learning-to-deliberate-practice-an-iximiuz-labs-case-study-2jmo</guid>
      <description>&lt;p&gt;Much of the educational tech content on the internet is in the form of blogs/videos/tutorials/walkthroughs/etc, and a lot of it is good, some great, some not so great. One thing that the great stuff has in common is that it encourages a "Learning by Doing" approach. After all, you get better at building a REST API by actually building a REST API...not just reading about it, watching somebody else do it, or drawing out a diagram on a whiteboard.&lt;/p&gt;

&lt;p&gt;To be sure, a lot of those things I just mentioned (eg, reading about REST APIs) are absolutely necessary input to build your conceptual framework of technical concepts, foundational components, and how the entire system fits together to accomplish a goal. There's very little chance you could just guess at what it should be and get it right on the first, or even 100th time, without &lt;em&gt;SOME&lt;/em&gt; context around what you should do.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxvet5t4dlb97f55u2cc0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxvet5t4dlb97f55u2cc0.gif" alt=" " width="245" height="132"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;And with this context built through content, active learning is an excellent learning strategy to extend and consolidate the knowledge that you gain through reading/watching/hearing/etc about different technical concepts. By actually performing the behavior required to create the thing you've learned about (eg, our example REST API), you get all sorts of added benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;feedback on whether the thing works&lt;/li&gt;
&lt;li&gt;having to deal with errors when it doesn't work&lt;/li&gt;
&lt;li&gt;seeing where the gaps in your knowledge still are&lt;/li&gt;
&lt;li&gt;exposing hidden assumptions about whether certain steps in the tutorial are obvious (they usually aren't)&lt;/li&gt;
&lt;li&gt;the satisfaction of completing something&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;

&lt;h2&gt;
  
  
  How This Content Gets Created
&lt;/h2&gt;

&lt;p&gt;One of the greatest shortcomings, however, in this wealth of content on active learning is that each of the examples/scenarios/challenges needs to be created by a human.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In before: I hope you're not suggesting we just create content entirely with AI...no, I'm not going to argue that, since I think it would be unnecessarily wastefully, and we can get pretty far anyway without it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Take, for example, a learning exercise where a learner is dropped into a linux environment, and has to find out why a systemd service has stopped. It used to be running, but now it's not, and eventually, with some helpful hints from the learning platform, the learner finds that permissions have been updated on some files that the service needs, and fixing those allows it to start again.&lt;/p&gt;

&lt;p&gt;The learner has actually applied some knowledge to fix a system issue and rejoices in the glow of active learning success!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7yezabr6ob8d71dgnx8v.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7yezabr6ob8d71dgnx8v.gif" alt=" " width="480" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;And now the learner would like to do it again, so they press "start" again on the activity, and they're dropped back into the same linux system, with the exact same instructions, the exact same error, and the exact same fix.&lt;/p&gt;

&lt;p&gt;They remember probably 80% of what they just did (it was only 5 minutes ago), and so they don't have much trouble fixing it again. After repeating this about 5 times, the learner can go on autopilot, just repeating the commands from memory, and maybe even jumping directly to the fix without needing to go through the whole debugging flow.&lt;/p&gt;

&lt;p&gt;This active learning can become passive when it's the same thing over and over, and can begin to be more of a test of memory than actual knowledge application. &lt;/p&gt;

&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;

&lt;h2&gt;
  
  
  Deliberate practice
&lt;/h2&gt;

&lt;p&gt;A much stronger paradigm supported by the work of researchers like Ericsson (1993), is the framework of deliberate practice (the seminal article on deliberate practice is &lt;a href="https://graphics8.nytimes.com/images/blogs/freakonomics/pdf/DeliberatePractice(PsychologicalReview).pdf" rel="noopener noreferrer"&gt;here&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The main points are that the learner needs to be engaged in narrowly focused intentional repetition, centered on a particular skill that they're trying to improve. So we could reimagine our previous example within this framing to have the following structure:&lt;/p&gt;

&lt;p&gt;The learner still enters the familiar linux environment, but it's unclear which systemd service is having issues (since this is randomized at the start of the activity). Through investigation, the learner isolates which service it is and proceeds to update the file permissions to fix the service. On the next attempt, it's a different service that is having issues, and so the learner is prompted to use a specific process to isolate which service is misbehaving. This can continue as long as the learner wants to continue practicing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that this new framing exposes a hidden complexity in the original activity...it's actually combining &lt;strong&gt;two&lt;/strong&gt; realistic debugging activities: identifying a failing service and using file system permissions to fix the issue.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So you could also imagine a slightly different path based on the original scenario as the learner in the linux system, and where the learner already knows (or is told) which system is behaving problematically, but the underlying permissions issue is randomized and requires investigation and remediation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the user the service is running under is randomized, which has implications for the file permissions necessary&lt;/li&gt;
&lt;li&gt;the binary of the service isn't executable&lt;/li&gt;
&lt;li&gt;the log directory isn't writable due to permissions mismatch&lt;/li&gt;
&lt;li&gt;the ownership of a file directory is correct, but new files don't inherit the ownershipt because &lt;code&gt;setgid&lt;/code&gt; is missing&lt;/li&gt;
&lt;li&gt;various permission bit issues &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compared to the single repeated scenario, this has some structural differences and advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the learning objective can be scoped down and isolated, which also allows more targeted feedback and assessment&lt;/li&gt;
&lt;li&gt;the practice is necessarily more deliberate and intentional, since the fix can't be guessed ahead of time, so the role of short-term memory changes to be about the process rather than the solution&lt;/li&gt;
&lt;li&gt;there's actually a reason for the learner to repeat the activity multiple times&lt;/li&gt;
&lt;li&gt;the variation now forces the learner to exhibit the behavior we're trying to gain improvement in (related to the specific learning objective)&lt;/li&gt;
&lt;li&gt;this approach is supported by research (as mentioned above)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0175jgx2ly5g1ko3p4sc.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0175jgx2ly5g1ko3p4sc.gif" alt=" " width="480" height="206"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;

&lt;h2&gt;
  
  
  Case Study #1 - Kubernetes OWASP Top Ten
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://labs.iximiuz.com/playgrounds/my-my-k3s-de88e13a" rel="noopener noreferrer"&gt;https://labs.iximiuz.com/playgrounds/my-my-k3s-de88e13a&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a playground set up to give the learner practice with running a vulnerability scanning tool (in this case, kubescape) to identify and fix a randomized security vulnerability from the &lt;a href="https://owasp.org/www-project-kubernetes-top-ten/" rel="noopener noreferrer"&gt;OWASP Kubernetes Top 10 list&lt;/a&gt; in a running cluster.&lt;/p&gt;

&lt;p&gt;The very specific and measurable learning objective is editing a kubernetes manifest/resource to fix the vulnerability. While this &lt;strong&gt;does&lt;/strong&gt; look a bit like our original example of both finding/fixing an issue, the scanner bit is very much just repeated commands, and there's not much of a learning objective to target there.&lt;/p&gt;

&lt;p&gt;Additionally, this first part of the activity is arguably not very authentic. Not many people are actually manually running scanners to identify vulnerable misconfigurations in their K8s clusters, since this is usually delegated to the CI pipelines (and rightly so!).&lt;/p&gt;

&lt;p&gt;The second part is more defensible, as a means to give learners a mapping between vulnerability category (K01 - insecure workload configurations, like running as root) and what that actually looks like in a manifest.&lt;/p&gt;

&lt;p&gt;While it does have gaps, it's a working MVP of a system that automatically presents a situation and gives feedback about whether the learner achieved a given outcome. So in terms of our discussion of deliberate practice, it's a bit closer to something that's supported by the research.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;

&lt;h2&gt;
  
  
  Case Study #2 - Log Parsing in Linux
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://labs.iximiuz.com/playgrounds/log-parser-lab-k3s-dd84febf" rel="noopener noreferrer"&gt;https://labs.iximiuz.com/playgrounds/log-parser-lab-k3s-dd84febf&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This playground is configured to present the learner with an opportunity to use common Linux tools (eg, &lt;code&gt;grep, sed, awk, wc&lt;/code&gt;) to parse various log types (nginx, apache, syslog) for various investigative purposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;finding the top unique IP address triggering failures&lt;/li&gt;
&lt;li&gt;summing of bytes for an IP with a specific HTTP method over a given time period&lt;/li&gt;
&lt;li&gt;finding the user with the most failed SSH logins over the last hour&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Similar to the previous case, a strength here is that the learner needs to convert the requirements into a chain of commands. So it's also narrowly focused, is completely self-contained, and can continue as long as the learner wants to practice.&lt;/p&gt;

&lt;p&gt;Since both the log type and investigation focus are varied at random for each new exercise, it's possible for the learner to use their short-term memory for applying knowledge of the various linux commands, rather than the keystrokes to type.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;

&lt;h2&gt;
  
  
  Disclaimer - We Need More Tools in General
&lt;/h2&gt;

&lt;p&gt;Having said all of the above, why might it still be the wrong thing to do?&lt;/p&gt;

&lt;p&gt;While I'm obviously biased towards the deliberate practice approach, it's not a format that a lot of learners are familiar with, specifically because it isn't very common.&lt;/p&gt;

&lt;p&gt;In addition, other strands of cognitive research which discuss desirable difficulty (another good thing that deliberate practice incorporates), has a downside in that learners can feel like they're learning less. This is also a proposed reason for why, given the choice, learners still gravitate towards youtube walkthroughs and blog tutorials:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;They feel like they're learning more because it's easy and they enjoy it&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So it &lt;strong&gt;could be&lt;/strong&gt; that this format isn't something that learners are even interested in, because they themselves feel like they're learning less from it.&lt;/p&gt;

&lt;p&gt;Another challenge is how to implement these types of activities from a technical perspective. There are a few different challenges like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;where/how is this activity presented to learners (web browser, terminal cli, etc)?&lt;/li&gt;
&lt;li&gt;what mechanism is used to randomize the activities?&lt;/li&gt;
&lt;li&gt;how do we assess whether the learning objective has been achieved?&lt;/li&gt;
&lt;li&gt;what format is the feedback on unsuccessful attempts, and when is it given?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nevertheless, more choice in how users like to learn new things is never a bad thing, and with the internet, we can create and share these things at basically zero marginal cost. More is better, and I'm here for it!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fho580le3zonwuodpxzgc.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fho580le3zonwuodpxzgc.gif" alt=" " width="480" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>teched</category>
      <category>iximiuz</category>
      <category>education</category>
      <category>learning</category>
    </item>
    <item>
      <title>An Iximiuz Cluster of Clusters with Tailscale and Cilium</title>
      <dc:creator>Adam Leskis</dc:creator>
      <pubDate>Thu, 19 Mar 2026 21:57:18 +0000</pubDate>
      <link>https://forem.com/lpmi13/an-iximiuz-cluster-of-clusters-with-tailscale-and-cilium-43d4</link>
      <guid>https://forem.com/lpmi13/an-iximiuz-cluster-of-clusters-with-tailscale-and-cilium-43d4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Editors note: This was originally much more ambitious, but for various reasons detailed below it had to be scaled back a bit. It's still pretty cool.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The final k8s cluster project is &lt;a href="https://github.com/lpmi-13/kubernetes-the-hard-way-iximiuz-plus-tailscale-cilium" rel="noopener noreferrer"&gt;here&lt;/a&gt; and the thing I build to visualize it is &lt;a href="https://github.com/lpmi-13/hubble-gazer" rel="noopener noreferrer"&gt;here&lt;/a&gt;, but read on for the story of how I got there!&lt;/p&gt;




&lt;p&gt;So I got hooked on the kubernetes-the-hard-way walkthroughs a couple years back and ran through them on various different public cloud providers. Since &lt;a href="https://labs.iximiuz.com" rel="noopener noreferrer"&gt;iximiuz Labs&lt;/a&gt; is my current favorite place to play, I thought I would give it a go there as well.&lt;/p&gt;

&lt;p&gt;It turns out that somebody else &lt;a href="https://labs.iximiuz.com/courses/kubernetes-the-very-hard-way-0cbfd997" rel="noopener noreferrer"&gt;already did this&lt;/a&gt;, and it's way better than I would have done anyway, so I guess I might as well just forget about it.&lt;/p&gt;

&lt;p&gt;...or maybe not!!!!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faw9m53k65o7oidyou8fl.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faw9m53k65o7oidyou8fl.gif" alt=" " width="480" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;What if, I thought to myself...just what IF instead of the traditional 1-3 controller nodes and 2-3 worker nodes (these numbers, I assume, are used in the typical k8s the hard way due to cost), I instead networked together not only more worker nodes, but different networks altogether?!?!&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way" rel="noopener noreferrer"&gt;canonical walkthrough&lt;/a&gt; uses only 4 machines (1 controller and 3 workers) all in the same subnet (this, as we shall see, MASSIVELY simplifies the networking). I decided that since I had so much fun playing with Tailscale in a &lt;a href="https://dev.to/lpmi13/remote-homelab-admin-with-tailscale-1jp2"&gt;previous post&lt;/a&gt;, I would use it a bit more to connect the various subnets across multiple iximiuz Labs playgrounds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fin2qyq1n7hpk1k2yy5o5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fin2qyq1n7hpk1k2yy5o5.png" alt="cluster of clusters original topology" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;

&lt;p&gt;So the original idea was to do all the inter-node routing via Tailscale, so that each controller and worker node would be able to address each other by hostname, which would map to the Tailscale overlay IP address (eg, &lt;code&gt;100.1.20.144&lt;/code&gt;). So even though we were wiring together a bunch of different playgrounds/subnets, all of the VMs (and hence also the eventual pods) would think they were on a flat network.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;

&lt;h2&gt;
  
  
  My Kingdom for Some Cool Viz
&lt;/h2&gt;

&lt;p&gt;Then I decided that just a big ole kubernetes cluster is pretty cool all by itself, but it's a bit difficult to get a good picture of exactly &lt;em&gt;HOW&lt;/em&gt; that differs from just a regular one (ie, all the other k8s the hard way walkthroughs).&lt;/p&gt;

&lt;p&gt;So I thought, maybe we could have some sort of visual, and even better to have a live traffic graph UI that would make it very obvious when we scaled a deployment up/down, or even just had a traffic generator hit one of the services with a ton of traffic and actually &lt;strong&gt;see&lt;/strong&gt; the difference between a 3 worker cluster and a 9 worker cluster.&lt;/p&gt;

&lt;p&gt;And so after a bit of research, it seemed like the easiest way to do this would be with some sort of a service mesh (the pods don't care about it, and there's usually baked in telemetry, so a ready data source that I could consume).&lt;/p&gt;

&lt;p&gt;The ones I was looking at were Istio/Envoy/Cilium, and since the iximiuz Labs VMs have a maximum of 4CPU and 8GiB RAM, I was trying to minimize the resource usage of the service mesh. Of those, not only is Cilium the clear winner, but it uses eBPF, which I though was really cool, and I wanted to play with it more.&lt;/p&gt;

&lt;p&gt;Okay, so I decided on Cilium, now what?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kb49yzd8szm3svf2qrc.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kb49yzd8szm3svf2qrc.jpg" alt=" " width="640" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;I started looking around for traffic visualization projects using Cilium (more specifically, hubble-observatory, which is Cilium's observability platform). The default view for hubble-ui, which is the included UI for hubble-observatory, just shows static traffic routes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9wtel6ptdv1vs7j51hq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9wtel6ptdv1vs7j51hq.png" alt=" " width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;Basically nothing I found was live traffic. There was a super old &lt;a href="https://github.com/Netflix/vizceral" rel="noopener noreferrer"&gt;Netflix project&lt;/a&gt;, but it wasn't maintained anymore.&lt;/p&gt;

&lt;p&gt;Hmmmm...there's nothing that does what I need, and I've been spending the last year or so getting acquainted with agentic workflows...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd14d0ryrw4u5qs744b1o.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd14d0ryrw4u5qs744b1o.gif" alt=" " width="640" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Just Make The Thing We Need
&lt;/h2&gt;

&lt;p&gt;Okay, so I need something running in the cluster that will grab data from hubble-observatory and I need a frontend that will open up and display something in a browser tab. Let's mix some Go and React and do the damned thing!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftayim89qv8efmjfvh4os.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftayim89qv8efmjfvh4os.png" alt=" " width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;I used codex for this, and I was actually surprised at how far it got on the first attempt. Most of the &lt;del&gt;work&lt;/del&gt; prompting was getting a local dev example up and running with sample data so I could see what it looked like and iterate on stuff...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no, those graph edges need to be thinner
 &lt;/li&gt;
&lt;li&gt;...thinner!
 &lt;/li&gt;
&lt;li&gt;alright, and stop resetting everything every 2 seconds, I said DECOUPLE the frontend and backend!
 &lt;/li&gt;
&lt;li&gt;WHY DID YOU SUGGEST WEBSOCKETS AGAIN?!?!! THIS COULD EASILY BE SERVER SENT EVENTS!!!! (to be fair, it probably doesn't matter that much in this case, since it's just a throwaway demo, but the agent always starts out with websockets even when it's definitely a worse choice than SSE)
 &lt;/li&gt;
&lt;li&gt;that's okay, but we need to auto-zoom when the topology changes
 &lt;/li&gt;
&lt;li&gt;forget everything I just said, we need to also cater for a mobile UI (I'm obsessed with running iximiuz Labs stuff on my phone, so this is a must for any of my projects)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...eventually, I had something that looked like I wanted, and I was ready to get it showing some actual live data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzjan652bwdr9qkwgfju.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzjan652bwdr9qkwgfju.png" alt=" " width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;

&lt;h2&gt;
  
  
  My MVP meets Live Cluster Reality
&lt;/h2&gt;

&lt;p&gt;Okay, so this is where things went a bit pear-shaped, and I spent probably 75% of the total project time.&lt;/p&gt;

&lt;p&gt;To start with, actually running a service inside kubernetes was going to require all kinds of extra stuff like RBAC, network policies, and pod specs for the deployment. No biggie, I've got an Intern to do that.&lt;/p&gt;

&lt;p&gt;I'll also admit to heavy ulitization of the scripts in the project, since there's no way I was gonna "hard way" build it from scratch every day (the playgrounds have a max lifetime of 8 hours, and since it's a group of them, I &lt;em&gt;guess&lt;/em&gt; we could have saved VM state and restarted them, but I also wanted to be sure the project would always come up from zero, so I went with the scripts), and I started to see some really weird failures and intermittent behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Everything would come up, but there would be no data coming through hubble-gazer&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I could see initial data in hubble-gazer, but then every 2 seconds (the same interval as the SSE pushes to the frontend) it got less and less until it completely disappeared&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;sometimes the scripts would just choke and things wouldn't start up&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The short answer to "why?" is that not only was I using an overlay network (Cilium VXLANs) wrapped in another overlay network (Tailscale), but the topology was such that we had 5 different subnets and 12 kubernetes nodes all communicating with each other for reachability.&lt;/p&gt;

&lt;p&gt;We ended up with a situation where some of the playgrounds would end up with the same public IP address but be on different private subnets, and so Tailscale would have to fall back to a relayed connection, which still works, but introduces latency.&lt;/p&gt;

&lt;p&gt;So things I had initially wanted to try, like HPA scaling based on CPU from the metrics server to show a cool demo of dynamic pod growth, just wouldn't work. I couldn't even get just KEDA with queues in redis to work, since the jitter and general network conditions just wouldn't support it.&lt;/p&gt;

&lt;p&gt;This was all very frustrating and a bit confusing to me, and I'm sure somebody with better networking knowledge could have sorted it out, but I had already learned a bunch and a deadline (I wanted this done by Kubecon EU 2026) was fast approaching.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd6a1fs485k1hwmkao0ce.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd6a1fs485k1hwmkao0ce.jpg" alt=" " width="500" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;

&lt;p&gt;Updating the topology so all of the worker nodes were in the same subnet basically solved everything, since now internode traffic didn't have to transit two layers of tunnelling, and we only used Tailscale for communication between the workers and the control plane.&lt;/p&gt;

&lt;p&gt;So we ended up with a much simplified topology, but it also finally worked and I could finish aligning hubble-gazer to show the live traffic.&lt;/p&gt;

&lt;p&gt;I had been hoping that the KEDA-based scaling I added would be more obvious, but oh well.&lt;/p&gt;

&lt;p&gt;The final topology (3 playgrounds, 9 VMs) looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyrjm6tyoxjhtnjiyidlr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyrjm6tyoxjhtnjiyidlr.png" alt="cluster of clusters final topology" width="800" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;

&lt;p&gt;And here are some tasty gifs of the live traffic being portforwarded from the jumpbox back to localhost to be accessible from the laptop I used to set up the cluster (that's why the browser URL bar says localhost:8888).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqhk7q8s1e49ytbhj3pq9.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqhk7q8s1e49ytbhj3pq9.gif" alt="All services and pods" width="480" height="253"&gt;&lt;/a&gt;&lt;br&gt;
All services and pods&lt;/p&gt;

&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2gb46kwoio59q37o6wtd.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2gb46kwoio59q37o6wtd.gif" alt="Application L4 traffic" width="600" height="316"&gt;&lt;/a&gt;&lt;br&gt;
Application L4 traffic, also grouped by worker node&lt;/p&gt;

&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3nxihzup92tu8h8vnqig.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3nxihzup92tu8h8vnqig.gif" alt="Application and System L7 traffic" width="720" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Application L7 traffic, as well as DNS queries&lt;/p&gt;

</description>
      <category>tailscale</category>
      <category>cilium</category>
      <category>kubernetes</category>
      <category>networking</category>
    </item>
    <item>
      <title>Custom rootFS for playgrounds in iximiuz Labs</title>
      <dc:creator>Adam Leskis</dc:creator>
      <pubDate>Sat, 21 Feb 2026 13:48:10 +0000</pubDate>
      <link>https://forem.com/lpmi13/custom-rootfs-for-playgrounds-in-iximiuz-labs-1p7g</link>
      <guid>https://forem.com/lpmi13/custom-rootfs-for-playgrounds-in-iximiuz-labs-1p7g</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ferfj9cwe6a5ygzlzv5h5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ferfj9cwe6a5ygzlzv5h5.jpg" alt="less servers is better than serverless" width="500" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The playgrounds on &lt;a href="https://labs.iximiuz.com" rel="noopener noreferrer"&gt;https://labs.iximiuz.com&lt;/a&gt; have always been amazing, but they recently got an upgrade to enable creation with a custom image for their root filesystem. Not only is this super cool, but it unlocks the ability for authors to significantly speed up the boot times of their playgrounds and labs by pre-installing and configuring everything to achieve massive startup speed improvements.&lt;/p&gt;

&lt;p&gt;I recently went on a journey to try this out for myself, since I'd never done it before, and this is a writeup of where I made silly mistakes, so you don't have to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqr1itgpxn452dql4rke3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqr1itgpxn452dql4rke3.gif" alt="great journey" width="480" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Project
&lt;/h2&gt;

&lt;p&gt;I'm heading to Kubecon EU next month, and I thought it would be cool to have a &lt;a href="https://github.com/lpmi-13/vulnerable-lab-operator?tab=readme-ov-file" rel="noopener noreferrer"&gt;new project&lt;/a&gt; to demo. The committee didn't accept my proposal to present, but the joke's on them, since obviously, with an iximiuz Labs playground, I can demo it to anybody with a web browser.&lt;/p&gt;

&lt;p&gt;Only problem is that when I run this locally, it downloads and compiles the kubernetes operator from source, plus a bunch of other stuff that slows down the initialization and basically means the user has to wait for about 5 minutes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnzxag8r3uoleev4crkv4.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnzxag8r3uoleev4crkv4.gif" alt="Mr Burns waiting" width="500" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;I knew that recent work had been done to allow you to &lt;a href="https://iximiuz.com/en/posts/iximiuz-labs-playgrounds-2.0/#how-to-bake-and-use-your-own-vm-rootfs" rel="noopener noreferrer"&gt;bring your own rootFS&lt;/a&gt; to an iximiuz Labs playground, and that sounded like exactly what I needed, but how to actually do it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The things that make this easy:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you can just set a link to an image in an OCI compliant container registry as the root file system for a VM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9o799o0xqswzzlbsaho4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9o799o0xqswzzlbsaho4.png" alt="configuration UI for a custom playground on iximiuz Labs" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;there are &lt;a href="https://github.com/iximiuz/labs/tree/main/playgrounds" rel="noopener noreferrer"&gt;a ton of examples&lt;/a&gt; that have been opensourced on what these look like for the current playgrounds&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;when trying it out, you can just run a playground and see what happens (if/when it fails, there's some helpful error messages to show you what went wrong)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The things that make this hard:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;conceptually, I didn't initially get how a container image and a VM could be the same (wasn't the whole Docker/VM thing like "the great divide" in how to run software?!?!)&lt;/li&gt;
&lt;li&gt;the examples can be used as base images, but how do I add my stuff on top of it?&lt;/li&gt;
&lt;li&gt;when you try out a new custom image as a rootFS mount...and it doesn't work...how do you debug it? (* - this actually has been improved since I started drafting this article...that's how awesome it is to be in the iximiuz discord and ask for a feature!)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Stuff I Did
&lt;/h2&gt;

&lt;p&gt;So I started out with one of the rootFS images, the one for &lt;a href="https://github.com/iximiuz/labs/blob/main/playgrounds/100.rootfs-ubuntu-24-04/Dockerfile" rel="noopener noreferrer"&gt;Ubuntu 24.04&lt;/a&gt;. Then I took some of the stuff from my custom playground and added it to the Dockerfile...basically the &lt;code&gt;apt install -y&lt;/code&gt; stuff, and also the &lt;code&gt;curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash&lt;/code&gt; stuff, cause I gots to have my kubescape scanning in this playground.&lt;/p&gt;

&lt;p&gt;That built okay, and so I pushed to GitHub's container registry (&lt;strong&gt;editor's note:&lt;/strong&gt; this is preferable to just pushing to dockerhub because the rate limiting is less of a pain).&lt;/p&gt;

&lt;p&gt;The VM immediately blew up though, and wouldn't even boot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Github packages, which container images are, are private by default, and you have to go through the whole rigmarole of switching their visibility to public before they can be pulled by anyone but you (this is why just pulling it locally isn't a good test!).&lt;/p&gt;

&lt;p&gt;Now having switched my image to public, it finally pulled and built fine, but nothing worked, because there was no kubectl installed, nor any of the stuff I actually needed to run k3s. I needed to use the same underlying image as the actual k3s playground I had based on &lt;a href="https://github.com/iximiuz/labs/blob/main/playgrounds/300.rootfs-ubuntu-k3s-server/Dockerfile" rel="noopener noreferrer"&gt;this Dockerfile&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; base the custom rootFS on the base image that actually has the stuff you need.&lt;/p&gt;

&lt;p&gt;There was another kerfuffle when I tried to copy in the kubernetes operator (y'know, the &lt;em&gt;WHOLE POINT&lt;/em&gt; of the playground) into the image, but it kept disappearing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; don't copy stuff into &lt;code&gt;/tmp&lt;/code&gt;, cause just like in AWS ECS (which you think I would have remembered by now cause it's happened to me about a bajillion times) it gets wiped when the system mounts it. It works in the playground configuration because all that stuff happens &lt;em&gt;after&lt;/em&gt; mounting.&lt;/p&gt;

&lt;p&gt;I was also trying out different things, pushing images to ghcr, but not seeing any differences in the lab VMs. Turns out there is some pretty aggressive image caching going on, and if you use the same tag, it might not get picked up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb04xyn9yoax5umzpvwhu.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb04xyn9yoax5umzpvwhu.jpg" alt="a bear looking at you, latest image tag" width="640" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; while prototyping, you might just need to cycle through different tags, and that's how I ended up with &lt;code&gt;v4&lt;/code&gt; as my (current) working tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  First Result
&lt;/h2&gt;

&lt;p&gt;So with all my efforts the startup time went from about 6 minutes to...about 5 minutes. It turns out that the package installation was already fairly quick, and the real slowdown was happening when I compiled all the kubernetes operator stuff (there's a LOT of stuff that goes on there!).&lt;/p&gt;

&lt;p&gt;So the next obvious move was to move all that compilation to build time, swap in a multi-stage build so that I could get rid of the entire golang toolchain in the final image, and just copy in the binary and CRD yaml.&lt;/p&gt;

&lt;h2&gt;
  
  
  Second Result
&lt;/h2&gt;

&lt;p&gt;Now with the binary all ready to go on boot, the VM startup went down to around 1-2 minutes. The remaining time is starting up all the pods in the k3s cluster and making sure things are running smoothly before we're all ready for the user to start doing thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; If you want to actually make sure things are ready to interact with, don't get rid of all your init scripts, cause those are what make the fancy "Warming up playground" screen, and then users are immediately dumped into the terminal and might &lt;em&gt;think&lt;/em&gt; things are ready, but they might not be.&lt;/p&gt;

&lt;p&gt;So I added a&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl wait --for=condition=Ready --timeout=60s pods --all -n test-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to my lab just to make sure that screen kept the user waiting while things get ready.&lt;/p&gt;

&lt;p&gt;I also was wondering if it would be necessary to create two different versions of the playground with different specs (obviously, more cpu/RAM would mean that it can start up and be ready even faster), but thankfully that's not even an issue, since you can just set it to the max of the premium tier, and when a free tier user starts it, it defaults to free tier limits (&lt;strong&gt;important&lt;/strong&gt; - test out your playground as a free user if you want to make sure this actually works)&lt;/p&gt;

&lt;h2&gt;
  
  
  The End Result
&lt;/h2&gt;

&lt;p&gt;So I've got a &lt;a href="https://labs.iximiuz.com/playgrounds/my-my-k3s-de88e13a" rel="noopener noreferrer"&gt;playground&lt;/a&gt; where you can practice running scans with kubescape to find security vulnerabilities in a running k3s cluster, then test out your fix (it's usually just updating the deployment), and the cluster will reset to another random vulnerability.&lt;/p&gt;

&lt;p&gt;The end result is a combo of Dockerfile, playground manifest, and some operator CRD stuff, which you can see &lt;a href="https://github.com/lpmi-13/owasp-k3s-cluster-iximiuz" rel="noopener noreferrer"&gt;here on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the reason I got confused initially between docker/VM is that in this case, we're only creating an image to use as a root volume, which is kind of what docker also does, but docker also layers on all of the fancy namespace (network/file/process/etc) stuff, which we don't use here, so a volume is a volume is a volume.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'll definitely be using this custom rootFS method going forward, and I encourage you to give it a try if you find yourself in need of some speed!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2pe1ld2kcmwzoig8788a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2pe1ld2kcmwzoig8788a.png" alt="Flash the sloth in a ferrari" width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>server</category>
      <category>iximiuz</category>
      <category>docker</category>
    </item>
    <item>
      <title>What to DevOps...??? An Iximiuz Labs Speedrun to the Rescue!</title>
      <dc:creator>Adam Leskis</dc:creator>
      <pubDate>Fri, 22 Aug 2025 12:54:06 +0000</pubDate>
      <link>https://forem.com/lpmi13/what-to-devops-an-iximiuz-labs-speedrun-to-the-rescue-384</link>
      <guid>https://forem.com/lpmi13/what-to-devops-an-iximiuz-labs-speedrun-to-the-rescue-384</guid>
      <description>&lt;p&gt;The video version of this blog is &lt;a href="https://www.youtube.com/watch?v=W2RTsBTbAsI" rel="noopener noreferrer"&gt;here&lt;/a&gt;. I used &lt;code&gt;box-01/02/03&lt;/code&gt; instead of &lt;code&gt;server-01/02/03&lt;/code&gt; in the video, but the rest is exactly the same.&lt;/p&gt;

&lt;p&gt;A lot of people who are interested in breaking into DevOps (or whatever title seems most appropriate for "learning about managing the things that actually run the software") often have the same question:&lt;/p&gt;

&lt;p&gt;"What should I do?"&lt;/p&gt;

&lt;p&gt;And they often get the same answer:&lt;/p&gt;

&lt;p&gt;"Just build something"&lt;/p&gt;

&lt;p&gt;But then they want to know:&lt;/p&gt;

&lt;p&gt;"What should I build?"&lt;/p&gt;

&lt;p&gt;And this is where it gets tricky...&lt;/p&gt;

&lt;p&gt;While building something that you actually want to use yourself is often one of the most motivating ways to learn while doing, for a lot of beginners, they're not even sure what the basic building blocks of "stuff that runs the software" are.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to build or how to build
&lt;/h2&gt;

&lt;p&gt;In very basic terms, those building blocks (especially as related to SaaS products) are easy enough to read about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a database&lt;/li&gt;
&lt;li&gt;a backend server&lt;/li&gt;
&lt;li&gt;a webserver sending a javascript frontend bundle to a browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lot of the web apps we use are basically web frontend wrappers around databases running CRUD, and that's okay!&lt;/p&gt;

&lt;p&gt;Though even knowing this, a lot of people can get stuck on the stage of "what kind of web frontend wrapper around a database should I build?"&lt;/p&gt;

&lt;p&gt;A lot of the tutorials walk learners through running everything locally, which is indeed a great way to get started on the software side of things.&lt;/p&gt;

&lt;p&gt;But what about the operating of that software on VMs somewhere? And yes, even containers run inside VMs, although a lot of platforms like ECS Fargate or Google Cloud Run abstract that away...&lt;/p&gt;

&lt;h2&gt;
  
  
  Forget the apps, lets deal with some VMs!
&lt;/h2&gt;

&lt;p&gt;It used to be that if you wanted to deploy an application on a VM and get experience doing that, you would have needed to sign up to a cloud provider like AWS, Digital Ocean, or Hetzner. This took time and oftentimes involved a financial commitment (even just entering your credit card details, which kept some people from going this route).&lt;/p&gt;

&lt;p&gt;I've &lt;a href="https://dev.to/lpmi13/turning-markdown-into-learning-publishing-a-challenge-on-labsiximiuzcom-3705"&gt;written&lt;/a&gt; &lt;a href="https://dev.to/lpmi13/pre-shaved-y-axis-the-case-for-preconfigured-rstudio-workshop-environments-1k9e"&gt;before&lt;/a&gt; about &lt;a href="https://labs.iximiuz.com" rel="noopener noreferrer"&gt;Iximiuz Labs&lt;/a&gt;, which is the perfect place to play around with VMs for free&lt;code&gt;*&lt;/code&gt;!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;*&lt;/code&gt; - requires a GitHub user&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the rest of this post, I'll walk through deploying and configuring a "3-Tier Web App" ™️ in one of the playgrounds, completely from scratch.&lt;/p&gt;

&lt;p&gt;...and here's what it will look like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffz1hq8cbp5ra0clyakt8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffz1hq8cbp5ra0clyakt8.png" alt=" " width="800" height="918"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We will&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up a postgres database&lt;/li&gt;
&lt;li&gt;Set up a backend application to interact with the database&lt;/li&gt;
&lt;li&gt;Set up a frontend application served via nginx to load in the browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We won't have to deal with the tougher aspects of DevOps, like DNS, firewalls, subnets, monitoring, or CI/CD pipelines...we want to keep this as simple as possible (though maybe we'll look at some of those in later blog posts).&lt;/p&gt;

&lt;p&gt;But don't worry...just in case you're worried it'll be too easy, we &lt;em&gt;will&lt;/em&gt; need to care about IP addresses and ports, so there's still a little bit of networking going on here.&lt;/p&gt;

&lt;p&gt;I cheated a little bit...the code for both the backend and frontend is already written (it's &lt;em&gt;VERY&lt;/em&gt; simple), but that makes it easier for other people, especially those who have no idea what to build, to follow along themselves!&lt;/p&gt;

&lt;h2&gt;
  
  
  My Kingdom for a Database!
&lt;/h2&gt;

&lt;p&gt;Let's fire up the &lt;a href="https://labs.iximiuz.com/playgrounds/flexbox" rel="noopener noreferrer"&gt;Flexbox playground&lt;/a&gt; and start installing stuff!&lt;/p&gt;

&lt;p&gt;You'll just need to add another machine and make sure the Rootfs (the root file system) for each of them is Ubuntu 24.04.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxx5f3kx2q2bixz9zowlv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxx5f3kx2q2bixz9zowlv.png" alt="Flexbox playground configuration screen" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ubuntu 22.04 would also still work, but we might as well go for the latest OS version.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've called my machines &lt;code&gt;server-01&lt;/code&gt;, &lt;code&gt;server-02&lt;/code&gt;, and &lt;code&gt;server-03&lt;/code&gt;, but you can call them whatever you want. Once you hit the "Start Playground" button, the playground will boot up, and you should see the following screen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fym8w4y6iphylz9c2jl30.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fym8w4y6iphylz9c2jl30.png" alt="Flexbox playground startup screen" width="800" height="627"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's go ahead and install the database on server-03:&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;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; postgresql postgresql-client-16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and now let's start the database&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start postgresql
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres psql &lt;span class="nt"&gt;-x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;This is a neat trick to get into the database without needing the password, since it automatically lets a user named "postgres" in if you're on the same server.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now you're in the database and can start configuring the application user&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;CREATE DATABASE drizzle&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="se"&gt;\c&lt;/span&gt; drizzle&lt;span class="p"&gt;;&lt;/span&gt;

CREATE USER laborant WITH PASSWORD &lt;span class="s1"&gt;'laborant'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(in the above, because these are isolated ephemeral environments, it doesn't really matter what you set as your password, so you might as well make it easy to remember...don't do this in production, obviously)&lt;/p&gt;

&lt;p&gt;and now we can grant the privileges for the new user to interact with the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GRANT ALL PRIVILEGES ON DATABASE drizzle TO laborant&lt;span class="p"&gt;;&lt;/span&gt;

GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO laborant&lt;span class="p"&gt;;&lt;/span&gt;

GRANT USAGE, CREATE ON SCHEMA public TO laborant&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;now the user is ready to be used by the backend service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backend user service...get that data!
&lt;/h2&gt;

&lt;p&gt;Now that we've got a database, we can use that for our backend application. On &lt;code&gt;server-02&lt;/code&gt;, let's clone the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/lpmi-13/speed-run-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because this backend service is written in typescript, we have to install &lt;code&gt;node&lt;/code&gt; and &lt;code&gt;npm&lt;/code&gt; to build and run it. I like &lt;a href="https://github.com/Schniz/fnm" rel="noopener noreferrer"&gt;fnm&lt;/a&gt; for this, but you could use &lt;a href="https://github.com/nvm-sh/nvm" rel="noopener noreferrer"&gt;nvm&lt;/a&gt; if you want to.&lt;/p&gt;

&lt;p&gt;Once that's installed and ready (don't forget to run &lt;code&gt;source ~/.bashrc&lt;/code&gt;, since you can't restart the shell inside the VM), go ahead and run the following:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Inspecting the code in &lt;code&gt;src/index.ts&lt;/code&gt;, you can see that it looks for an environment variable called &lt;code&gt;DATABASE_URL&lt;/code&gt;, and if that's not set, it tries a default connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&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;express&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;drizzle&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;drizzle-orm/node-postgres&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;migrate&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;drizzle-orm/node-postgres/migrator&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;seed&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;drizzle-seed&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;Pool&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;pg&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="nx"&gt;dotenv&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;dotenv&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="nx"&gt;path&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;path&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;usersTable&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;./db/schema&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                                        

&lt;span class="nx"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                                                           
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connectionString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;                                                         
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;                                                  
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres://postgres:mypassword@localhost:5432/drizzle&lt;/span&gt;&lt;span class="dl"&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;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;connectionString&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have a user and password (the one we created in the last section in our database), so let's create a &lt;code&gt;.env&lt;/code&gt; file and set up our &lt;code&gt;DATABASE_URL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;...there's a problem though...what's the hostname going to be? You can't use &lt;code&gt;localhost&lt;/code&gt;, because the database isn't running on the same VM.&lt;/p&gt;

&lt;p&gt;That's easy enough to fix, let's jump back on &lt;code&gt;server-03&lt;/code&gt; and find out what the IP address is (hint: they will all be &lt;code&gt;172.X.X.X&lt;/code&gt;, because that's the virtual subnet they get run in).&lt;/p&gt;

&lt;p&gt;You can run either &lt;code&gt;ip addr&lt;/code&gt; or good ole &lt;code&gt;ifconfig&lt;/code&gt; and find the address for the &lt;code&gt;eth0&lt;/code&gt; interface that starts with &lt;code&gt;172...&lt;/code&gt;, that's the IP address of &lt;code&gt;server-03&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now with that information, you can fill in the connection string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'DATABASE_URL=postgres://laborant:laborant@172.16.0.4:5432/drizzle'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;We're specifying the &lt;code&gt;drizzle&lt;/code&gt; database, since that's the one the application is configured to try and connect to, but if you're feeling adventurous, go ahead and try with your own custom-defined database.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once that's all configured, let's build and run the application!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run build
node dist/index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OH NO! Our first big problem!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;laborant@server-02:speed-run-backend&lt;span class="nv"&gt;$ &lt;/span&gt;node dist/index.js 
Connecting to PostgreSQL database...
Running migrations &lt;span class="k"&gt;if &lt;/span&gt;needed...
Failed to start server: Error: connect ECONNREFUSED 172.16.0.4:5432
    at /home/laborant/speed-run-backend/node_modules/pg-pool/index.js:45:11
    at process.processTicksAndRejections &lt;span class="o"&gt;(&lt;/span&gt;node:internal/process/task_queues:105:5&lt;span class="o"&gt;)&lt;/span&gt;
    at async PgDialect.migrate &lt;span class="o"&gt;(&lt;/span&gt;/home/laborant/speed-run-backend/node_modules/drizzle-orm/pg-core/dialect.cjs:56:5&lt;span class="o"&gt;)&lt;/span&gt;
    at async migrate &lt;span class="o"&gt;(&lt;/span&gt;/home/laborant/speed-run-backend/node_modules/drizzle-orm/node-postgres/migrator.cjs:27:3&lt;span class="o"&gt;)&lt;/span&gt;
    at async startServer &lt;span class="o"&gt;(&lt;/span&gt;/home/laborant/speed-run-backend/dist/index.js:38:9&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  errno: &lt;span class="nt"&gt;-111&lt;/span&gt;,
  code: &lt;span class="s1"&gt;'ECONNREFUSED'&lt;/span&gt;,
  syscall: &lt;span class="s1"&gt;'connect'&lt;/span&gt;,
  address: &lt;span class="s1"&gt;'172.16.0.4'&lt;/span&gt;,
  port: 5432
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can't connect to the database. Let's debug that 🕵️...&lt;/p&gt;

&lt;p&gt;With a quick netcat command, we can see that the port is not accepting connections&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;laborant@server-02:speed-run-backend&lt;span class="nv"&gt;$ &lt;/span&gt;nc &lt;span class="nt"&gt;-vz&lt;/span&gt; 172.16.0.4 5432
nc: connect to 172.16.0.4 port 5432 &lt;span class="o"&gt;(&lt;/span&gt;tcp&lt;span class="o"&gt;)&lt;/span&gt; failed: Connection refused
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the reason is simple enough. We didn't update the postgres config to accept incoming connections from outside its own VM 💡.&lt;/p&gt;

&lt;p&gt;Let's update that now.&lt;/p&gt;

&lt;p&gt;Back on &lt;code&gt;server-03&lt;/code&gt;, we want to edit &lt;code&gt;/etc/postgresql/16/main/pg_hba.conf&lt;/code&gt; to change the following line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;host    all             all             127.0.0.1/32            scram-sha-256 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;host    all             all             0.0.0.0/0            scram-sha-256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That basically means "instead of just listening for connections from inside the VM, listen to connections trying from outside the VM". See? Easy networking fix!&lt;/p&gt;

&lt;p&gt;...but we're not done yet. We still need to tweak one more bit of config. Let's also edit &lt;code&gt;/etc/postgresql/16/main/postgresql.conf&lt;/code&gt; to change the following line:&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;#listen_addresses = 'localhost'   # what IP address(es) to listen on;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;listen_addresses = '*'   # what IP address(es) to listen on;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;make sure to remove the initial &lt;code&gt;#&lt;/code&gt;, which turns the above line into a comment, as well as update the &lt;code&gt;listen_addresses&lt;/code&gt; 💡&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The above is a similar update to tell the database that instead of just listening for connections from inside the VM on &lt;code&gt;localhost&lt;/code&gt;, we want to listen for connections from outside.&lt;/p&gt;

&lt;p&gt;To apply these configuration changes, we need a full restart of the database, so we can run:&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 restart postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now let's try our netcat test from &lt;code&gt;server-02&lt;/code&gt; again, and we should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;laborant@server-02:speed-run-backend&lt;span class="nv"&gt;$ &lt;/span&gt;nc &lt;span class="nt"&gt;-vz&lt;/span&gt; 172.16.0.4 5432
Connection to 172.16.0.4 5432 port &lt;span class="o"&gt;[&lt;/span&gt;tcp/postgresql] succeeded!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great, now we're ready to try to run the application again.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;we &lt;em&gt;could&lt;/em&gt; run this as a background process, but we're not going to keep this playground around very long, so we're not doing anything fancy like setting up a systemd service or anything.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;laborant@server-02:speed-run-backend&lt;span class="nv"&gt;$ &lt;/span&gt;node dist/index.js
Connecting to PostgreSQL database...
Running migrations &lt;span class="k"&gt;if &lt;/span&gt;needed...
Migrations completed successfully
Database seeded with 10 &lt;span class="nb"&gt;users
&lt;/span&gt;Server running on port 4000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🎉 our server is running and connected to the database!&lt;/p&gt;

&lt;p&gt;If you'd like to confirm it's running fine, let's send a request to its endpoint...FROM ANOTHER VM!!!! (so exciting!)&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend user service...show that data!
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnv9a66m9tpqirpogh0lh.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnv9a66m9tpqirpogh0lh.jpg" alt=" " width="680" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just like we planned in the previous step, let's head over to &lt;code&gt;server-01&lt;/code&gt; and connect to the backend server and query the list of users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl 172.16.0.3:4000/users | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;we're just using &lt;code&gt;jq&lt;/code&gt; because it comes pre-installed on these VMs, and it makes the output easier to read.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Awesome, we can see the users data coming through from the backend server&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;laborant@server-01:~&lt;span class="nv"&gt;$ &lt;/span&gt;curl 172.16.0.3:4000/users | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   815  100   815    0     0  85411      0 &lt;span class="nt"&gt;--&lt;/span&gt;:--:-- &lt;span class="nt"&gt;--&lt;/span&gt;:--:-- &lt;span class="nt"&gt;--&lt;/span&gt;:--:-- 90555
&lt;span class="o"&gt;[&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: 1,
    &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"Mouhamed"&lt;/span&gt;,
    &lt;span class="s2"&gt;"age"&lt;/span&gt;: &lt;span class="nt"&gt;-118655047&lt;/span&gt;,
    &lt;span class="s2"&gt;"email"&lt;/span&gt;: &lt;span class="s2"&gt;"motivational_shakeia@outlook.com"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: 2,
    &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"Gisele"&lt;/span&gt;,
    &lt;span class="s2"&gt;"age"&lt;/span&gt;: 1415217029,
    &lt;span class="s2"&gt;"email"&lt;/span&gt;: &lt;span class="s2"&gt;"bronchial_myrtice@orange.fr"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: 3,
    &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"Garvin"&lt;/span&gt;,
    &lt;span class="s2"&gt;"age"&lt;/span&gt;: &lt;span class="nt"&gt;-1302366560&lt;/span&gt;,
    &lt;span class="s2"&gt;"email"&lt;/span&gt;: &lt;span class="s2"&gt;"logical_mariacristina@live.com"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: 4,
    &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"Klay"&lt;/span&gt;,
    &lt;span class="s2"&gt;"age"&lt;/span&gt;: 1243263009,
    &lt;span class="s2"&gt;"email"&lt;/span&gt;: &lt;span class="s2"&gt;"blameless_annalyn@web.de"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,

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

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ignore the ludicrous ages, this post isn't gonna cover a data-cleaning pipeline...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So we have a working backend server getting data from a database...but how do we get that to load in the browser for a user?&lt;/p&gt;

&lt;p&gt;You might think that you could just clone the frontend code and run the dev server with &lt;code&gt;npm start&lt;/code&gt;, but you're gonna have all kinds of problems with binding to &lt;code&gt;0.0.0.0/0&lt;/code&gt; (remember that means "listens for outside connections") as well as host headers and general frontend-y types of issues.&lt;/p&gt;

&lt;p&gt;...but don't worry! We're gonna use Nginx to do what Nginx does (serve bundled frontend code to users and redirect requests to the backend server).&lt;/p&gt;

&lt;p&gt;So let's first clone the frontend code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/lpmi-13/speed-run-frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can do the same trick of installing fnm/nvm then building the frontend assets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;...previous step of installing fnm or nvm...
npm i
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The assets (here, we mean &lt;code&gt;html/js/css&lt;/code&gt;) are now located in a &lt;code&gt;build/&lt;/code&gt; directory, and we just need to put those in a place where Nginx knows how to find them.&lt;/p&gt;

&lt;p&gt;Let's also install Nginx, since we haven't done that yet:&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;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;service nginx start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll know that Nginx is up and running since it listens on localhost port 80 by default, so you should be able to run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;laborant@server-01:speed-run-frontend&lt;span class="nv"&gt;$ &lt;/span&gt;curl localhost
&amp;lt;&lt;span class="o"&gt;!&lt;/span&gt;DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;&lt;span class="nb"&gt;head&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&amp;lt;title&amp;gt;Welcome to nginx!&amp;lt;/title&amp;gt;
&amp;lt;style&amp;gt;
html &lt;span class="o"&gt;{&lt;/span&gt; color-scheme: light dark&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
body &lt;span class="o"&gt;{&lt;/span&gt; width: 35em&lt;span class="p"&gt;;&lt;/span&gt; margin: 0 auto&lt;span class="p"&gt;;&lt;/span&gt;
font-family: Tahoma, Verdana, Arial, sans-serif&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;h1&amp;gt;Welcome to nginx!&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;For online documentation and support please refer to
&amp;lt;a &lt;span class="nv"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://nginx.org/"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;nginx.org&amp;lt;/a&amp;gt;.&amp;lt;br/&amp;gt;
Commercial support is available at
&amp;lt;a &lt;span class="nv"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://nginx.com/"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;nginx.com&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;em&amp;gt;Thank you &lt;span class="k"&gt;for &lt;/span&gt;using nginx.&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And see the delightful Nginx html coming back.&lt;/p&gt;

&lt;p&gt;Now we need to copy over the files in the &lt;code&gt;build/&lt;/code&gt; directory to an Nginx location.&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 rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/www/html/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; build/&lt;span class="k"&gt;*&lt;/span&gt; /var/www/html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll know it's copied correctly because you can now re-run the &lt;code&gt;curl&lt;/code&gt; command and you'll see the react html coming back&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;laborant@server-01:speed-run-frontend&lt;span class="nv"&gt;$ &lt;/span&gt;curl localhost
&amp;lt;&lt;span class="o"&gt;!&lt;/span&gt;doctype html&amp;gt;&amp;lt;html &lt;span class="nv"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;lt;&lt;span class="nb"&gt;head&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;lt;meta &lt;span class="nv"&gt;charset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;/&amp;gt;&amp;lt;&lt;span class="nb"&gt;link &lt;/span&gt;&lt;span class="nv"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"icon"&lt;/span&gt; &lt;span class="nv"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/favicon.ico"&lt;/span&gt;/&amp;gt;&amp;lt;meta &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"viewport"&lt;/span&gt; &lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"width=device-width,initial-scale=1"&lt;/span&gt;/&amp;gt;&amp;lt;meta &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"theme-color"&lt;/span&gt; &lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"#000000"&lt;/span&gt;/&amp;gt;&amp;lt;meta &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"description"&lt;/span&gt; &lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Web site created using create-react-app"&lt;/span&gt;/&amp;gt;&amp;lt;&lt;span class="nb"&gt;link &lt;/span&gt;&lt;span class="nv"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"apple-touch-icon"&lt;/span&gt; &lt;span class="nv"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/logo192.png"&lt;/span&gt;/&amp;gt;&amp;lt;&lt;span class="nb"&gt;link &lt;/span&gt;&lt;span class="nv"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"manifest"&lt;/span&gt; &lt;span class="nv"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/manifest.json"&lt;/span&gt;/&amp;gt;&amp;lt;title&amp;gt;React App&amp;lt;/title&amp;gt;&amp;lt;script &lt;span class="nv"&gt;defer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"defer"&lt;/span&gt; &lt;span class="nv"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/static/js/main.d1e5e332.js"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;lt;/script&amp;gt;&amp;lt;&lt;span class="nb"&gt;link &lt;/span&gt;&lt;span class="nv"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/static/css/main.e6c13ad2.css"&lt;/span&gt; &lt;span class="nv"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"stylesheet"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;noscript&amp;gt;You need to &lt;span class="nb"&gt;enable &lt;/span&gt;JavaScript to run this app.&amp;lt;/noscript&amp;gt;&amp;lt;div &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"root"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;lt;/div&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to update the Nginx config a bit so that it sends the requests made by the browser to the correct backend (the request will come into Nginx first running on &lt;code&gt;server-01&lt;/code&gt;, which will pass it on to the backend running on &lt;code&gt;server-02&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;Now we can cheat again (since I have a working nginx config in the frontend code repo we cloned earlier), and copy it into the right place:&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 cp &lt;/span&gt;nginx/nginx.conf /etc/nginx/nginx.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can take a look at it yourself if you want, it's very basic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;user www-data&lt;span class="p"&gt;;&lt;/span&gt;
worker_processes auto&lt;span class="p"&gt;;&lt;/span&gt;
pid /run/nginx.pid&lt;span class="p"&gt;;&lt;/span&gt;
error_log /var/log/nginx/error.log&lt;span class="p"&gt;;&lt;/span&gt;
include /etc/nginx/modules-enabled/&lt;span class="k"&gt;*&lt;/span&gt;.conf&lt;span class="p"&gt;;&lt;/span&gt;

events &lt;span class="o"&gt;{&lt;/span&gt;
    worker_connections 1024&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

http &lt;span class="o"&gt;{&lt;/span&gt;

    server &lt;span class="o"&gt;{&lt;/span&gt;
        listen 80&lt;span class="p"&gt;;&lt;/span&gt; 

        root /var/www/html&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c"&gt;# Serves the frontend html/js on page load&lt;/span&gt;
        location / &lt;span class="o"&gt;{&lt;/span&gt; 
            try_files &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;/ /index.html&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="c"&gt;# proxy the requests for the backend to the other server&lt;/span&gt;
        &lt;span class="c"&gt;# the domain for server-02 is resolvable by the hostname&lt;/span&gt;
        location /users &lt;span class="o"&gt;{&lt;/span&gt;
            proxy_pass http://server-02:4000&lt;span class="p"&gt;;&lt;/span&gt;
            proxy_http_version 1.1&lt;span class="p"&gt;;&lt;/span&gt;
            proxy_set_header Host &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            proxy_set_header X-Real-IP &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            proxy_set_header X-Forwarded-For &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            proxy_set_header X-Forwarded-Proto &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It basically says "listen for connections on port 80, and send the html/javascript for the root domain, and also forward any requests with &lt;code&gt;/users&lt;/code&gt; at the end to the other server on port 4000."&lt;/p&gt;

&lt;p&gt;Once that's all copied, go ahead and reload the config with &lt;code&gt;sudo service nginx reload&lt;/code&gt; (I forgot this step in the demo and had to quickly debug it).&lt;/p&gt;

&lt;p&gt;So we've got the &lt;code&gt;index.html&lt;/code&gt; in the correct place, we just need to expose it to the world.&lt;/p&gt;

&lt;p&gt;This is one of the best parts of Iximiuz Labs...you can expose a port directly and get a nice public URL.&lt;/p&gt;

&lt;p&gt;In the upper right of the UI, there's a little box icon with an arrow coming out of it...click that, and type in &lt;code&gt;80&lt;/code&gt; for the port (the default nginx port).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fir7lx3vio974kget7iuf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fir7lx3vio974kget7iuf.png" alt=" " width="338" height="140"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuu3vhn55uq6yktwipl5g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuu3vhn55uq6yktwipl5g.png" alt=" " width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and BOOM! We've got users showing up in the browser&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fauhfoh46q8atuzjq8zvi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fauhfoh46q8atuzjq8zvi.png" alt=" " width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Actually deploying and connecting real things (even if they're temporary and disappear when you close the playground) is the heart of DevOps.&lt;/p&gt;

&lt;p&gt;Ideally, there would also be a bit of automation in there, but in this speedrun, the whole idea was to do something super super simple and connect it across independent VMs.&lt;/p&gt;

&lt;p&gt;It would be a great addition to add in some database replication or caching or load balancing or...&lt;/p&gt;

&lt;p&gt;...but that's probably gonna be a different speed run.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Remote Homelab Admin with Tailscale</title>
      <dc:creator>Adam Leskis</dc:creator>
      <pubDate>Thu, 26 Jun 2025 21:02:45 +0000</pubDate>
      <link>https://forem.com/lpmi13/remote-homelab-admin-with-tailscale-1jp2</link>
      <guid>https://forem.com/lpmi13/remote-homelab-admin-with-tailscale-1jp2</guid>
      <description>&lt;p&gt;For a video version of me walking through this is you can &lt;a href="https://youtu.be/VlHK9XdbQtY" rel="noopener noreferrer"&gt;watch here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvwrvjuxio1f90vnq3qm8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvwrvjuxio1f90vnq3qm8.png" alt="Network Overview" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tailscale.com/" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt; is a super cool networking tool, and &lt;a href="https://labs.iximiuz.com/playgrounds" rel="noopener noreferrer"&gt;Iximiuz Labs Playgrounds&lt;/a&gt; are a great place to run a Remote Homelab, so I thought I'd smash em together and make a super cool tech sandwich!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Author's note: This particular playground will only run on the Iximiuz Labs premium tier, but you can sign up for a tailscale account and play with your own VMs for free.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://labs.iximiuz.com/playgrounds/my-k3s-89c9799c" rel="noopener noreferrer"&gt;Playground direct link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/lpmi-13/iximiuz-tailscale-homelab-admin/" rel="noopener noreferrer"&gt;Source config&lt;/a&gt; for the playground.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;To make things at least a little authentic, I replicated a very basic homelab running two main services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;home assistant&lt;/li&gt;
&lt;li&gt;jellyfin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and two monitoring components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grafana&lt;/li&gt;
&lt;li&gt;prometheus&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;along with tailscale on each of the nodes to allow me to connect securely via my phone (which I do via &lt;a href="https://termux.dev/en/" rel="noopener noreferrer"&gt;termux&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;all inside of a multi-node k3s cluster. The graphic below shows these components and how they're deployed across the various 4 VMs&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnqlv8kfq3oapz3rkuced.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnqlv8kfq3oapz3rkuced.png" alt="Homelab architecture diagram" width="800" height="741"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The cool networking part
&lt;/h2&gt;

&lt;p&gt;The TL;DR of tailscale is that once you connect running nodes to your tailnet, they can find each other by exchanging public keys with the centralized tailscale key servers, and using UDP holepunching to find out their public IP addresses.&lt;/p&gt;

&lt;p&gt;The traffic heading out of a node first goes to the virtual network interface, where the wireguard process is running and signs the messages with the public keys of the other nodes they send traffic to. Then it gets forwarded onto the physical network interface with the IP address of the destination node (found via UDP holepunching earlier).&lt;/p&gt;

&lt;p&gt;It's all very slick and I've found it to be a great UX. If you want to read more in depth about how it all works, &lt;a href="https://tailscale.com/blog/how-tailscale-works" rel="noopener noreferrer"&gt;this is a good article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And so if your phone has also joined the tailnet, it can communicate directly with the VMs inside the homelab (or in this case, remotelab) with end-to-end encrypted tunnels straight through to the private subnets behind NAT!&lt;/p&gt;

&lt;p&gt;Your servers are never exposed to the public internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;I wanted to demo an authentic scenario to show off the capabilities, and so I did a bunch of work up front so I could crash one of the services (jellyfin) and then bring it back to life, all connecting via my phone over the secure tailnet.&lt;/p&gt;

&lt;p&gt;...okay, so I cheat a little bit and crash the server from the Iximiuz Labs web interface, but you probably wouldn't run that command from your phone anyway, so I'm gonna claim it's still totally authentic ™️.&lt;/p&gt;

&lt;p&gt;First, we set up our slideshow of http status cats via jellyfin...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flvsn0y8bztj6d6p0i582.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flvsn0y8bztj6d6p0i582.jpg" alt="Jellyfin slideshow with http status cats" width="800" height="1783"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then we start filling up the server with fake logs&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk47avgwylfhkt7ytrn4q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk47avgwylfhkt7ytrn4q.png" alt="Filling up the node with fake logs" width="642" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;...and then all of a sudden, the slideshow turns to sadness!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xv8c2k1jovjf9qhpfnz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xv8c2k1jovjf9qhpfnz.jpg" alt="Jellyfin slideshow broken" width="800" height="1783"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OH NO! The cats have crashed and we're far away from home...time to debug remotely!&lt;/p&gt;

&lt;p&gt;Luckily, we have a (very basic) monitoring stack set up with Prometheus and Grafana, so we can see if that tells us anything interesting...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9aqo2gqcpoub6343ckh.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9aqo2gqcpoub6343ckh.jpg" alt="Grafana dashboard showing node-02 down" width="800" height="1783"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ah ha! It looks like one of the nodes is down, and we can also see that the file usage spiked to 100%...that's probably what crashed the server :lightbulb:&lt;/p&gt;

&lt;p&gt;Let's dive into the node and see if we can work out where all the logs are filling up the server (we've already stopped the log generation script via the Iximiuz Labs web UI, so we don't need to worry about that anymore).&lt;/p&gt;

&lt;p&gt;Lets first connect to the node with termux. Grab either the magic DNS or just the private IP of the node and use that when you run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh phoneuser@IP_ADDRESS_FROM_TAILSCALE_UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note, this requires having a valid &lt;code&gt;authorized_keys&lt;/code&gt; file in your ssh config for the &lt;code&gt;phoneuser&lt;/code&gt; user on the node, containing the public key from a keypair generated on your phone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then we can run this command to see where all the disk bloat is&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo du -h --max-depth=1 /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks like it's the &lt;code&gt;/var&lt;/code&gt; directory. Looking further down the directory tree, we can see the culprit is in &lt;code&gt;/var/log/fake_logs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We found all the (fake) logs...let's clean them up. Now run &lt;code&gt;sudo rm -rf /var/log/fake_logs/*&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now that there's free space on the server, let's restart the k3s-agent (the thing that crashed when we ran out of disk space).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl restart k3s-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and we probably also want to clean up all the left over crud in the pod namespace...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;k delete po -l app=jellyfin
k delete po -l app=node-exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;since these are stateless services that mount in their config, they're completely fine to be deleted and recreated&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And now we can see that the node-exporter is back up and the pod has returned to normal functioning. Our jellyfin server is back up, and we can continue admiring how cats' facial expressions never change, and that's why they're so funny.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffvsxf0lef0vdn66m0xwk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffvsxf0lef0vdn66m0xwk.jpg" alt="Jellyfin cats slideshow is back" width="800" height="1783"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Both Tailscale and Iximiuz Labs are very fun and approachable tools to help us learn about different homelab components, and they let us do it (basically) for free! What an internet!&lt;/p&gt;

</description>
      <category>remotelab</category>
      <category>tailscale</category>
      <category>networking</category>
      <category>homelab</category>
    </item>
    <item>
      <title>Pre-shaved Y-Axis: the case for preconfigured Rstudio workshop environments</title>
      <dc:creator>Adam Leskis</dc:creator>
      <pubDate>Sun, 16 Feb 2025 23:08:12 +0000</pubDate>
      <link>https://forem.com/lpmi13/pre-shaved-y-axis-the-case-for-preconfigured-rstudio-workshop-environments-1k9e</link>
      <guid>https://forem.com/lpmi13/pre-shaved-y-axis-the-case-for-preconfigured-rstudio-workshop-environments-1k9e</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0003c8pz612dja7ilr3q.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0003c8pz612dja7ilr3q.jpg" alt="r in the cloud" width="500" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You might think that not being an avid user of either Rstudio or the R programming language in general makes me unqualified to write about them, and you would be 100% correct; but it's my blog, so I'm gonna do it anyway!&lt;/p&gt;

&lt;p&gt;Now that we're past my extremely obscure joke in the title related to data viz in R and yak shaving, I will tell you about how I had a lot of fun creating &lt;a href="https://labs.iximiuz.com/playgrounds/custom-e21deb0c" rel="noopener noreferrer"&gt;a prepackaged playground&lt;/a&gt; on &lt;a href="https://labs.iximiuz.com" rel="noopener noreferrer"&gt;labs.iximiuz.com&lt;/a&gt; to follow along with a &lt;a href="https://www.cambridge.org/core/journals/journal-of-linguistic-geography/article/visualizing-map-data-for-linguistics-using-ggplot2-a-tutorial-with-examples-from-dialectology-and-typology/369F9643F85781AAAC0096D6BD146215" rel="noopener noreferrer"&gt;recent paper&lt;/a&gt; presenting a tutorial for visualizing linguistics data using the R package ggplot2.&lt;/p&gt;

&lt;p&gt;I'd like to briefly discuss &lt;strong&gt;why&lt;/strong&gt; I did it, and also &lt;strong&gt;how&lt;/strong&gt;, in case you are interested in doing something similar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did I do this?
&lt;/h2&gt;

&lt;p&gt;While I may not be accustomed to using R for...well...anything, really...what I do have loads of experience in is listening to friends in the academic realm talk about how difficult it is to work with.&lt;/p&gt;

&lt;p&gt;The dependency management is very difficult, it's nontrivial to upgrade Rstudio to a version that works with the packages that are needed for a given task, and even just setting up working environments on students' personal devices to take part in class work can be a multi-day job.&lt;/p&gt;

&lt;p&gt;Imagine it didn't have to be this way...&lt;/p&gt;

&lt;p&gt;Imagine that students in a workshop could open up any modern web browser, load the URL, and click one button to have a 100% deterministic working version of Rstudio loaded with all the packages needed for the activity...&lt;/p&gt;

&lt;p&gt;Well, imagine no more (and you already knew this was possible if you read &lt;a href="https://micromaterialsblog.wordpress.com/2025/01/13/turning-markdown-into-learning-publishing-a-challenge-on-labs-iximiuz-com/" rel="noopener noreferrer"&gt;my last post&lt;/a&gt;), this is possible right now!&lt;/p&gt;

&lt;p&gt;The very cool platform on &lt;a href="https://labs.iximiuz.com" rel="noopener noreferrer"&gt;labs.iximiuz.com&lt;/a&gt; has playgrounds that can be customized for this exact usecase and shared publicly. I know because I made one. Given the ability to run basically whatever you want in the ephemeral environments there, this is the perfect platform to create on-demand environments pre-configured with almost any software your heart desires.&lt;/p&gt;

&lt;p&gt;To borrow a term from the testing and security domains, we can "shift left" and get the user to actually doing the thing they wanna do faster and more effectively. This is devops for education, and I'm here for it!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvrr0ottlkgwuhihrx2wi.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvrr0ottlkgwuhihrx2wi.gif" alt="devops in the morning" width="260" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How did I do this?
&lt;/h2&gt;

&lt;p&gt;Because the Iximiuz platform is basically a free-form sandbox, the most difficult part was figuring out how to run the appropriate code in there. I knew it could be done, I just needed to connect all the fun lego blocks.&lt;/p&gt;

&lt;p&gt;I started with the &lt;a href="https://hub.docker.com/r/rocker/tidyverse" rel="noopener noreferrer"&gt;rocker/tidyverse&lt;/a&gt; container image, pinned to base R version 4.4.1 (this was originally 4.4.2 since that's the latest, but I updated it after noticing the author specifying in GitHub that the base version of R they used was 4.4.1).&lt;/p&gt;

&lt;p&gt;After firing that up locally with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -it --rm -p 127.0.0.1:8787:8787 -e PASSWORD=rstudio rocker/tidyverse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had a running version of Rstudio at &lt;code&gt;localhost:8787&lt;/code&gt;. Then it was a matter of just running the commands from the tutorial, seeing what broke and then fixing it by installing stuff via an iteratively growing Dockerfile.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyo3gqrhmk3o1x3eysrx5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyo3gqrhmk3o1x3eysrx5.gif" alt="rstudio in the container" width="299" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once that was all set, all I had to do was build the container locally and push to a public GitHub container registry, then add one line to customize a &lt;a href="https://labs.iximiuz.com/playgrounds/docker" rel="noopener noreferrer"&gt;linux playground with Docker pre-installed&lt;/a&gt; to run my container.&lt;/p&gt;

&lt;p&gt;But that's not all!&lt;/p&gt;

&lt;p&gt;There's a super slick feature of the platform where you can basically create a new tab that accesses a port from the running container, and this can be configured to be available on startup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3g2qyuqh7ml2o7bgjko.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3g2qyuqh7ml2o7bgjko.png" alt="Image description" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the entire flow for the user is&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Click "Start" for the playground&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wait for playground to be ready&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click "Rstudio" tab&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Log in with the supplied username/password&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start running commands from the tutorial&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk8ph53nmn9kldjpnri3z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk8ph53nmn9kldjpnri3z.png" alt="Image description" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're interested in seeing the complete Dockerfile, that's &lt;a href="https://github.com/danaroemling/mapping-for-linguists/blob/main/Dockerfile" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is a completely disposable learning environment where the users can have a running start without worrying about any annoying configuration challenges, and when they're done, they can just close the browser tab. They don't even need to know that Docker is a thing!&lt;/p&gt;

&lt;p&gt;The yak is fully pre-shaved! The future is wow!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevnha0bk3kq4u5flimh8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevnha0bk3kq4u5flimh8.gif" alt="wow" width="480" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>workshop</category>
      <category>docker</category>
      <category>r</category>
    </item>
    <item>
      <title>Turning Markdown into Learning: publishing a challenge on labs.iximiuz.com</title>
      <dc:creator>Adam Leskis</dc:creator>
      <pubDate>Tue, 14 Jan 2025 20:26:46 +0000</pubDate>
      <link>https://forem.com/lpmi13/turning-markdown-into-learning-publishing-a-challenge-on-labsiximiuzcom-3705</link>
      <guid>https://forem.com/lpmi13/turning-markdown-into-learning-publishing-a-challenge-on-labsiximiuzcom-3705</guid>
      <description>&lt;p&gt;This post will detail my experience (which was a very good one) of authoring my first challenge on the very cool new(ish) &lt;a href="https://labs.iximiuz.com" rel="noopener noreferrer"&gt;labs.iximiuz.com&lt;/a&gt; platform.&lt;/p&gt;

&lt;p&gt;...but first, a short digression into why it's so cool, and why I'm excited about it! &lt;/p&gt;

&lt;p&gt;It used to be that to create an entire learning environment, you had to create the whole thing yourself. If you wanted, like I did, to make a &lt;a href="https://micromaterialsblog.wordpress.com/2022/06/29/mongo-dojo-a-place-to-practice-mongodb-replicaset-skills/" rel="noopener noreferrer"&gt;mongo dojo&lt;/a&gt;, you had to set up the various VMs with something like vagrant, and then manage the machine states and the networking, and the storage, etc etc...&lt;/p&gt;

&lt;p&gt;But now, for particular learning cases, this platform makes it ridiculously straightforward. It's a very simple/easy way to get console access directly in the browser to running linux VMs in the cloud. Since it uses firecracker microVM's under the hood, it boots very fast as is ready to learn with minimal wait time. The accompanying content is also very comprehensive and easy to read.&lt;/p&gt;

&lt;p&gt;(For the full description of the tech stack that powers the platform, check out &lt;a href="https://iximiuz.com/en/posts/iximiuz-labs-story/" rel="noopener noreferrer"&gt;this post&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;So instead of needing to know about how to orchestrate all the VMs behind the scenes and wire that up to a very slick looking frontend with networking all smoothed out and support various users starting and stopping playgrounds and challenges constantly...you can just write yaml and markdown, and the platform can convert it into a working learning environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxad7eg8jt2v2nndeuztg.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxad7eg8jt2v2nndeuztg.jpg" alt="Image description" width="500" height="759"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Anatomy of a Challenge
&lt;/h2&gt;

&lt;p&gt;If you'd like to see for yourself what all the challenges look like, you can &lt;a href="https://labs.iximiuz.com/challenges" rel="noopener noreferrer"&gt;just go there now&lt;/a&gt;. There are loads of free ones, as well as a premium tier with all the content and higher usage limits for things like CPU and bandwidth. Also, the platform even works (fairly well) in a mobile UI (though turn off predictive text and spelling fixes for your keyboard or it's much harder).&lt;/p&gt;

&lt;p&gt;...for a bit more of a descriptive introduction to the challenges, read on...&lt;/p&gt;

&lt;p&gt;Each challenge is based on one of the existing playgrounds (or can even use a custom playground you design yourself), and so can be situated in environments like a single node k3s cluster or a multi-node VM configuration with Docker preinstalled.&lt;/p&gt;

&lt;p&gt;(for a full list of playgrounds, check out &lt;a href="https://labs.iximiuz.com/playgrounds" rel="noopener noreferrer"&gt;this page&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The challenge starts with some description (and ideally also a nice graphic) to set the stage for what the user needs to do. The platform handles all the formatting and styling, so all you need to do is bring the text, and it'll automatically show up looking very sharp.&lt;/p&gt;

&lt;p&gt;Each step can have associated hints that can be exposed if you're a bit stuck. Often these are links to the docs, but can also be examples of commands to try running.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu3cupd3811tr6xc1ygks.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu3cupd3811tr6xc1ygks.png" alt="Image description" width="730" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The UI has verification elements that go green when you've satisfied the requirements for that particular step. These are backed by simple bash scripts in the markdown file to check, for example, whether a certain number of pods are running with given labels, or whether two pods are able to reach each other over the network.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0mdl6byjv7ertxhw9njq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0mdl6byjv7ertxhw9njq.png" alt="Image description" width="735" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One last aspect that's available if you need it, is init tasks that can run before the user is presented with the terminal to play around in. These would be for installing some specific software that isn't available in the default playground base, or potentially generating some sample data that needs to be available when the user starts the challenge.&lt;/p&gt;

&lt;h2&gt;
  
  
  My CKA Challenge Creation
&lt;/h2&gt;

&lt;p&gt;Even though I don't have that much experience with kubernetes, it's still one of the most popular technologies in the DevOps space, and a lot of people are interested in it. So I figured I would enhance my own knowledge of how networking policies are configured and used in those systems.&lt;/p&gt;

&lt;p&gt;And if you've ever read this blog before, you'll know that my attitude is that the only thing better than building to learn is building to teach...so this platform is the perfect opportunity to do that.&lt;/p&gt;

&lt;p&gt;A very nice companion piece of tech for the platform is the official &lt;a href="https://github.com/iximiuz/labctl" rel="noopener noreferrer"&gt;Iximiuz Labs CLI&lt;/a&gt;. You can use it to easily create new challenges, update existing challenges, and generally do cool stuff from the comfort of your terminal. The obvious first step in this case was creating new content via the CLI.&lt;/p&gt;

&lt;p&gt;The default file autogenerated via the CLI has lots of helpful comments, and can guide you towards how to use the different fields available for configuration:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9i6mf1wapt6crbxxtu89.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9i6mf1wapt6crbxxtu89.png" alt="Image description" width="800" height="610"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(If you want to see what the markdown for my finished challenge looks like, &lt;a href="https://raw.githubusercontent.com/lpmi-13/cka-network-policies-between-deployments/refs/heads/main/index.md" rel="noopener noreferrer"&gt;that's here&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;There are a few things to be aware of, though, that tripped me up the first time:&lt;/p&gt;

&lt;p&gt;the categories are intended as higher level concepts like "kubernetes" that can be used to filter content in the platform UI, whereas the tagz are more for specific topics (like CKA) that should not overlap with the categories&lt;/p&gt;

&lt;p&gt;the machine and tab names need to be values from the existing system, and if you try to configure something custom, it (currently) has the risk of breaking things.&lt;/p&gt;

&lt;p&gt;if you expect to have some long-running tasks (or some very short-running ones), you might need to tweak the default timeouts for those in your configuration. I've seen a couple times now where I had tasks that didn't play nice with the default timeouts and I had to either explicitly make them shorter or longer to get things working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Iterating on the content
&lt;/h2&gt;

&lt;p&gt;The CLI isn't only able to generate/push content...it also has the ability to hot reload changes on save so you can get very fast feedback loops while authoring content. It's a total gamechanger to not need to wait for a daemon running somewhere on a server to pick up changes to see what you've updated.&lt;/p&gt;

&lt;p&gt;I found it very smooth to sketch out my ideas, see them running immediately in the environment, and then fix things that weren't yet quite right.&lt;/p&gt;

&lt;p&gt;And you can join the &lt;a href="https://discord.gg/p6nxaHm46p" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; server if you want to report a bug or request a feature. Ivan is very helpful and very interested in making sure learners (and authors!) have a positive experience on the platform.&lt;/p&gt;

&lt;p&gt;Some things I learned along the way to pass on:&lt;/p&gt;

&lt;p&gt;it's better to avoid using Dockerhub images in your challenges, since the rate limiting can be a bit aggressive. Much preferred is ghcr.io or another more robust container registry that allows public access.&lt;/p&gt;

&lt;p&gt;Instead of batching together related verification tasks into one check, it's a much nicer learner experience to separate them into discrete tasks that give iterative feedback more quickly as the user is completing the challenge. For example, in my challenge I originally had all the checks for correct labels in one big scripted check, though it was much nicer to split that into 3 checks and 3 pieces of feedback.&lt;/p&gt;

&lt;p&gt;I definitely recommend &lt;a href="https://excalidraw.com/" rel="noopener noreferrer"&gt;https://excalidraw.com/&lt;/a&gt; for getting simple diagrams together (in general, and for the purposes of putting in challenges). It (somewhat) hides my absolutely awful design skills.&lt;/p&gt;

&lt;p&gt;And here's the finished product, my very first challenge on this very cool platform!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://labs.iximiuz.com/challenges/cka-network-policies-between-deployments" rel="noopener noreferrer"&gt;https://labs.iximiuz.com/challenges/cka-network-policies-between-deployments&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fulo6v6242awcqih57ma2.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fulo6v6242awcqih57ma2.gif" width="260" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>linux</category>
      <category>cka</category>
    </item>
    <item>
      <title>Scary Git Commands Made Easy with Terminal-based Micromaterials</title>
      <dc:creator>Adam Leskis</dc:creator>
      <pubDate>Thu, 10 Sep 2020 09:57:21 +0000</pubDate>
      <link>https://forem.com/lpmi13/scary-git-commands-made-easy-with-terminal-based-micromaterials-42dl</link>
      <guid>https://forem.com/lpmi13/scary-git-commands-made-easy-with-terminal-based-micromaterials-42dl</guid>
      <description>&lt;p&gt;&lt;code&gt;rebase&lt;/code&gt;, &lt;code&gt;reflog&lt;/code&gt;, &lt;code&gt;submodule&lt;/code&gt;...these git commands can seem very scary and unapproachable when you first hear about them. They definitely seemed that way to me.&lt;/p&gt;

&lt;p&gt;Since I had been avoiding &lt;code&gt;rebase&lt;/code&gt; for that very reason, the first time I actually need to use it in my professional life as a developer was very stressful, and thankfully I had a much more experienced git user to pair with while doing it.&lt;/p&gt;

&lt;p&gt;It definitely DOES NOT have to be that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Existing resources
&lt;/h2&gt;

&lt;p&gt;There are an overwhelming number of free resources online to help learn about every conceivable git command, so why add more?&lt;/p&gt;

&lt;p&gt;While there are many informational resources (such as the excellent &lt;a href="https://git-scm.com/docs/git-rebase" rel="noopener noreferrer"&gt;documentation on rebase&lt;/a&gt;), these can tell you a lot "about" git, but they don't necessary tell you about the process of actually using it.&lt;/p&gt;

&lt;p&gt;In addition, there are also numerous tutorials that do actually guide users through the process of using these commands. Though by necessity, they tend to be very broad, speaking about rebasing in general rather than presenting one very specific objective detailing the context around why you would want to do something like &lt;code&gt;rebase&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I'd add that I'm sure a lot of git users have learned perfectly well using these kinds of resources, and if they're adequate for your purposes, by all means, go ahead and use what works.&lt;/p&gt;

&lt;p&gt;Personally, I always found them to be a bit lacking, so I decided to just make the stuff that I wish existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Micromaterials for git
&lt;/h2&gt;

&lt;p&gt;My aim was to create some very simple learning materials, which were very specific, open-source, and fast. I also wanted to focus on learning by doing, involving actually typing out the git commands in your terminal to see what happens.&lt;/p&gt;

&lt;p&gt;Unlike a situation involving a time-sensitive project at work, using these commands in your own local environment is very low-pressure, you can go at your own pace, and making a mistake is no big deal.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;for &lt;code&gt;rebase&lt;/code&gt;, I made &lt;a href="https://github.com/lpmi-13/rebasic" rel="noopener noreferrer"&gt;https://github.com/lpmi-13/rebasic&lt;/a&gt;, which asks you to just remove two commit messages while keeping the commits themselves. I do this all the time after finishing work on a feature branch and before opening a pull request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;for &lt;code&gt;reflog&lt;/code&gt;, I made &lt;a href="https://github.com/lpmi-13/reflog-power" rel="noopener noreferrer"&gt;https://github.com/lpmi-13/reflog-power&lt;/a&gt;, which simulates deleting a local branch and then bringing it back to life. This is also something that I didn't know was possible until I accidentally deleted the local branch with all my work, and then I had to get it back.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;for &lt;code&gt;submodule&lt;/code&gt; I made both &lt;a href="https://github.com/lpmi-13/submodz" rel="noopener noreferrer"&gt;https://github.com/lpmi-13/submodz&lt;/a&gt; and &lt;a href="https://github.com/lpmi-13/snubmodule" rel="noopener noreferrer"&gt;https://github.com/lpmi-13/snubmodule&lt;/a&gt;. The first one involves a very basic scenario of updating a submodule, and the second one deals with moving a submodule into your main repository. I don't do much with submodules, but I did have to update one in my job recently, and it took me a long time to figure out what to do.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Whatever works
&lt;/h2&gt;

&lt;p&gt;In such a wide field of software development, involving so many different types of people, there can never be one learning resource that works for everyone. Whatever works best for you is the correct resource to use. Successful learners use learning strategies, and the most successful learners select different learning strategies based on the situation.&lt;/p&gt;

&lt;p&gt;I'm hoping that these micromaterials can add to the wealth of resources online to help people learn about using git, and also start the wider conversation on what other micromaterials could help people develop their tech skills.&lt;/p&gt;

</description>
      <category>git</category>
      <category>micromaterials</category>
      <category>tutorial</category>
      <category>terminal</category>
    </item>
  </channel>
</rss>
