<?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: Luger Lex Pit-og</title>
    <description>The latest articles on DEV Community by Luger Lex Pit-og (@lugerlogic).</description>
    <link>https://dev.to/lugerlogic</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%2F3218475%2F38b682a6-1866-4427-8d6d-99a216bd86a5.jpg</url>
      <title>DEV Community: Luger Lex Pit-og</title>
      <link>https://dev.to/lugerlogic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lugerlogic"/>
    <language>en</language>
    <item>
      <title>[Lab Notes] Kubernetes the Hard Way, For Real This Time (Step 08)</title>
      <dc:creator>Luger Lex Pit-og</dc:creator>
      <pubDate>Wed, 23 Sep 2026 20:47:21 +0000</pubDate>
      <link>https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-step-08-3m60</link>
      <guid>https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-step-08-3m60</guid>
      <description>&lt;p&gt;Continuing my &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way" rel="noopener noreferrer"&gt;Kubernetes the Hard Way&lt;/a&gt; homelab build. Steps 01-07 are already done, this covers step 08.&lt;/p&gt;

&lt;p&gt;Original guide: &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/08-bootstrapping-kubernetes-controllers.md" rel="noopener noreferrer"&gt;08-bootstrapping-kubernetes-controllers.md&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Thoughts I had while doing this
&lt;/h2&gt;

&lt;p&gt;Much like the previous step with etcd, this one is about installing more components. Mainly the Kubernetes API Server, Scheduler, and Controller Manager.&lt;/p&gt;

&lt;p&gt;Same pattern as before: copy the binaries over to the controller node, configure them, and each one becomes a running Linux service.&lt;/p&gt;

&lt;p&gt;What I was trying to figure out was &lt;em&gt;what each of these components is for&lt;/em&gt;. I already get etcd, it stores the state. Here's my attempt at explaining the other three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Server&lt;/strong&gt; - this is where every API call from the other Kubernetes components ends up. If you want to do anything in the cluster, it has to go through here, since it's the only component that reads/writes directly to etcd. That's also why it's loaded with security config (&lt;code&gt;ca.crt&lt;/code&gt;, &lt;code&gt;ca.key&lt;/code&gt;, etc.). It's the gatekeeper in front of the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduler&lt;/strong&gt; - decides which node a pod should run on. It just checks whether a pod already has an assigned node; if not, it calls the API server to assign one. The API server is the one that actually writes that update to etcd, the scheduler just makes the decision and reports it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controller Manager&lt;/strong&gt; - as far as I understand it, this is really just a bundle of other controllers. If the API server is the one directly reading/writing etcd, and the scheduler just decides pod placement, the controller manager is just "everything else." A couple examples of what that covers:

&lt;ul&gt;
&lt;li&gt;"One of the physical servers running our pods went down, let me do something about it."&lt;/li&gt;
&lt;li&gt;"A job failed inside a pod, let me retry it."&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A gotcha along the way: apiserver wouldn't start
&lt;/h2&gt;

&lt;p&gt;Hit a wall trying to get &lt;code&gt;kube-apiserver&lt;/code&gt; up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;systemctl is-active kube-apiserver
&lt;span class="go"&gt;activating
&lt;/span&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;systemctl status kube-apiserver
&lt;span class="go"&gt;● kube-apiserver.service - Kubernetes API Server
&lt;/span&gt;&lt;span class="gp"&gt;     Loaded: loaded (/etc/systemd/system/kube-apiserver.service;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;enabled&lt;span class="p"&gt;;&lt;/span&gt; vendor preset: enabled&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;     Active: activating (auto-restart) (Result: exit-code) since Thu 2026-09-17 13:13:42 UTC;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;89ms ago
&lt;span class="go"&gt;       Docs: https://github.com/kubernetes/kubernetes
    Process: 56537 ExecStart=/usr/local/bin/kube-apiserver --allow-privileged=true ... (code=exited, status=1/FAILURE)
   Main PID: 56537 (code=exited, status=1/FAILURE)
        CPU: 48ms

Sep 17 13:13:42 server systemd[1]: kube-apiserver.service: Main process exited, code=exited, status=1/FAILURE
Sep 17 13:13:42 server systemd[1]: kube-apiserver.service: Failed with result 'exit-code'.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checked the logs for the actual error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; kube-apiserver
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;Sep 17 13:08:07 server kube-apiserver[55147]: I0917 13:08:07.493634   55147 server.go:147] Version: v1.32.3
Sep 17 13:08:07 server kube-apiserver[55147]: E0917 13:08:07.493805   55147 run.go:72] "command failed" err="failed to create listener: failed to listen on 0.0.0.0:6443: listen tcp 0.0.0.0:6443: bind: address already in use"
Sep 17 13:08:07 server systemd[1]: kube-apiserver.service: Main process exited, code=exited, status=1/FAILURE
Sep 17 13:08:07 server systemd[1]: kube-apiserver.service: Failed with result 'exit-code'.
Sep 17 13:08:12 server systemd[1]: kube-apiserver.service: Scheduled restart job, restart counter is at 1.
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Something else was already bound to port 6443:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;6443
&lt;span class="go"&gt;LISTEN 0      4096               *:6443             *:*    users:(("k3s-server",pid=623,fd=13))
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turned out I'd tried setting up k3s on this same machine a few months back and forgot about it. Stopped and disabled the leftover service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop k3s
&lt;span class="go"&gt;sudo systemctl disable k3s
Removed /etc/systemd/system/multi-user.target.wants/k3s.service.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, &lt;code&gt;kube-apiserver&lt;/code&gt; started cleanly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;systemctl is-active kube-apiserver
&lt;span class="go"&gt;active
&lt;/span&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl cluster-info &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  --kubeconfig admin.kubeconfig
Kubernetes control plane is running at https://127.0.0.1:6443
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now here's the actual step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;scp &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  downloads/controller/kube-apiserver \
  downloads/controller/kube-controller-manager \
  downloads/controller/kube-scheduler \
  downloads/client/kubectl \
  units/kube-apiserver.service \
  units/kube-controller-manager.service \
  units/kube-scheduler.service \
  configs/kube-scheduler.yaml \
  configs/kube-apiserver-to-kubelet.yaml \
  root@server:~/
kube-apiserver                     100%   89MB  90.4MB/s   00:00
kube-controller-manager            100%   82MB  93.6MB/s   00:00
kube-scheduler                     100%   63MB 157.4MB/s   00:00
kubectl                            100%   55MB 153.5MB/s   00:00
kube-apiserver.service             100% 1374     1.6MB/s   00:00
kube-controller-manager.service    100%  735   936.9KB/s   00:00
kube-scheduler.service             100%  281   349.7KB/s   00:00
kube-scheduler.yaml                100%  191   231.0KB/s   00:00
kube-apiserver-to-kubelet.yaml     100%  727   952.4KB/s   00:00
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Provision the Kubernetes control plane
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/kubernetes/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install the Kubernetes controller binaries
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="go"&gt;  mv kube-apiserver \
    kube-controller-manager \
    kube-scheduler kubectl \
    /usr/local/bin/
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configure the Kubernetes API Server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="go"&gt;  mkdir -p /var/lib/kubernetes/

  mv ca.crt ca.key \
    kube-api-server.key kube-api-server.crt \
    service-accounts.key service-accounts.crt \
    encryption-config.yaml \
    /var/lib/kubernetes/
}

&lt;/span&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;kube-apiserver.service &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  /etc/systemd/system/kube-apiserver.service
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configure the Kubernetes Controller Manager
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;kube-controller-manager.kubeconfig /var/lib/kubernetes/
&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;kube-controller-manager.service /etc/systemd/system/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configure the Kubernetes Scheduler
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;kube-scheduler.kubeconfig /var/lib/kubernetes/
&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;kube-scheduler.yaml /etc/kubernetes/config/
&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;kube-scheduler.service /etc/systemd/system/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Start the controller services
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="go"&gt;  systemctl daemon-reload

  systemctl enable kube-apiserver \
    kube-controller-manager kube-scheduler

  systemctl start kube-apiserver \
    kube-controller-manager kube-scheduler
}
Created symlink /etc/systemd/system/multi-user.target.wants/kube-apiserver.service → /etc/systemd/system/kube-apiserver.service.
Created symlink /etc/systemd/system/multi-user.target.wants/kube-controller-manager.service → /etc/systemd/system/kube-controller-manager.service.
Created symlink /etc/systemd/system/multi-user.target.wants/kube-scheduler.service → /etc/systemd/system/kube-scheduler.service.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  API server verification
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl cluster-info &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  --kubeconfig admin.kubeconfig
Kubernetes control plane is running at https://127.0.0.1:6443

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  RBAC for kubelet authorization
&lt;/h2&gt;

