<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dhiraj Shrotri</title>
    <description>The latest articles on DEV Community by Dhiraj Shrotri (@dhiraj-shrotri).</description>
    <link>https://dev.to/dhiraj-shrotri</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3617590%2Fbab7d6a2-2005-4857-aba7-86818d3ab157.jpg</url>
      <title>DEV Community: Dhiraj Shrotri</title>
      <link>https://dev.to/dhiraj-shrotri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dhiraj-shrotri"/>
    <language>en</language>
    <item>
      <title>Self-hosting Paperless-ngx on k3s with GitOps</title>
      <dc:creator>Dhiraj Shrotri</dc:creator>
      <pubDate>Mon, 26 Oct 2026 09:58:26 +0000</pubDate>
      <link>https://dev.to/dhiraj-shrotri/-self-hosting-paperless-ngx-on-k3s-with-gitops-2n0i</link>
      <guid>https://dev.to/dhiraj-shrotri/-self-hosting-paperless-ngx-on-k3s-with-gitops-2n0i</guid>
      <description>&lt;p&gt;I almost always use the Adobe Scan app to scan any documents that I have. I recently noticed that by default, Adobe saves the documents to their own cloud, instead of storing them on my device. I had a beelink mini pc and a 1TB external HDD lying around. I started looking for solutions which would let me scan a document using my phone's camera, tag the document by content and then upload it to my beelink and eventually to the external HDD.&lt;/p&gt;

&lt;p&gt;I ended up with Paperless-ngx running on k3s on a Beelink mini PC. Documents live on an external HDD, and Flux deploys every config change I push to GitHub. This post covers the setup and, more usefully, every error I hit along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Paperless-ngx
&lt;/h2&gt;

&lt;p&gt;Paperless-ngx is a document archive, not a file cabinet. It OCRs everything you add, so searching "electricity bill March" finds the scan. It also tags and sorts documents automatically.&lt;/p&gt;

&lt;p&gt;Nextcloud was the other contender. It's a better general Google Drive replacement, but its document scanner isn't available in every Android build, and it organises by folders rather than content. For paperwork, Paperless won.&lt;/p&gt;

&lt;p&gt;There's no official mobile app, but the community ones are good:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Swift Paperless&lt;/td&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;Native, actively maintained, camera scanning, Face ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PaperNext&lt;/td&gt;
&lt;td&gt;Android, iOS&lt;/td&gt;
&lt;td&gt;Built-in scanner, offline cache, supports Paperless-ngx 2 and 3 (open beta)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paperless Mobile&lt;/td&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;The longer-standing Android client&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three let you scan, tag and upload in one flow. The server then OCRs and files the document.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;Everything runs on one Beelink mini PC with k3s. Paperless-ngx uses the same components as the official Docker Compose file: PostgreSQL, Redis, and Tika plus Gotenberg for Word, Excel and email files.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdnvt672j4pqn4ekn7qnb.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdnvt672j4pqn4ekn7qnb.png" alt=" " width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The database and search index stay on the fast internal SSD. Documents and nightly backups go to the external HDD. My phone reaches Paperless through k3s's bundled Traefik, and config changes arrive from GitHub through Flux.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using an external HDD as a Kubernetes volume
&lt;/h2&gt;

&lt;p&gt;I wanted documents on the big external HDD, not the Beelink's small internal SSD. The trick is a static &lt;code&gt;local&lt;/code&gt; PersistentVolume pointing at a folder on the HDD.&lt;/p&gt;