&lt;p&gt;This part just makes sure the API server is allowed to talk to the kubelet APIs on the worker nodes. As I understand it, the command here applies whatever's defined in &lt;code&gt;kube-apiserver-to-kubelet.yaml&lt;/code&gt; to the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@server:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; kube-apiserver-to-kubelet.yaml &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  --kubeconfig admin.kubeconfig
clusterrole.rbac.authorization.k8s.io/system:kube-apiserver-to-kubelet created
clusterrolebinding.rbac.authorization.k8s.io/system:kube-apiserver created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;--cacert&lt;/span&gt; ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  https://server.kubernetes.local:6443/version
{
  "major": "1",
  "minor": "32",
  "gitVersion": "v1.32.3",
  "gitCommit": "32cc146f75aad04beaaa245a7157eb35063a9f99",
  "gitTreeState": "clean",
  "buildDate": "2025-03-11T19:52:21Z",
  "goVersion": "go1.23.6",
  "compiler": "gc",
  "platform": "linux/amd64"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TLS is working end-to-end. The jumpbox hit the API server over HTTPS using only the CA cert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Setup the three control plane components: API server, controller manager, and scheduler as systemd services on the controller node, and placed RBAC so the API server can talk to kubelets on the workers.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>proxmox</category>
      <category>beginners</category>
      <category>devops</category>
    </item>
    <item>
      <title>[Lab Notes] Kubernetes the Hard Way, For Real This Time (Step 07)</title>
      <dc:creator>Luger Lex Pit-og</dc:creator>
      <pubDate>Sun, 20 Sep 2026 19:14:17 +0000</pubDate>
      <link>https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-step-07-4hna</link>
      <guid>https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-step-07-4hna</guid>
      <description>&lt;p&gt;Continuing my &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way" rel="noopener noreferrer"&gt;Kubernetes the Hard Way&lt;/a&gt; homelab build. Steps 01-06 are already done, this covers step 07.&lt;/p&gt;

&lt;p&gt;Original guide: &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/07-bootstrapping-etcd.md" rel="noopener noreferrer"&gt;07-bootstrapping-etcd.md&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Thoughts I had while doing this
&lt;/h2&gt;

&lt;p&gt;This step sets up the etcd cluster. From what I understand, etcd is basically Kubernetes' database, it's where the "state" of the whole cluster gets stored.&lt;/p&gt;

&lt;p&gt;Quick TLDR before the actual steps: &lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;grab the etcd binaries/service file &lt;/li&gt;
&lt;li&gt;copy them over to the controller/server node&lt;/li&gt;
&lt;li&gt;then install and configure etcd as a systemd service&lt;/li&gt;
&lt;li&gt;End result: a running &lt;code&gt;etcd&lt;/code&gt; Linux service on the controller.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I basically just installed a database for Kubernetes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# scp &lt;span class="se"&gt;\&lt;/span&gt;
  downloads/controller/etcd &lt;span class="se"&gt;\&lt;/span&gt;
  downloads/client/etcdctl &lt;span class="se"&gt;\&lt;/span&gt;
  units/etcd.service &lt;span class="se"&gt;\&lt;/span&gt;
  root@server:~/
etcd        100%   24MB  92.2MB/s   00:00
etcdctl     100%   16MB 130.8MB/s   00:00
etcd.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install the etcd binaries
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@server:~# &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;mv &lt;/span&gt;etcd etcdctl /usr/local/bin/
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configure the etcd server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@server:~# &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/etcd /var/lib/etcd
  &lt;span class="nb"&gt;chmod &lt;/span&gt;700 /var/lib/etcd
  &lt;span class="nb"&gt;cp &lt;/span&gt;ca.crt kube-api-server.key kube-api-server.crt &lt;span class="se"&gt;\&lt;/span&gt;
    /etc/etcd/
&lt;span class="o"&gt;}&lt;/span&gt;
root@server:~# &lt;span class="nb"&gt;mv &lt;/span&gt;etcd.service /etc/systemd/system/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Start the etcd server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@server:~# &lt;span class="o"&gt;{&lt;/span&gt;
  systemctl daemon-reload
  systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;etcd
  systemctl start etcd
&lt;span class="o"&gt;}&lt;/span&gt;
Created symlink /etc/systemd/system/multi-user.target.wants/etcd.service → /etc/systemd/system/etcd.service.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@server:~# etcdctl member list
6702b0a34e2cfd39, started, controller, http://127.0.0.1:2380, http://127.0.0.1:2379, &lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;etcd is up, and &lt;code&gt;member list&lt;/code&gt; confirms it's running as a single-member cluster named &lt;code&gt;controller&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Got etcd installed and running as a systemd service on the controller node, using the CA and API server certs from step 04 to secure it. Followed the guide as-is, no deviations this time.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>beginners</category>
      <category>sre</category>
    </item>
    <item>
      <title>[Lab Notes] Kubernetes the Hard Way, For Real This Time (Step 06)</title>
      <dc:creator>Luger Lex Pit-og</dc:creator>
      <pubDate>Thu, 17 Sep 2026 16:08:28 +0000</pubDate>
      <link>https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-step-06-5g19</link>
      <guid>https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-step-06-5g19</guid>
      <description>&lt;p&gt;Continuing my &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way" rel="noopener noreferrer"&gt;Kubernetes the Hard Way&lt;/a&gt; homelab build. Steps 01-05 are already done, this covers step 06.&lt;/p&gt;

&lt;p&gt;Original guide: &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/06-data-encryption-keys.md" rel="noopener noreferrer"&gt;06-data-encryption-keys.md&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Thoughts I had while doing this
&lt;/h2&gt;

&lt;p&gt;This step was pretty straightforward, just generate an encryption key and an encryption config file. But it did make me think back to step 04, where I set up the TLS/CA cert. Aren't both steps just "setting up encryption"? Kind of, but the difference is &lt;em&gt;where&lt;/em&gt; the encryption applies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step 04 (TLS/CA) secures data &lt;strong&gt;in transit&lt;/strong&gt; - traffic moving between Kubernetes components over the network.&lt;/li&gt;
&lt;li&gt;Step 06 (encryption key) secures data &lt;strong&gt;at rest&lt;/strong&gt; - specifically, Secrets stored in etcd(its like the db of kubernetes), so they're not sitting there in plain text on disk.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The encryption key
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ENCRYPTION_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 32 /dev/urandom | &lt;span class="nb"&gt;base64&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The encryption config file
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;envsubst &amp;lt; configs/encryption-config.yaml &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;  &amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;encryption-config.yaml
&lt;span class="gp"&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;scp encryption-config.yaml root@server:~/
&lt;span class="go"&gt;encryption-config.yaml
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Generated a random 32-byte encryption key and used it to fill in &lt;code&gt;encryption-config.yaml&lt;/code&gt;, then copied that config to the controller. This is what encrypts Kubernetes Secrets at rest in etcd - separate from the TLS setup in step 04, which only covers data in transit.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>proxmox</category>
      <category>beginners</category>
      <category>devops</category>
    </item>
    <item>
      <title>[Lab Notes] Kubernetes the Hard Way, For Real This Time (Step 05)</title>
      <dc:creator>Luger Lex Pit-og</dc:creator>
      <pubDate>Wed, 16 Sep 2026 21:45:37 +0000</pubDate>
      <link>https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-step-05-3887</link>
      <guid>https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-step-05-3887</guid>
      <description>&lt;p&gt;Continuing my &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way" rel="noopener noreferrer"&gt;Kubernetes the Hard Way&lt;/a&gt; homelab build. Steps 1-4 are already done, this is for step 05.&lt;/p&gt;

&lt;p&gt;Original guide: &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/05-kubernetes-configuration-files.md" rel="noopener noreferrer"&gt;05-kubernetes-configuration-files.md&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Thoughts I had while doing this
&lt;/h2&gt;

&lt;p&gt;This step is about creating kubeconfigs, basically the &lt;code&gt;.conf&lt;/code&gt; files each Kubernetes service uses to know how to connect. Each service (scheduler, proxy, etc.) gets its own kubeconfig, and each one bundles in the cert files from step 04 so the connection to the API server (the control node) is authenticated and encrypted.&lt;/p&gt;

&lt;h2&gt;
  
  
  A gotcha along the way
&lt;/h2&gt;

&lt;p&gt;Since I'm on Ubuntu instead of the guide's recommended Debian, I keep running into small differences. This step's version: my &lt;code&gt;/etc/hosts&lt;/code&gt; changes from step 03 weren't surviving a reboot. Turned out Ubuntu's cloud-init manages &lt;code&gt;/etc/hosts&lt;/code&gt; by default and quietly overwrites it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Your system has configured 'manage_etc_hosts' as True.
# As a result, if you wish for changes to this file to persist
# then you will need to either
# a.) make changes to the master file in /etc/cloud/templates/hosts.debian.tmpl
# b.) change or remove the value of 'manage_etc_hosts' in
#     /etc/cloud/cloud.cfg or cloud-config from user-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fixed it by updating the cloud-init template with the Kubernetes hostname entries instead of &lt;code&gt;/etc/hosts&lt;/code&gt; directly:&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;# edited /etc/cloud/templates/hosts.debian.tmpl to include the Kubernetes host entries&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;cloud-init clean &lt;span class="nt"&gt;--logs&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That made the entries persist across reboots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now here's the actual step.
&lt;/h2&gt;