&lt;p&gt;First, format the disk as ext4 (NTFS and exFAT can't store Linux ownership) and mount it by UUID:&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;mkfs.ext4 &lt;span class="nt"&gt;-L&lt;/span&gt; paperless-hdd /dev/sdb1     &lt;span class="c"&gt;# erases the disk&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/hdd
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'UUID=&amp;lt;uuid&amp;gt;  /mnt/hdd  ext4  defaults,nofail,noatime  0 2'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/fstab
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/hdd/paperless/&lt;span class="o"&gt;{&lt;/span&gt;media,export&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 1000:1000 /mnt/hdd/paperless
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;nofail&lt;/code&gt; lets the machine still boot without the disk. Then make k3s wait for the mount, via &lt;code&gt;sudo systemctl edit k3s&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;RequiresMountsFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/mnt/hdd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the Kubernetes side: a StorageClass with no provisioner, and one PersistentVolume per folder, pinned to the node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;storage.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StorageClass&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hdd-local&lt;/span&gt;
&lt;span class="na"&gt;provisioner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubernetes.io/no-provisioner&lt;/span&gt;
&lt;span class="na"&gt;volumeBindingMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WaitForFirstConsumer&lt;/span&gt;
&lt;span class="na"&gt;reclaimPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Retain&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PersistentVolume&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paperless-media-hdd&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;capacity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;500Gi&lt;/span&gt;
  &lt;span class="na"&gt;accessModes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ReadWriteOnce&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;persistentVolumeReclaimPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Retain&lt;/span&gt;
  &lt;span class="na"&gt;storageClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hdd-local&lt;/span&gt;
  &lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/mnt/hdd/paperless/media&lt;/span&gt;
  &lt;span class="na"&gt;nodeAffinity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;nodeSelectorTerms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matchExpressions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubernetes.io/hostname&lt;/span&gt;
              &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
              &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-node"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# kubectl get nodes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PVC then binds to it with &lt;code&gt;storageClassName: hdd-local&lt;/code&gt; and &lt;code&gt;volumeName: paperless-media-hdd&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why &lt;code&gt;local&lt;/code&gt; and not &lt;code&gt;hostPath&lt;/code&gt;? Because the volume points at a subfolder that only exists on the HDD. If the disk isn't mounted, the pod refuses to start, instead of quietly writing your documents to the internal SSD. &lt;code&gt;Retain&lt;/code&gt; means deleting the PVC never deletes your files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every error I hit, and the fix
&lt;/h2&gt;

&lt;p&gt;The manifests were the easy part. Here's what actually took the afternoon.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. k3s died halfway through &lt;code&gt;kubectl apply&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dial tcp 127.0.0.1:6443: connect: connection refused
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API server went down mid-apply. &lt;code&gt;kubectl apply&lt;/code&gt; is idempotent, so once k3s is back you just re-run it. The real question was why k3s died.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;bind: address already in use&lt;/code&gt; on port 10257
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;failed to listen on 127.0.0.1:10257: bind: address already in use
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sudo ss -ltnp | grep 10257&lt;/code&gt; showed the culprit: &lt;code&gt;kubelite&lt;/code&gt;, which is MicroK8s. I had a forgotten MicroK8s install on the same machine, and both distributions want the same controller-manager ports. Snap had probably auto-restarted it mid-install.&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;microk8s stop
&lt;span class="nb"&gt;sudo &lt;/span&gt;snap disable microk8s     &lt;span class="c"&gt;# or: sudo snap remove microk8s&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart k3s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. &lt;code&gt;failed to find interface with specified node ip&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;k3s had registered with an IP the machine no longer had, because my router handed out a new DHCP lease. Pin the current IP in &lt;code&gt;/etc/rancher/k3s/config.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;node-ip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;192.168.1.50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then set a DHCP reservation in the router so it never drifts again.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Pod stuck in &lt;code&gt;ContainerCreating&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MountVolume.NewMounter initialization failed ... path "/mnt/hdd/paperless/media" does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was the HDD safety check working as designed: I hadn't created the folders. Check &lt;code&gt;findmnt /mnt/hdd&lt;/code&gt; &lt;strong&gt;first&lt;/strong&gt;. If you create the folders while the disk isn't mounted, they land on the SSD.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Paperless crash-looping: &lt;code&gt;'tcp://10.43.x.x:8000' is not a valid integer&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;My favourite one. Kubernetes injects environment variables for every Service in the namespace. A Service named &lt;code&gt;paperless&lt;/code&gt; produces &lt;code&gt;PAPERLESS_PORT=tcp://10.43.220.175:8000&lt;/code&gt;, and Paperless reads &lt;code&gt;PAPERLESS_PORT&lt;/code&gt; as its own listen port. One line on the pod spec fixes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enableServiceLinks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. CSRF verification failed after login
&lt;/h3&gt;

&lt;p&gt;Paperless only accepts logins from the exact origin in &lt;code&gt;PAPERLESS_URL&lt;/code&gt;, and my phone's browser was silently upgrading to &lt;code&gt;https&lt;/code&gt;. Trust both schemes:&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="nv"&gt;PAPERLESS_CSRF_TRUSTED_ORIGINS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://paperless.192.168.1.50.nip.io,https://paperless.192.168.1.50.nip.io
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used &lt;a href="https://nip.io" rel="noopener noreferrer"&gt;nip.io&lt;/a&gt; for the hostname: &lt;code&gt;paperless.192.168.1.50.nip.io&lt;/code&gt; resolves to &lt;code&gt;192.168.1.50&lt;/code&gt;, so phones work with no local DNS.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitOps: deploy with a git push
&lt;/h2&gt;

&lt;p&gt;Once it worked, I didn't want to SSH in and &lt;code&gt;kubectl apply&lt;/code&gt; every tweak. Flux runs inside the cluster and &lt;em&gt;pulls&lt;/em&gt; from GitHub, so the home server never needs an open inbound port. That's why I chose it over a push-based CI job. It's also lighter than Argo CD on a mini PC, and it decrypts SOPS secrets natively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Repo layout
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clusters/home/
  apps.yaml            # Flux: deploy ./apps, decrypt with SOPS
  flux-system/         # created by flux bootstrap
apps/
  kustomization.yaml
  paperless/
    kustomization.yaml
    paperless.env      # day-to-day settings
    secret.yaml        # SOPS-encrypted
    paperless.yaml
    backing-services.yaml
    storage.yaml
    namespace.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repo lives on my laptop. The server never holds a copy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Config changes restart Paperless automatically
&lt;/h3&gt;

&lt;p&gt;A pod reads env vars only at startup, so editing a ConfigMap does nothing to a running pod. Kustomize's &lt;code&gt;configMapGenerator&lt;/code&gt; fixes that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;configMapGenerator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paperless-config&lt;/span&gt;
    &lt;span class="na"&gt;envs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;paperless.env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ConfigMap gets a content hash in its name (&lt;code&gt;paperless-config-tg67g2tb44&lt;/code&gt;). Kustomize rewrites every reference to it, so any edit changes the pod spec and triggers a rollout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secrets with SOPS + age
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;age-keygen &lt;span class="nt"&gt;-o&lt;/span&gt; ~/.config/sops/age/keys.txt
sops &lt;span class="nt"&gt;--encrypt&lt;/span&gt; &lt;span class="nt"&gt;--in-place&lt;/span&gt; apps/paperless/secret.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.sops.yaml&lt;/code&gt; (committed, since it only holds the public key) encrypts just the &lt;code&gt;data&lt;/code&gt; and &lt;code&gt;stringData&lt;/code&gt; fields. The private key goes into the cluster once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; flux-system create secret generic sops-age &lt;span class="nt"&gt;--from-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;age.agekey&lt;span class="o"&gt;=&lt;/span&gt;keys.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the Flux Kustomization points at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kustomize.toolkit.fluxcd.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Kustomization&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flux-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps&lt;/span&gt;
  &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;sourceRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GitRepository&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flux-system&lt;/span&gt;
  &lt;span class="na"&gt;decryption&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sops&lt;/span&gt;
    &lt;span class="na"&gt;secretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sops-age&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Don't let &lt;code&gt;prune&lt;/code&gt; delete your data
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;prune: true&lt;/code&gt; means removing a file from Git removes the resource from the cluster. That's great for Deployments and terrifying for storage. I annotated the namespace, PVCs, PVs and StorageClass so Flux never deletes them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kustomize.toolkit.fluxcd.io/prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;disabled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bootstrap gotchas
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Push first.&lt;/strong&gt; &lt;code&gt;flux bootstrap&lt;/code&gt; doesn't push your local commits, only its own &lt;code&gt;flux-system&lt;/code&gt; files. Push the repo, run bootstrap on the server, then &lt;code&gt;git pull&lt;/code&gt; on the laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it where kubectl works.&lt;/strong&gt; For me that was the server over SSH.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;x509: certificate signed by unknown authority.&lt;/strong&gt; &lt;code&gt;flux&lt;/code&gt; reads &lt;code&gt;~/.kube/config&lt;/code&gt;, which was stale from MicroK8s. k3s's &lt;code&gt;kubectl&lt;/code&gt; reads &lt;code&gt;/etc/rancher/k3s/k3s.yaml&lt;/code&gt;. Copy that file to &lt;code&gt;~/.kube/config&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Now I scan a bill on my phone and it's OCR'd and searchable on my own hardware within a minute. Changing a setting is one edit in &lt;code&gt;paperless.env&lt;/code&gt; and a &lt;code&gt;git push&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What's next on my list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observability and alerting.&lt;/strong&gt; I am thinking of integrating Prometheus + Grafana for metrics and configuring alertmanager to send me alerts if a pod goes down via Telegram.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailscale.&lt;/strong&gt; It gives me access from outside the house and real HTTPS on a &lt;code&gt;*.ts.net&lt;/code&gt; name, without exposing anything to the internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pinned image tags.&lt;/strong&gt; &lt;code&gt;:latest&lt;/code&gt; plus GitOps means a restart can upgrade you silently. Pin the tag and bump it with a commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're doing this on a single-node k3s box, the two lines that would have saved me the most time are &lt;code&gt;enableServiceLinks: false&lt;/code&gt; and "check for a second Kubernetes install before anything else."&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.postgres-tika.yml" rel="noopener noreferrer"&gt;Paperless-ngx official Docker Compose (Postgres + Tika)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/paperless-ngx/paperless-ngx/wiki/Related-Projects" rel="noopener noreferrer"&gt;Paperless-ngx related projects (mobile apps)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.klissner.uk/en/paperless-apps-for-on-the-go/" rel="noopener noreferrer"&gt;Paperless apps for on the go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.papernext.de/" rel="noopener noreferrer"&gt;PaperNext&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fluxcd.io/flux/installation/bootstrap/github/" rel="noopener noreferrer"&gt;Flux: GitHub bootstrap&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm open to full-stack and platform software engineering roles. If your team needs someone who builds the app and the infrastructure it runs on, please feel free to reach out to me on &lt;a href="https://www.linkedin.com/in/dhiraj-shrotri" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or &lt;a href="mailto:dhiraj.shrotri@gmail.com"&gt;Email Me&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>selfhosted</category>
      <category>gitops</category>
      <category>homelab</category>
    </item>
    <item>
      <title>How do you scale a frontend application to a million users ?</title>
      <dc:creator>Dhiraj Shrotri</dc:creator>
      <pubDate>Thu, 18 Jun 2026 20:23:45 +0000</pubDate>
      <link>https://dev.to/dhiraj-shrotri/how-do-you-scale-a-frontend-application-to-a-million-users--3777</link>
      <guid>https://dev.to/dhiraj-shrotri/how-do-you-scale-a-frontend-application-to-a-million-users--3777</guid>
      <description>&lt;p&gt;I was recently in a job interview (yes, the kind you have to do when you get laid off) and the interviewer asked me: "How would you scale a frontend application from 100,000 users to a million plus?"&lt;br&gt;
I gave a decent enough answer at the moment. But afterwards, I kept thinking about everything I didn't say. So here we are. Consider this my full answer, delivered about three months too late, to actually matter for that job.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;First, Let's Calibrate&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Scaling a frontend isn't just about throwing more servers at the problem. That's your backend team's headache. The frontend has its own scaling concerns, and a lot of them are invisible until you're already on fire. The jump from 100k to 1M+ users is less about one dramatic architectural overhaul and more about fixing the 20% of things causing 80% of your slowness. The trick is knowing which 20%.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Performance &amp;amp; Asset Delivery: The Biggest Bang For Your Buck&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If you're not serving your static assets through a CDN at this scale, stop reading this and go fix that first. Cloudflare, CloudFront, Fastly;  pick one. Your origin server should never be sweating over a CSS file.&lt;/p&gt;

&lt;p&gt;Pair your CDN with content-hashed filenames. This lets you set aggressive long-term cache TTLs without worrying about users getting stale assets after a deployment. It's one of those things that feels like a small detail until you realise it's quietly saving you a fortune in bandwidth costs and keeping your load times fast for repeat visitors.&lt;/p&gt;

&lt;p&gt;The other non-negotiable is code splitting and lazy loading. Users should only download the JavaScript they need for the page they're actually on. Nobody visiting your landing page needs the bundle for your admin dashboard. With Vite this is largely handled for you at the route level, but you still need to be intentional about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;State Management: Where Things Get Messy At Scale&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;At 100k users, messy state management is annoying. At a million plus, it becomes a performance crisis. The most important distinction to get right is separating server state from client state. These are fundamentally different things and treating them the same way is a trap I've fallen into personally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server state&lt;/strong&gt;: data that lives on your backend and needs to be fetched, cached, and kept in sync, should be handled by something like React Query or SWR. These libraries give you stale-while-revalidate caching out of the box, which means your UI feels snappy while fresh data loads in the background.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client state&lt;/strong&gt;:  things like UI toggles, modal open/close, user preferences etc belong in something lean like Redux or even just local component state if it doesn't need to be shared widely. Not everything needs to be in a global store. Redux is a perfectly good tool that a lot of people use as a hammer for every nail.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The API &amp;amp; Data Layer&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A few principles that matter a lot at scale:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Virtualize large lists&lt;/strong&gt;: Never render 10,000 rows in the DOM. Libraries like TanStack Virtual handle this elegantly, only the rows visible in the viewport actually exist in the DOM. This sounds obvious until you're staring at a performance profile wondering why scrolling feels like wading through concrete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debounce and throttle user-triggered requests&lt;/strong&gt;: Search inputs that fire an API call on every keystroke are a classic scaling antipattern. Debounce them. Your backend team will thank you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paginate everything&lt;/strong&gt;: Infinite scroll or traditional pagination - pick your poison, but loading entire datasets upfront is a sin at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;You Cannot Optimize What You Cannot Measure&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is probably the lesson I wish I'd learned earlier in my career. At scale, gut feel and local testing will actively mislead you. You need Real User Monitoring — tools like Datadog or Sentry that show you what actual users on actual devices on actual network conditions are experiencing, not just what your M2 MacBook on gigabit ethernet thinks is happening.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track your Core Web Vitals ( LCP, CLS, and INP specifically)&lt;/strong&gt;: These aren't just Google SEO metrics, they directly correlate with whether users stay on your page or leave. If your LCP is over 2.5 seconds, a non-trivial percentage of users are already gone before they've seen anything meaningful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error tracking with context is also non-negotiable&lt;/strong&gt;: At a million users, you will have errors you never saw in testing. You need to know about them before your users start tweeting about them.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Bundle Size: The Silent Killer&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Run a bundle analysis on your app right now. I'll wait. Tools like &lt;code&gt;rollup-visualizer&lt;/code&gt; (for Vite) or &lt;code&gt;webpack-bundle-analyzer&lt;/code&gt; will show you a treemap of everything you're shipping to the user. There is almost always something in there that makes you say "why is that so big?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common culprits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A date library you imported for one utility function that weighs 70kb.&lt;/li&gt;
&lt;li&gt;A component library where you're importing the entire thing for three components.&lt;/li&gt;
&lt;li&gt;Polyfills targeting browsers that represent 0.2% of your user base.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tree shaking handles a lot of this automatically, but it's not magic. Named imports, side effectful modules, and certain CommonJS packages can defeat it. The bundle analyzer doesn't lie.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Architecture Patterns For The Long Game&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Micro-frontends become a real conversation at a very large scale. Different teams owning different slices of the UI independently, deploying without coordinating with everyone else. The tradeoff is real though. Shared dependency management becomes its own full-time job and the coordination overhead can eat the productivity gains if you're not careful. I wouldn't reach for this unless you have multiple large teams stepping on each other's toes.&lt;/p&gt;

&lt;p&gt;Edge rendering tools like Next.js Edge Runtime, Cloudflare Workers etc can dramatically reduce time-to-first-byte for a globally distributed user base. If your users are spread across continents and you're rendering from a single region, you're leaving a lot of performance on the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Stuff Nobody Talks About Enough&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Images&lt;/strong&gt;. Consistently the most underestimated performance problem. Use modern formats (WebP, AVIF). Lazy load anything below the fold. Serve correctly sized images for each viewport. An image that looks fine on your screen might be a 4MB monster getting served to someone on mobile data.&lt;br&gt;
Fonts. font-display: swap and preloading your critical fonts goes a long way. A font that blocks rendering is a font that's costing you users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third party scripts&lt;/strong&gt;. Analytics tools, chat widgets, A/B testing libraries audit these ruthlessly. Every third party script is code you don't control running on your page, and at scale their performance cost adds up. Some of them are genuinely heavier than your entire application.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;So, What's The Actual Answer?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If I could go back and answer that interview question more completely, I'd say this: scaling a frontend to a million users is mostly about discipline. Discipline in measuring before optimizing, discipline in keeping your bundle lean, discipline in not letting state management become a free-for-all, and discipline in treating performance as a feature rather than a cleanup task you'll get to eventually.&lt;/p&gt;

&lt;p&gt;The technical solutions - CDNs, virtualization, code splitting, RUM - are well documented and not particularly exotic. The hard part is building a culture and a codebase where performance doesn't quietly degrade every sprint until you're suddenly wondering why conversion dropped 15%.&lt;/p&gt;

&lt;p&gt;Anyway. If anyone's hiring a frontend engineer who thinks a lot about this stuff, you know where to find me.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>reactjsdevelopment</category>
      <category>webdev</category>
    </item>
    <item>
      <title>K3S troubleshooting: Fixing K3s permission denied and port binding errors on Ubuntu Server</title>
      <dc:creator>Dhiraj Shrotri</dc:creator>
      <pubDate>Sun, 30 Nov 2025 20:09:01 +0000</pubDate>
      <link>https://dev.to/dhiraj-shrotri/k3s-troubleshooting-fixing-k3s-permission-denied-and-port-binding-errors-on-ubuntu-server-422d</link>
      <guid>https://dev.to/dhiraj-shrotri/k3s-troubleshooting-fixing-k3s-permission-denied-and-port-binding-errors-on-ubuntu-server-422d</guid>
      <description>&lt;p&gt;Recently, while setting up K3s on a Beelink mini PC running Ubuntu Server, I ran into two frustrating errors while trying to run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first issue was a permissions error loading the kubeconfig, and once that was fixed, a second error appeared indicating that the Kubernetes control plane wasn’t running. After debugging, here’s the full process and solution in case you run into the same problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: kubeconfig Permission Denied
&lt;/h2&gt;

&lt;p&gt;The initial error looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARN[0000] Unable to read /etc/rancher/k3s/k3s.yaml, please start server with --write-kubeconfig-mode or --write-kubeconfig-group to modify kube config permissions
error: error loading config file "/etc/rancher/k3s/k3s.yaml": open /etc/rancher/k3s/k3s.yaml: permission denied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This happens because K3s generates the Kubernetes config file owned by root with restrictive permissions (600). When we run kubectl as a regular user, kubectl can't access it. &lt;/p&gt;

&lt;p&gt;To fix the issue, we create another directory, copy the kubectl config to it and change the ownership of the directory so that kubectl can access it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now running kubectl get nodes threw a different error&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The connection to the server 127.0.0.1:6443 was refused - did you specify the right host or port?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally some progress!&lt;/p&gt;

&lt;p&gt;This means kubectl can now read the config file, but the K3s API server isn’t running or reachable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 2: K3s controller-manager port conflict
&lt;/h3&gt;

&lt;p&gt;Checking the K3s service:&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 status k3s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logs showed this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;failed to listen on 127.0.0.1:10257: bind: address already in use
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This indicates another process is already using port 10257, which is required by the Kubernetes controller-manager. We verify what process is using the port by running the lsof command with the port as argument.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo lsof -i :10257
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubelite 188849 root 120u IPv6 ... TCP *:10257 (LISTEN)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;kubelite is K3s’s internal Kubernetes binary. That means a previous crashed instance left it running, preventing the new instance from starting.&lt;/p&gt;

&lt;p&gt;To fix this, stop k3s completely:&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 stop k3s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and kill leftover processes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo pkill -f k3s
sudo pkill -f kubelite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now to verify the ports are free, we run the lsof command again and that should give us no output.&lt;/p&gt;

&lt;p&gt;Finally we just restart k3s:&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 start k3s
sudo systemctl status k3s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when we run kubectl get nodes, it should run without any issues!&lt;/p&gt;

&lt;p&gt;K3s is incredibly powerful and lightweight, but diagnosing startup issues can be intimidating if you’re new to Kubernetes internals. Understanding the logs and system service behavior helped uncover the root cause quickly.&lt;/p&gt;

&lt;p&gt;Hopefully this helps someone trying to set up K3s on bare metal or home lab hardware!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>containers</category>
      <category>homelab</category>
    </item>
  </channel>
</rss>