&lt;p&gt;Quick TLDR though: I just followed the guide to generate the kubeconfigs, and they worked as expected. Let me take a shot at explaining what these commands are actually doing.&lt;/p&gt;

&lt;p&gt;Essentially, there are 4 commands run for each kubeconfig file, and all four are just pointed at that one file. Think of the kubeconfig as a plain text file, these commands just append to or edit pieces of it. That's really all that's happening:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;set-cluster&lt;/strong&gt;: adds the API server's URL and the CA cert, so the client knows who it's talking to and can verify it's really the API server.&lt;br&gt;
&lt;strong&gt;set-credentials&lt;/strong&gt;: adds the client's own cert and key, so the client can prove who it is to the API server.&lt;br&gt;
&lt;strong&gt;set-context&lt;/strong&gt;: bundles a cluster and a set of credentials together under a named context, so "use this identity to talk to this cluster" becomes one reusable label instead of two separate things. Basically, it just takes the outputs of set-cluster and set-credentials and bundles them together, that's really all set-context is doing.&lt;br&gt;
&lt;strong&gt;use-context&lt;/strong&gt;: sets that context as the active/default one, so any command using this kubeconfig automatically uses that cluster + credentials pair without you specifying them every time. Basically, you're just telling the kubeconfig "use the context I created in set-context as the default whenever a client loads this file."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The kubelet kubeconfig
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="k"&gt;for &lt;/span&gt;host &lt;span class="k"&gt;in &lt;/span&gt;node-0 node-1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;kubectl config set-cluster kubernetes-the-hard-way &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--certificate-authority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--embed-certs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://server.kubernetes.local:6443 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.kubeconfig

  kubectl config set-credentials system:node:&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--client-certificate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.crt &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--client-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.key &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--embed-certs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.kubeconfig

  kubectl config set-context default &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kubernetes-the-hard-way &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;system:node:&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.kubeconfig

  kubectl config use-context default &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.kubeconfig
&lt;span class="k"&gt;done
&lt;/span&gt;Cluster &lt;span class="s2"&gt;"kubernetes-the-hard-way"&lt;/span&gt; set.
User &lt;span class="s2"&gt;"system:node:node-0"&lt;/span&gt; set.
Context &lt;span class="s2"&gt;"default"&lt;/span&gt; created.
Switched to context &lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Cluster &lt;span class="s2"&gt;"kubernetes-the-hard-way"&lt;/span&gt; set.
User &lt;span class="s2"&gt;"system:node:node-1"&lt;/span&gt; set.
Context &lt;span class="s2"&gt;"default"&lt;/span&gt; created.
Switched to context &lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The kube-proxy kubeconfig
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="o"&gt;{&lt;/span&gt;
  kubectl config set-cluster kubernetes-the-hard-way &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--certificate-authority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--embed-certs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://server.kubernetes.local:6443 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-proxy.kubeconfig

  kubectl config set-credentials system:kube-proxy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--client-certificate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-proxy.crt &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--client-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-proxy.key &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--embed-certs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-proxy.kubeconfig

  kubectl config set-context default &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kubernetes-the-hard-way &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;system:kube-proxy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-proxy.kubeconfig

  kubectl config use-context default &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-proxy.kubeconfig
&lt;span class="o"&gt;}&lt;/span&gt;
Cluster &lt;span class="s2"&gt;"kubernetes-the-hard-way"&lt;/span&gt; set.
User &lt;span class="s2"&gt;"system:kube-proxy"&lt;/span&gt; set.
Context &lt;span class="s2"&gt;"default"&lt;/span&gt; created.
Switched to context &lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The kube-controller-manager kubeconfig
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="o"&gt;{&lt;/span&gt;
  kubectl config set-cluster kubernetes-the-hard-way &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--certificate-authority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--embed-certs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://server.kubernetes.local:6443 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-controller-manager.kubeconfig

  kubectl config set-credentials system:kube-controller-manager &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--client-certificate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-controller-manager.crt &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--client-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-controller-manager.key &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--embed-certs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-controller-manager.kubeconfig

  kubectl config set-context default &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kubernetes-the-hard-way &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;system:kube-controller-manager &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-controller-manager.kubeconfig

  kubectl config use-context default &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-controller-manager.kubeconfig
&lt;span class="o"&gt;}&lt;/span&gt;
Cluster &lt;span class="s2"&gt;"kubernetes-the-hard-way"&lt;/span&gt; set.
User &lt;span class="s2"&gt;"system:kube-controller-manager"&lt;/span&gt; set.
Context &lt;span class="s2"&gt;"default"&lt;/span&gt; created.
Switched to context &lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The kube-scheduler kubeconfig
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="o"&gt;{&lt;/span&gt;
  kubectl config set-cluster kubernetes-the-hard-way &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--certificate-authority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--embed-certs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://server.kubernetes.local:6443 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-scheduler.kubeconfig

  kubectl config set-credentials system:kube-scheduler &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--client-certificate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-scheduler.crt &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--client-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-scheduler.key &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--embed-certs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-scheduler.kubeconfig

  kubectl config set-context default &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kubernetes-the-hard-way &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;system:kube-scheduler &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-scheduler.kubeconfig

  kubectl config use-context default &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-scheduler.kubeconfig
&lt;span class="o"&gt;}&lt;/span&gt;
Cluster &lt;span class="s2"&gt;"kubernetes-the-hard-way"&lt;/span&gt; set.
User &lt;span class="s2"&gt;"system:kube-scheduler"&lt;/span&gt; set.
Context &lt;span class="s2"&gt;"default"&lt;/span&gt; created.
Switched to context &lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The admin kubeconfig
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="o"&gt;{&lt;/span&gt;
  kubectl config set-cluster kubernetes-the-hard-way &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--certificate-authority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--embed-certs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://127.0.0.1:6443 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin.kubeconfig

  kubectl config set-credentials admin &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--client-certificate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin.crt &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--client-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin.key &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--embed-certs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin.kubeconfig

  kubectl config set-context default &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kubernetes-the-hard-way &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin.kubeconfig

  kubectl config use-context default &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin.kubeconfig
&lt;span class="o"&gt;}&lt;/span&gt;
Cluster &lt;span class="s2"&gt;"kubernetes-the-hard-way"&lt;/span&gt; set.
User &lt;span class="s2"&gt;"admin"&lt;/span&gt; set.
Context &lt;span class="s2"&gt;"default"&lt;/span&gt; created.
Switched to context &lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Kubeconfig files generated
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ltr&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.kubeconfig
&lt;span class="nt"&gt;-rw-------&lt;/span&gt; 1 root root 10157 Sep 16 20:59 node-0.kubeconfig
&lt;span class="nt"&gt;-rw-------&lt;/span&gt; 1 root root 10161 Sep 16 20:59 node-1.kubeconfig
&lt;span class="nt"&gt;-rw-------&lt;/span&gt; 1 root root 10187 Sep 16 21:14 kube-proxy.kubeconfig
&lt;span class="nt"&gt;-rw-------&lt;/span&gt; 1 root root 10305 Sep 16 21:14 kube-controller-manager.kubeconfig
&lt;span class="nt"&gt;-rw-------&lt;/span&gt; 1 root root 10231 Sep 16 21:14 kube-scheduler.kubeconfig
&lt;span class="nt"&gt;-rw-------&lt;/span&gt; 1 root root  9953 Sep 16 21:14 admin.kubeconfig
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Distribute the Kubernetes configuration files
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="k"&gt;for &lt;/span&gt;host &lt;span class="k"&gt;in &lt;/span&gt;node-0 node-1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;ssh root@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="s2"&gt;"mkdir -p /var/lib/{kube-proxy,kubelet}"&lt;/span&gt;

  scp kube-proxy.kubeconfig &lt;span class="se"&gt;\&lt;/span&gt;
    root@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:/var/lib/kube-proxy/kubeconfig

  scp &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.kubeconfig &lt;span class="se"&gt;\&lt;/span&gt;
    root@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:/var/lib/kubelet/kubeconfig
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Step 05 built a kubeconfig for every cluster component that needs to talk to the API server, kubelet on each worker, kube-proxy, kube-controller-manager, kube-scheduler, and admin. Each one embedding the right cert/key from step 04 so the connection is authenticated. Also patched a Ubuntu-specific gotcha where cloud-init was silently reverting &lt;code&gt;/etc/hosts&lt;/code&gt; changes on reboot.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>proxmox</category>
      <category>beginners</category>
      <category>devops</category>
    </item>
    <item>
      <title>[Lab Notes] Kubernetes the Hard Way, For Real This Time (Step 04)</title>
      <dc:creator>Luger Lex Pit-og</dc:creator>
      <pubDate>Sun, 13 Sep 2026 20:22:17 +0000</pubDate>
      <link>https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-step-04-3cc0</link>
      <guid>https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-step-04-3cc0</guid>
      <description>&lt;p&gt;Continuing my &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way" rel="noopener noreferrer"&gt;Kubernetes the Hard Way&lt;/a&gt; homelab build on Proxmox. Steps 01-03 are already done, this covers step 04.&lt;/p&gt;

&lt;p&gt;Original guide: &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/04-certificate-authority.md" rel="noopener noreferrer"&gt;04-certificate-authority.md&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Thoughts I had while doing this
&lt;/h2&gt;

&lt;p&gt;Before the actual step though, here are some stuff I learned or was thinking about along the way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes is starting to feel like a miniaturized version of an entire IT infrastructure. Compute already got abstracted from physical servers to containers. It's like Kubernetes took that same idea and asked "hey we already abstracted compute, why not abstract the whole infrastructure?"&lt;/li&gt;
&lt;li&gt;Generating and installing all these certs felt exactly like setting up TLS between a bunch of servers so they can trust each other. Every Kubernetes component (API server, kubelet, etc.) is like another service on the network, and they all need to trust each other the same way any two servers would.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anyways, here's Step 04:&lt;/p&gt;

&lt;h2&gt;
  
  
  Certificate Authority
&lt;/h2&gt;

&lt;p&gt;Generated the CA key and self-signed CA certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="o"&gt;{&lt;/span&gt;
  openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; ca.key 4096
  openssl req &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-sha512&lt;/span&gt; &lt;span class="nt"&gt;-noenc&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-key&lt;/span&gt; ca.key &lt;span class="nt"&gt;-days&lt;/span&gt; 3653 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-config&lt;/span&gt; ca.conf &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-out&lt;/span&gt; ca.crt
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Working directory at this point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ltr&lt;/span&gt;
&lt;span class="go"&gt;total 72
drwxr-xr-x 2 root root  4096 Sep  7 03:47 units
-rw-r--r-- 1 root root  2624 Sep  7 03:47 README.md
-rw-r--r-- 1 root root 11358 Sep  7 03:47 LICENSE
-rw-r--r-- 1 root root   839 Sep  7 03:47 downloads-arm64.txt
-rw-r--r-- 1 root root   839 Sep  7 03:47 downloads-amd64.txt
drwxr-xr-x 2 root root  4096 Sep  7 03:47 docs
-rw-r--r-- 1 root root   407 Sep  7 03:47 COPYRIGHT.md
-rw-r--r-- 1 root root  1059 Sep  7 03:47 CONTRIBUTING.md
drwxr-xr-x 2 root root  4096 Sep  7 03:47 configs
-rw-r--r-- 1 root root  5863 Sep  7 03:47 ca.conf
drwxr-xr-x 6 root root  4096 Sep  7 03:59 downloads
-rw-r--r-- 1 root root   163 Sep 10 04:05 machines.txt
-rw-r--r-- 1 root root   162 Sep 10 04:16 hosts
-rw------- 1 root root  3272 Sep 11 03:39 ca.key
-rw-r--r-- 1 root root  1899 Sep 11 03:39 ca.crt
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No deviations here, followed the guide exactly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create client and server certificates
&lt;/h2&gt;

&lt;p&gt;Generated a key, CSR, and signed cert for each Kubernetes component in one loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="nv"&gt;certs&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
  &lt;span class="s2"&gt;"admin"&lt;/span&gt; &lt;span class="s2"&gt;"node-0"&lt;/span&gt; &lt;span class="s2"&gt;"node-1"&lt;/span&gt;
  &lt;span class="s2"&gt;"kube-proxy"&lt;/span&gt; &lt;span class="s2"&gt;"kube-scheduler"&lt;/span&gt;
  &lt;span class="s2"&gt;"kube-controller-manager"&lt;/span&gt;
  &lt;span class="s2"&gt;"kube-api-server"&lt;/span&gt;
  &lt;span class="s2"&gt;"service-accounts"&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;certs&lt;/span&gt;&lt;span class="p"&gt;[*]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.key"&lt;/span&gt; 4096

  openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.key"&lt;/span&gt; &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-config&lt;/span&gt; &lt;span class="s2"&gt;"ca.conf"&lt;/span&gt; &lt;span class="nt"&gt;-section&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-out&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.csr"&lt;/span&gt;

  openssl x509 &lt;span class="nt"&gt;-req&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 3653 &lt;span class="nt"&gt;-in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.csr"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-copy_extensions&lt;/span&gt; copyall &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-CA&lt;/span&gt; &lt;span class="s2"&gt;"ca.crt"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-CAkey&lt;/span&gt; &lt;span class="s2"&gt;"ca.key"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-CAcreateserial&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-out&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.crt"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirmed all the expected files were generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.crt &lt;span class="k"&gt;*&lt;/span&gt;.key &lt;span class="k"&gt;*&lt;/span&gt;.csr
&lt;span class="go"&gt;admin.crt
admin.csr
admin.key
ca.crt
ca.key
kube-api-server.crt
kube-api-server.csr
kube-api-server.key
kube-controller-manager.crt
kube-controller-manager.csr
kube-controller-manager.key
kube-proxy.crt
kube-proxy.csr
kube-proxy.key
kube-scheduler.crt
kube-scheduler.csr
kube-scheduler.key
node-0.crt
node-0.csr
node-0.key
node-1.crt
node-1.csr
node-1.key
service-accounts.crt
service-accounts.csr
service-accounts.key
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also no deviations here, straight from the guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distribute the client and server certificates
&lt;/h2&gt;

&lt;p&gt;Copied the CA cert plus each worker's own cert/key to the right nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="k"&gt;for &lt;/span&gt;host &lt;span class="k"&gt;in &lt;/span&gt;node-0 node-1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;ssh root@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; /var/lib/kubelet/

  scp ca.crt root@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:/var/lib/kubelet/

  scp &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.crt &lt;span class="se"&gt;\&lt;/span&gt;
    root@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:/var/lib/kubelet/kubelet.crt

  scp &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.key &lt;span class="se"&gt;\&lt;/span&gt;
    root@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:/var/lib/kubelet/kubelet.key
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Got a "directory already exists" warning on both nodes, but the file copies all succeeded, so no issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;mkdir: cannot create directory ‘/var/lib/kubelet/’: File exists
ca.crt        100% 1899     3.1MB/s   00:00
node-0.crt    100% 2147     3.9MB/s   00:00
node-0.key    100% 3268     6.7MB/s   00:00
mkdir: cannot create directory ‘/var/lib/kubelet/’: File exists
ca.crt        100% 1899     2.8MB/s   00:00
node-1.crt    100% 2147     3.1MB/s   00:00
node-1.key    100% 3272     5.1MB/s   00:00
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then copied the CA, API server, and service-account key/cert pairs to the controller (&lt;code&gt;server&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;scp &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  ca.key ca.crt \
  kube-api-server.key kube-api-server.crt \
  service-accounts.key service-accounts.crt \
  root@server:~/
ca.key                  100% 3272     4.5MB/s   00:00
ca.crt                  100% 1899     5.0MB/s   00:00
kube-api-server.key     100% 3272     8.2MB/s   00:00
kube-api-server.crt     100% 2354     5.0MB/s   00:00
service-accounts.key    100% 3272     7.7MB/s   00:00
service-accounts.crt    100% 2004     3.9MB/s   00:00
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Step 04 built out the cluster's PKI: a self-signed CA, then a key/cert pair for every component (admin, both worker nodes, kube-proxy, kube-scheduler, kube-controller-manager, kube-api-server, and service-accounts), distributed to the right machines.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>proxmox</category>
      <category>beginners</category>
      <category>devops</category>
    </item>
    <item>
      <title>[Lab Notes] Kubernetes the Hard Way, For Real This Time (Step 03)</title>
      <dc:creator>Luger Lex Pit-og</dc:creator>
      <pubDate>Thu, 10 Sep 2026 16:04:45 +0000</pubDate>
      <link>https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-step-03-3bc2</link>
      <guid>https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-step-03-3bc2</guid>
      <description>&lt;p&gt;Continuing my &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way" rel="noopener noreferrer"&gt;Kubernetes the Hard Way&lt;/a&gt; homelab build on Proxmox. Prerequisites and jumpbox setup (&lt;a href="https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-steps-01-02-nog"&gt;steps 01-02&lt;/a&gt;) are already done, this covers step 03 only.&lt;/p&gt;

&lt;p&gt;Steps 01 and 02 were straightforward. Well, this one was a reminder that I'm doing this manually. No scripts or AI to automate all of the setup for me. Just me and the guide. And man, even if you have a guide but if you needed to tweak something in your setup. A whole bunch of problems pop up. It goes something like: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Guide says do it this way, but I'm using a different tool, so I do it this way instead. Now something's broken because that tool needed extra config. Now the next step in the guide needs adjusting too."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's like a row of light switches wired together. Flip one, and a few others flip with it. Now you've got to figure out which ones to switch back. But hey, thats exactly how Ops/Infra work is done(to me at least).&lt;/p&gt;

&lt;p&gt;Anyway, here's how it went.&lt;/p&gt;

&lt;h2&gt;
  
  
  Machine database
&lt;/h2&gt;

&lt;p&gt;Created &lt;code&gt;machines.txt&lt;/code&gt; on the jumpbox, listing IP, FQDN, hostname, and pod subnet for each node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="nb"&gt;cat &lt;/span&gt;machines.txt
XXX.XXX.XXX.XXX k8s-solo.home.lab k8s-solo
XXX.XXX.XXX.XXX k8s-solo-w1.home.lab k8s-solo-w1 XXX.XXX.XXX.XXX/24
XXX.XXX.XXX.XXX k8s-solo-w2.home.lab k8s-solo-w2 XXX.XXX.XXX.XXX/24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(IPs are masked cuz I'm this 🤏 close to wearing a tin foil hat... even if they're internal IPs)&lt;/p&gt;

&lt;h2&gt;
  
  
  SSH access
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Enabling root login:&lt;/strong&gt; the guide assumes Debian, where root SSH login just needs to be turned on. On Ubuntu, root login is disabled by default too, so the fix was the same. Just edit &lt;code&gt;sshd_config&lt;/code&gt; and restart the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ubuntu@k8s-solo:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/^#*PermitRootLogin.*/PermitRootLogin yes/'&lt;/span&gt; /etc/ssh/sshd_config
ubuntu@k8s-solo:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Generating and distributing keys:&lt;/strong&gt; I originally planned to use root like the guide suggests, but kept hitting a &lt;code&gt;Please login as the user "ubuntu" rather than the user "root" prompt on every node&lt;/code&gt;. I didn't bother digging into it right away, so it wasn't until the end of this step that I actually tracked down why. Ubuntu's cloud images bake a forced command into /root/.ssh/authorized_keys that blocks direct root SSH login and prints that message instead. Rather than fight it, I just switched to the ubuntu user for all admin access. So keys were copied to ubuntu@ on each node instead of &lt;a href="mailto:root@"&gt;root@&lt;/a&gt;. Also used a dedicated keypair instead of overwriting any existing id_rsa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# ssh-keygen
Enter file &lt;span class="k"&gt;in &lt;/span&gt;which to save the key &lt;span class="o"&gt;(&lt;/span&gt;/root/.ssh/id_rsa&lt;span class="o"&gt;)&lt;/span&gt;: /root/.ssh/id_rsa_kthw

root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;IP FQDN HOST SUBNET&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;ssh-copy-id &lt;span class="nt"&gt;-i&lt;/span&gt; /root/.ssh/id_rsa_kthw.pub ubuntu@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; machines.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verified access to all three nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;IP FQDN HOST SUBNET&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;ssh &lt;span class="nt"&gt;-n&lt;/span&gt; ubuntu@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nb"&gt;hostname
&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; machines.txt
k8s-solo
k8s-solo-w1
k8s-solo-w2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hostnames
&lt;/h2&gt;

&lt;p&gt;The guide sets each machine's hostname to match its role: &lt;code&gt;server&lt;/code&gt;, &lt;code&gt;node-0&lt;/code&gt;, &lt;code&gt;node-1&lt;/code&gt;. I updated &lt;code&gt;machines.txt&lt;/code&gt; accordingly and ran the rename commands against the &lt;code&gt;ubuntu&lt;/code&gt; user instead of &lt;code&gt;root&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;IP FQDN HOST SUBNET&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sudo sed -i 's/^127.0.1.1.*/127.0.1.1&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FQDN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/' /etc/hosts"&lt;/span&gt;
    ssh &lt;span class="nt"&gt;-n&lt;/span&gt; ubuntu@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CMD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    ssh &lt;span class="nt"&gt;-n&lt;/span&gt; ubuntu@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;hostnamectl set-hostname &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
    ssh &lt;span class="nt"&gt;-n&lt;/span&gt; ubuntu@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart systemd-hostnamed
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; machines.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;IP FQDN HOST SUBNET&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;ssh &lt;span class="nt"&gt;-n&lt;/span&gt; ubuntu@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nb"&gt;hostname&lt;/span&gt; &lt;span class="nt"&gt;--fqdn&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; machines.txt
server.kubernetes.local
node-0.kubernetes.local
node-1.kubernetes.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;From this point on, the nodes are addressed as &lt;code&gt;server&lt;/code&gt;, &lt;code&gt;node-0&lt;/code&gt;, and &lt;code&gt;node-1&lt;/code&gt; per the guide's convention. Not the earlier &lt;code&gt;k8s-solo&lt;/code&gt; / &lt;code&gt;k8s-solo-w1&lt;/code&gt; / &lt;code&gt;k8s-solo-w2&lt;/code&gt; names.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Host lookup table
&lt;/h2&gt;

&lt;p&gt;No surprises here, followed the guide as-is. Built a &lt;code&gt;hosts&lt;/code&gt; file on the jumpbox and appended it to the jumpbox's own &lt;code&gt;/etc/hosts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"# Kubernetes The Hard Way"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; hosts
root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;IP FQDN HOST SUBNET&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FQDN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; hosts
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; machines.txt
root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="nb"&gt;cat &lt;/span&gt;hosts &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verified hostname resolution works locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="k"&gt;for &lt;/span&gt;host &lt;span class="k"&gt;in &lt;/span&gt;server node-0 node-1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;ssh ubuntu@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;server
node-0
node-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding &lt;code&gt;/etc/hosts&lt;/code&gt; entries to the remote machines
&lt;/h2&gt;

&lt;p&gt;Had to adjust the guide's command here too, same reason as before (&lt;code&gt;ubuntu&lt;/code&gt; user instead of &lt;code&gt;root&lt;/code&gt;), plus a gotcha with &lt;code&gt;sudo&lt;/code&gt; and output redirection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way# &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;IP FQDN HOST SUBNET&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;scp hosts ubuntu@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:~/
  ssh &lt;span class="nt"&gt;-n&lt;/span&gt; ubuntu@&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"sudo sh -c 'cat hosts &amp;gt;&amp;gt; /etc/hosts'"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; machines.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Interesting nugget of info I learned here:&lt;/strong&gt; &lt;code&gt;sudo somecommand &amp;gt;&amp;gt; file&lt;/code&gt; doesn't work the way you'd expect, &lt;code&gt;sudo&lt;/code&gt; only elevates the command itself, not the &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; redirection. The redirection is set up by your &lt;em&gt;current&lt;/em&gt; shell before &lt;code&gt;sudo&lt;/code&gt; ever runs, so it still tries to write as your regular user. Wrapping the whole thing in &lt;code&gt;sudo sh -c '...'&lt;/code&gt; fixes it, since now the shell doing the redirecting is the one running as root.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Step 03 set up the actual infrastructure: root/admin SSH access between the jumpbox and all three nodes, and consistent hostnames (&lt;code&gt;server&lt;/code&gt;, &lt;code&gt;node-0&lt;/code&gt;, &lt;code&gt;node-1&lt;/code&gt;) so every machine can address the others by name instead of IP.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>proxmox</category>
      <category>beginners</category>
      <category>devops</category>
    </item>
    <item>
      <title>[Lab Notes] Kubernetes the Hard Way, For Real This Time (Steps 01-02)</title>
      <dc:creator>Luger Lex Pit-og</dc:creator>
      <pubDate>Mon, 07 Sep 2026 19:27:17 +0000</pubDate>
      <link>https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-steps-01-02-nog</link>
      <guid>https://dev.to/lugerlogic/lab-notes-kubernetes-the-hard-way-for-real-this-time-steps-01-02-nog</guid>
      <description>&lt;p&gt;Following &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way" rel="noopener noreferrer"&gt;Kelsey Hightower's Kubernetes the Hard Way&lt;/a&gt;, but on my own Proxmox homelab. These are my notes on what I did and what's different from the original guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm doing this
&lt;/h2&gt;

&lt;p&gt;As a DevOps engineer, I wanted to actually understand Kubernetes instead of just knowing the commands. My real question going in was: why do we even need Kubernetes? If containers already package and run our apps, isn't that enough?&lt;/p&gt;

&lt;p&gt;My first attempt was to just spin up a cluster with AI/scripts doing most of the work. I provisioned a 1+2 node k3s cluster on this same Proxmox homelab:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;k3sup&lt;/code&gt; and &lt;code&gt;kubectl&lt;/code&gt; installed on a WSL workstation&lt;/li&gt;
&lt;li&gt;k3s control plane running on &lt;code&gt;k3s-cp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Both worker nodes joined and healthy&lt;/li&gt;
&lt;li&gt;Cilium CNI v1.19.3 installed, all 3 nodes Ready&lt;/li&gt;
&lt;li&gt;kubeconfig merged and working on the WSL workstation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It worked. But I didn't understand jack of what I built. So I scrapped that and started over with Kubernetes the Hard Way instead, building each piece by hand.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Host&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;OS&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UbuntuDesktop-Sandbox1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jumpbox&lt;/td&gt;
&lt;td&gt;Ubuntu 22.04 LTS (Jammy)&lt;/td&gt;
&lt;td&gt;16GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;k8s-solo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controller&lt;/td&gt;
&lt;td&gt;Ubuntu Server 22.04.5 LTS&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;k8s-solo-w1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Worker 1&lt;/td&gt;
&lt;td&gt;Ubuntu Server 22.04.5 LTS&lt;/td&gt;
&lt;td&gt;2 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;k8s-solo-w2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Worker 2&lt;/td&gt;
&lt;td&gt;Ubuntu Server 22.04.5 LTS&lt;/td&gt;
&lt;td&gt;2 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All VMs run on Proxmox. The jumpbox is an existing Ubuntu sandbox VM I already had, repurposed for this project. Static IPs and hostnames are set for all four VMs, and SSH keys are already set up from the jumpbox to all three nodes.&lt;/p&gt;

&lt;p&gt;Topology is 1 controller + 2 workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  01 - Prerequisites
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kubectl&lt;/code&gt; is installed on the jumpbox:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way/downloads#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl version &lt;span class="nt"&gt;--client&lt;/span&gt;
&lt;span class="go"&gt;Client Version: v1.32.3
Kustomize Version: v5.5.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  02 - Jumpbox
&lt;/h2&gt;

&lt;p&gt;Repo cloned to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;
&lt;span class="go"&gt;/root/kubernetes-the-hard-way
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Binaries downloaded into &lt;code&gt;downloads/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@luger-VirtualBox:~/kubernetes-the-hard-way/downloads#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ltr&lt;/span&gt;
&lt;span class="go"&gt;total 16
drwxr-xr-x 2 root root 4096 Jan  7  2025 cni-plugins
drwxr-xr-x 2 root root 4096 Sep  7 03:59 controller
drwxr-xr-x 2 root root 4096 Sep  7 03:59 client
drwxr-xr-x 2 root root 4096 Sep  7 03:59 worker
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SSH from the jumpbox to &lt;code&gt;k8s-solo&lt;/code&gt;, &lt;code&gt;k8s-solo-w1&lt;/code&gt;, and &lt;code&gt;k8s-solo-w2&lt;/code&gt; is already working with key-based auth, no passwords.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>proxmox</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
    <item>
      <title>[Quick Notes] "Docker-fying" my Portfolio Website</title>
      <dc:creator>Luger Lex Pit-og</dc:creator>
      <pubDate>Thu, 03 Sep 2026 19:31:04 +0000</pubDate>
      <link>https://dev.to/lugerlogic/quick-notes-docker-fying-my-portfolio-website-anc</link>
      <guid>https://dev.to/lugerlogic/quick-notes-docker-fying-my-portfolio-website-anc</guid>
      <description>&lt;p&gt;&lt;em&gt;Notes to my future self, in plain English.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Was Trying To Do
&lt;/h2&gt;

&lt;p&gt;I already had my portfolio website working on my computer using Hugo and a template for the design. I wanted to package it up in Docker so I could run it the same way anywhere, instead of relying on my machine's setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 1: Hugo Doesn't Actually "Run" a Website
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When I ran &lt;code&gt;hugo server&lt;/code&gt; on my computer, it felt like a website was "running." But that command is really just a &lt;strong&gt;preview tool&lt;/strong&gt; for editing. It's not meant to be used for real, live traffic.&lt;/li&gt;
&lt;li&gt;The command &lt;code&gt;hugo --minify&lt;/code&gt; does something different: it takes all my content and the template and &lt;strong&gt;converts it into plain files&lt;/strong&gt; — HTML, CSS, images — and dumps them in a folder called &lt;code&gt;public/&lt;/code&gt;. Then it stops. Nothing is left running. It's like exporting a Word doc to PDF — a one-time conversion, not an app that stays open.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a real website needs two separate things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Something to &lt;strong&gt;build&lt;/strong&gt; the files (Hugo).&lt;/li&gt;
&lt;li&gt;Something to &lt;strong&gt;serve&lt;/strong&gt; those files to visitors (a web server — I used nginx).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's why my Docker setup has two stages.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 2: The Two-Stage Dockerfile
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;hugomods/hugo:exts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /src&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;hugo &lt;span class="nt"&gt;--minify&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nginx:alpine&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /src/public /usr/share/nginx/html&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 80&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["nginx", "-g", "daemon off;"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of it as two mini-containers glued together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First part:&lt;/strong&gt; a temporary workspace with Hugo installed. It builds my site into plain files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Second part:&lt;/strong&gt; a clean, lightweight container with just nginx (the web server). It grabs the finished files from the first part and throws away everything else — no Hugo, no build tools, just the website files and something to hand them out.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lesson 3: My Template Needed an Extra Build Step
&lt;/h2&gt;

&lt;p&gt;My first build failed. After digging in, the real problem was simple: my Hugo template also needed some JavaScript/CSS tooling (npm) to build properly, and I wasn't running that step. Without it, the template's styling code came up empty and broke the build.&lt;/p&gt;

&lt;p&gt;Fix: install Node.js in the build stage and run the extra setup commands before building the site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;hugomods/hugo:exts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /src&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; nodejs npm
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;hugo mod npm pack &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;hugo &lt;span class="nt"&gt;--minify&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nginx:alpine&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /src/public /usr/share/nginx/html&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 80&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["nginx", "-g", "daemon off;"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; if a template comes with its own JavaScript/CSS setup, I need to run that setup too — Hugo alone won't do it automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 4: Ports Are Confusing At First
&lt;/h2&gt;

&lt;p&gt;Two separate mistakes here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake A:&lt;/strong&gt; I typed &lt;code&gt;-p 80:8080&lt;/code&gt;, but Docker's format is &lt;code&gt;-p my_computer_port:container_port&lt;/code&gt;. Nginx inside the container listens on port &lt;code&gt;80&lt;/code&gt;, so the second number always has to be &lt;code&gt;80&lt;/code&gt; — no matter what port I want to use on my own machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake B:&lt;/strong&gt; Even after fixing that, port &lt;code&gt;80&lt;/code&gt; on my actual computer was already being used by something else. Easiest fix: just pick a different, unused port on my side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8888:80 portfolio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; the &lt;code&gt;EXPOSE&lt;/code&gt; line in a Dockerfile is just a note for humans — it doesn't control anything. The real listening port comes from nginx's own settings.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 5: The &lt;code&gt;/cv&lt;/code&gt; Page Redirect Bug
&lt;/h2&gt;

&lt;p&gt;Visiting &lt;code&gt;mysite.com/cv&lt;/code&gt; sent me somewhere broken.&lt;/p&gt;

&lt;p&gt;Turns out this was expected behavior gone slightly wrong: Hugo saves that page as a folder (&lt;code&gt;/cv/index.html&lt;/code&gt;), so nginx automatically redirects &lt;code&gt;/cv&lt;/code&gt; to &lt;code&gt;/cv/&lt;/code&gt;. That part's normal.&lt;/p&gt;

&lt;p&gt;The bug was that nginx was guessing the wrong port when building that redirect, it assumed port &lt;code&gt;80&lt;/code&gt; (the port &lt;em&gt;inside&lt;/em&gt; the container) instead of &lt;code&gt;8888&lt;/code&gt; (the port I was actually using on my computer). So it sent my browser to the wrong address.&lt;/p&gt;

&lt;p&gt;Fix: tell nginx not to guess the full address, just use a relative path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;absolute_redirect&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/usr/share/nginx/html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And load that file into the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; nginx.conf /etc/nginx/conf.d/default.conf&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Final Setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Dockerfile:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;hugomods/hugo:exts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /src&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; nodejs npm
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;hugo mod npm pack &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;hugo &lt;span class="nt"&gt;--minify&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nginx:alpine&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /src/public /usr/share/nginx/html&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; nginx.conf /etc/nginx/conf.d/default.conf&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 80&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["nginx", "-g", "daemon off;"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;nginx.conf:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;absolute_redirect&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/usr/share/nginx/html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;.dockerignore:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public/
resources/
.git/
node_modules/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;To run it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; portfolio &lt;span class="nb"&gt;.&lt;/span&gt;
docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8888:80 portfolio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Short Version, In Plain Words
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hugo builds a website into files. It doesn't keep a website "running" — that's nginx's job.&lt;/li&gt;
&lt;li&gt;A Docker build can use one big toolbox to build something, then hand the finished result to a small, clean container to actually serve it.&lt;/li&gt;
&lt;li&gt;Some templates need an extra JavaScript/CSS setup step — don't skip it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EXPOSE&lt;/code&gt; in a Dockerfile is just a label. The real port comes from the app's own settings.&lt;/li&gt;
&lt;li&gt;When mapping ports, the right-hand number always has to match what the app inside is really listening on.&lt;/li&gt;
&lt;li&gt;Redirects inside a container can point to the wrong port if you don't tell the web server to keep them relative.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>webdev</category>
    </item>
    <item>
      <title>[Quick Notes] Growing My Ubuntu VM's Boot Disk</title>
      <dc:creator>Luger Lex Pit-og</dc:creator>
      <pubDate>Thu, 03 Sep 2026 19:00:47 +0000</pubDate>
      <link>https://dev.to/lugerlogic/quick-notes-growing-my-ubuntu-vms-boot-disk-1d8o</link>
      <guid>https://dev.to/lugerlogic/quick-notes-growing-my-ubuntu-vms-boot-disk-1d8o</guid>
      <description>&lt;p&gt;Made the VM disk bigger and had to tell the OS to actually use the extra space. Took 3 steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Grow the disk (hypervisor side)
&lt;/h2&gt;

&lt;p&gt;Resized the virtual disk in Proxmox/VirtualBox, adding extra space (e.g. +20G). OS doesn't see it yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Grow the partition
&lt;/h2&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 &lt;span class="nb"&gt;install &lt;/span&gt;cloud-guest-utils
&lt;span class="nb"&gt;sudo &lt;/span&gt;growpart /dev/vda 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;growpart&lt;/code&gt; stretches a partition to fill new space.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;3&lt;/code&gt; is the partition number (from &lt;code&gt;lsblk&lt;/code&gt;), not part of the disk name.&lt;/li&gt;
&lt;li&gt;Ignored an unrelated &lt;code&gt;shim-signed&lt;/code&gt; error during install — didn't affect anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Grow the filesystem
&lt;/h2&gt;

&lt;p&gt;Checked for LVM first:&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;pvresize /dev/vda3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Got "command not found" → no LVM here, skip it.&lt;/p&gt;

&lt;p&gt;Then resized the filesystem directly:&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;resize2fs /dev/vda3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Use &lt;code&gt;xfs_growfs /&lt;/code&gt; instead if filesystem is XFS, not ext4.)&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Confirm
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Showed full new size. And we're done! No reboot needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check disk/partitions&lt;/th&gt;
&lt;th&gt;&lt;code&gt;lsblk&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Check filesystem usage&lt;/td&gt;
&lt;td&gt;&lt;code&gt;df -h /&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grow partition&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo growpart /dev/vda 3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grow filesystem (ext4)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo resize2fs /dev/vda3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grow filesystem (XFS)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo xfs_growfs /&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Remember
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Back up/snapshot before resizing.&lt;/li&gt;
&lt;li&gt;"Command not found" on LVM tools = you're not using LVM, that's fine.&lt;/li&gt;
&lt;li&gt;Whole thing can be done live, no reboot.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ubuntu</category>
      <category>linux</category>
      <category>sysadm</category>
      <category>ops</category>
    </item>
    <item>
      <title>[Quick Notes] RDP Into a Proxmox VM Without Opening It Up to the Network</title>
      <dc:creator>Luger Lex Pit-og</dc:creator>
      <pubDate>Thu, 03 Sep 2026 16:08:29 +0000</pubDate>
      <link>https://dev.to/lugerlogic/rdp-into-a-proxmox-vm-without-opening-it-up-to-the-network-52nj</link>
      <guid>https://dev.to/lugerlogic/rdp-into-a-proxmox-vm-without-opening-it-up-to-the-network-52nj</guid>
      <description>&lt;p&gt;I run an Ubuntu Desktop VM on my Proxmox server and wanted to RDP into it  without exposing port 3389 to my whole network. What i ended up with: &lt;strong&gt;tunnel RDP through SSH&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;I already trust my SSH connection to the Proxmox host. So instead of opening RDP up directly, I have SSH secretly carry the RDP traffic for me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Laptop] --localhost:3389--&amp;gt; [SSH tunnel] --&amp;gt; [Proxmox host] --3389--&amp;gt; [VM]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My RDP client just connects to &lt;code&gt;localhost:3389&lt;/code&gt; like normal. It has no idea SSH is quietly relaying that traffic all the way to the VM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;VM side: install and run xrdp like usual.&lt;/p&gt;

&lt;p&gt;Laptop side, add this to &lt;code&gt;~/.ssh/config&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; proxmox-rdp
    &lt;span class="k"&gt;HostName&lt;/span&gt; &amp;lt;proxmox-host-ip&amp;gt;
    &lt;span class="k"&gt;User&lt;/span&gt; &amp;lt;your-user&amp;gt;
    &lt;span class="k"&gt;LocalForward&lt;/span&gt; &lt;span class="m"&gt;3389&lt;/span&gt; &amp;lt;VM_IP&amp;gt;:3389
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;LocalForward&lt;/code&gt; line forwards local port 3389 to the VM's port 3389.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting
&lt;/h2&gt;

&lt;p&gt;1.Start the VM in Proxmox.&lt;br&gt;
2.Open the tunnel(ran on my client):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   ssh proxmox-rdp &lt;span class="nt"&gt;-N&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;-N&lt;/code&gt; = just forward the port. It'll look frozen. That's normal, just leave it open.)&lt;/p&gt;

&lt;p&gt;3.RDP to &lt;code&gt;localhost:3389&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Close that terminal, tunnel's gone, RDP stops working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Bother
&lt;/h2&gt;

&lt;p&gt;Only SSH needs to be exposed, not RDP. All the traffic rides inside SSH's encryption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch Out For
&lt;/h2&gt;

&lt;p&gt;If the VM's IP is DHCP and changes after a reboot, the &lt;code&gt;LocalForward&lt;/code&gt; line goes stale. Set a static IP or DHCP reservation to avoid this.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>proxmox</category>
      <category>ops</category>
      <category>security</category>
    </item>
    <item>
      <title>[2026] Passed the AWS Certified Security Specialty (SCS-C03)</title>
      <dc:creator>Luger Lex Pit-og</dc:creator>
      <pubDate>Wed, 02 Sep 2026 16:47:26 +0000</pubDate>
      <link>https://dev.to/lugerlogic/2026-passed-the-aws-certified-security-specialty-scs-c03-2dfa</link>
      <guid>https://dev.to/lugerlogic/2026-passed-the-aws-certified-security-specialty-scs-c03-2dfa</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Passed - Score: &lt;strong&gt;831&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&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%2Frnhyos3foiw4goo7usy2.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%2Frnhyos3foiw4goo7usy2.png" alt="Screenshot of SCS Exam Results" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A quick log of how I prepped for and passed the AWS Security Specialty exam.&lt;/p&gt;




&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DevOps/Cloud Engineer&lt;/strong&gt;, 5+ years of experience&lt;/li&gt;
&lt;li&gt;~2-3 years hands-on with AWS&lt;/li&gt;
&lt;li&gt;AWS Certs held before attempting SCS-C03:

&lt;ul&gt;
&lt;li&gt;AWS Certified Solutions Architect - Associate &lt;em&gt;(Oct 2025)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;AWS Certified CloudOps Engineer - Associate &lt;em&gt;(Jan 2026)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So yeah, I already had some XP with AWS and got 2x Associate-level certs before attempting the Security Specialty.&lt;/p&gt;




&lt;h2&gt;
  
  
  Timeline
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;~2 months, on and off&lt;/strong&gt; - Struggled a bit with staying consistent in trying to finish the Udemy Courses. Kept starting and stopping without finishing them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final 3 weeks, locked in&lt;/strong&gt; - Annoyed at my progress with the Udemy Courses, I switched entirely to using Practice Exams. Found it more interactive. This is where things actually clicked and prep became consistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; For a cert like this (with real AWS experience already backing it up), practice-exam prep was far more effective for me.&lt;/p&gt;




&lt;h2&gt;
  
  
  Study Resources
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Primary: Practice Exams
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://portal.tutorialsdojo.com/courses/aws-certified-security-specialty-practice-exams-scs-c03/" rel="noopener noreferrer"&gt;Tutorials Dojo – AWS Certified Security Specialty Practice Exams (SCS-C03)&lt;/a&gt;&lt;/strong&gt;
This was the backbone of my prep, especially in the final 3 weeks. I went ahead and did a consistent grind and review of all the questions. If I made a mistake, I took note of it. Why I was wrong, what the correct answer was. If I still didn't get it, I asked AI to dumb it down until I did. Here's my Practice Exam Logs before actually taking the exam.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Practice Exam&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Randomized Test&lt;/td&gt;
&lt;td&gt;52.31%&lt;/td&gt;
&lt;td&gt;Jul 6, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review Mode Set 1&lt;/td&gt;
&lt;td&gt;75.71%&lt;/td&gt;
&lt;td&gt;Jul 30, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review Mode Set 2&lt;/td&gt;
&lt;td&gt;65.71%&lt;/td&gt;
&lt;td&gt;Aug 1, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review Mode Set 3&lt;/td&gt;
&lt;td&gt;64.62%&lt;/td&gt;
&lt;td&gt;Aug 9, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Randomized Test&lt;/td&gt;
&lt;td&gt;89.23%&lt;/td&gt;
&lt;td&gt;Aug 9, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Randomized Test&lt;/td&gt;
&lt;td&gt;89.23%&lt;/td&gt;
&lt;td&gt;Aug 11, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timed Mode Set 2&lt;/td&gt;
&lt;td&gt;90.77%&lt;/td&gt;
&lt;td&gt;Aug 14, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timed Mode Set 1&lt;/td&gt;
&lt;td&gt;89.23%&lt;/td&gt;
&lt;td&gt;Aug 14, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Supplementary: Video Courses (targeted, not full playthroughs)
&lt;/h3&gt;

&lt;p&gt;Used these reactively. I'd just search a topic on their courses where I was weak at, then watch that. Maarek courses are a given when doing AWS Cert Preps. But Zeal Vora's course, although quite lengthy, was kinda fun to watch. The guy sometimes goes off on tangents and tells anecdotes from his industry experience and how it relates to the topic at hand.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Ultimate AWS Certified Security Specialty [NEW 2026] SCS-C03&lt;/em&gt; — Stephane Maarek&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;[NEW] AWS Certified Security Specialty 2026&lt;/em&gt; — Zeal Vora&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI-Assisted Deep Dives
&lt;/h3&gt;

&lt;p&gt;So AI obviously is a must nowadays. When I didn't get something, I'd use AI to further break it down, ask follow-up questions, get analogies, or clarify edge cases until it stuck.&lt;/p&gt;

&lt;h3&gt;
  
  
  Staying Motivated
&lt;/h3&gt;

&lt;p&gt;Every time I felt a bit down with my progress, I'd just lurk on the &lt;a href="https://www.reddit.com/r/AWSCertifications/" rel="noopener noreferrer"&gt;r/AWSCertifications&lt;/a&gt; sub. Seeing other folks in the trenches, sharing their prep, posting their pass stories, honestly hit different. Gave me that "if they can do it, so can I" push to lock back in.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Worked
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Practice exams &amp;gt; passive video watching.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Targeted topic review&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI as a tutor&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prior hands-on AWS experience&lt;/strong&gt; made a real difference.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Start the practice-exam-heavy approach earlier instead of spending the first ~6 weeks trying to grind through full video courses.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;I already had the hands-on AWS experience. I already had 2x AWS Associate Certs going in. So really, I should've just trusted that and gone straight to practice exams to find my weak spots, instead of grinding through full courses first. &lt;u&gt;This is what worked for me&lt;/u&gt;. If you're already coming in with real AWS experience, don't sleep on practice exams.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Running Tiny LLMs on Your Local Machine with vLLM + WSL2 (No Cloud Needed!)</title>
      <dc:creator>Luger Lex Pit-og</dc:creator>
      <pubDate>Thu, 03 Jul 2025 16:40:33 +0000</pubDate>
      <link>https://dev.to/lugerlogic/running-tiny-llms-on-your-local-machine-with-vllm-wsl2-no-cloud-needed-5c44</link>
      <guid>https://dev.to/lugerlogic/running-tiny-llms-on-your-local-machine-with-vllm-wsl2-no-cloud-needed-5c44</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;“Surely my gaming laptop can handle a 7B model, right?”&lt;br&gt;
— Me, before reality hit me like an OOM error.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So here’s the story: I dove into the world of local LLMs thinking my Lenovo Legion 5i Gaming Laptop—rocking 6GB of VRAM, 32GB of RAM, and a solid Intel i7 CPU could surely handle running a local LLM right? I figured, “Let’s try running Mistral-7B locally. Psssh, what could go wrong?”&lt;/p&gt;

&lt;p&gt;Everything. Everything went wrong.&lt;/p&gt;

&lt;p&gt;The moment I tried, my GPU begged for mercy, RAM usage spiked like it saw a ghost, and the model refused to even load. That's when I realized something important: bigger isn’t always better, especially when it comes to LLMs and local hardware.&lt;/p&gt;

&lt;p&gt;But hey, that failure was a blessing in disguise. It led me to the world of tiny but mighty LLMs—specifically TinyLlama—paired with the vLLM inference engine, which is optimized for performance and memory efficiency. This combo actually works on my hardware, and it might work for yours too.&lt;/p&gt;

&lt;p&gt;If you're in the same boat—curious about LLMs, want to run them locally, but don't have an RTX 4090 lying around—then this guide is for you.&lt;/p&gt;

&lt;p&gt;Let’s get into it. 👇&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Bother with Local "Tiny" vLLMs?
&lt;/h2&gt;

&lt;p&gt;Running AI models locally has a lot of benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No OpenAI/HuggingFace API costs&lt;/li&gt;
&lt;li&gt;Full control over the system&lt;/li&gt;
&lt;li&gt;Great for testing, hacking, or even building apps offline&lt;/li&gt;
&lt;li&gt;You learn a LOT more doing it this way&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But we're also being realistic here—if you're like me and you're not running an RTX 4090, you need something that fits your hardware. That's why I'm using TinyLlama—a small but capable model—and leveraging vLLM, a super-efficient inference engine built for LLMs.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠 Step-by-Step: How I Set It Up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Install WSL2&lt;/strong&gt;&lt;br&gt;
First things first, we need WSL2 set up. I didn’t go through the Microsoft Store route—just used good old command line.&lt;/p&gt;

&lt;p&gt;Open PowerShell as Administrator and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wsl --install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That installs WSL2 along with the default Ubuntu distro. If you want a specific Ubuntu version (like 20.04), you can install it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wsl --install -d Ubuntu-20.04
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reboot if necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Launch Ubuntu and Prep Your Environment&lt;/strong&gt;&lt;br&gt;
Once Ubuntu is installed, launch it via your Start Menu or run wsl from a terminal. From here, it’s just regular Linux commands.&lt;/p&gt;

&lt;p&gt;Inside Ubuntu, I did the usual system update and installed Python tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y
sudo apt install python3 python3-pip python3-venv -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I set up a Python virtual environment for this project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 -m venv vllm-env
source vllm-env/bin/activate
pip install --upgrade pip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Run vLLM with TinyLlama&lt;/strong&gt;&lt;br&gt;
Now we launch the vLLM API server with TinyLlama. This runs a local OpenAI-compatible endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python -m vllm.entrypoints.openai.api_server \
  --model TinyLlama/TinyLlama-1.1B-Chat-v1.0 \
  --host 0.0.0.0 \
  --port 8000 \
  --gpu-memory-utilization 0.7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;What these flags mean:&lt;/em&gt;&lt;br&gt;
--model: Specifies the model from Hugging Face&lt;br&gt;
--host 0.0.0.0: Exposes the API to your host machine&lt;br&gt;
--port 8000: Runs it on port 8000&lt;br&gt;
--gpu-memory-utilization 0.7: Only uses 70% of your GPU’s memory (you can tweak this)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing It&lt;/strong&gt;&lt;br&gt;
Once it’s up and running, try hitting the endpoint from your browser or a tool like Postman:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:8000/docs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use it like the OpenAI API—just pass in your prompt, model name, and get a response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;So yeah—turns out you can run LLMs locally, just don’t expect your laptop to handle a 7B model without crying. TinyLlama + vLLM is a great combo for tinkering and learning without frying your GPU.&lt;/p&gt;

&lt;p&gt;Now that it’s running, I’ll be hacking away at this black-box AI, trying to make it actually follow my prompts—especially when I ask for a specific word count (seriously, why is that so hard?).&lt;/p&gt;

&lt;p&gt;More experiments to come. Stay tuned!&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
