0. Why I Decided to Learn Kubernetes#

About one month ago, I attended an electronics fair where I discovered an irresistible deal on an HP ProDesk 400 G6 Mini PC. Although it is a basic model equipped with an Intel Core i3-10100T, it represented a significant upgrade over my existing server, which was powered by an Intel Core i5-6500T.

Despite the upgrade, I didn’t want to retire a perfectly functioning PC that was still handling my workload well. Instead, I opted for a more educational (and perhaps slightly overkill) approach: repurposing both computers to create a Kubernetes (k8s) cluster.

1. k8s Setup#

1.1 OS Choice#

Like most consumer PCs, my new ProDesk came with Windows pre-installed. I quickly replaced it with Ubuntu Server 24.04. My choice was mainly dictated by my previous experience with the OS; I didn’t want to throw a spanner in the works by opting for something I was less confident using.

1.2 Installing kubeadm#

As a proponent of the “learn by doing” philosophy, I decided to configure everything manually using kubeadm for my first encounter with k8s.

I studied the official Kubernetes introduction to better understand the k8s structure and the role of each component. Once I felt confident enough, I began following the kubeadm installation guide.

1.2.1 Prerequisites#

I didn’t encounter any issues during the initial phase. I disabled swap using swapoff -a, installed Docker via the convenience script (as it was the fastest method), and followed the post-installation steps to allow container execution as a non-root user.

1.2.2 Installation of kubeadm, kubectl, and kubelet#

These three packages are essential to the cluster, each serving a specific role:

  • kubeadm: Used to bootstrap the cluster.
  • kubelet: Runs on all nodes and manages the pods.
  • kubectl: The command-line utility used to communicate with the cluster control plane, managing node-level operations and the lifecycle of workloads.

Fortunately, the installation process is as simple as adding the Kubernetes apt repositories and signature keys. After that, I only needed to run:

sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl # To prevent these packages from being updated

At this point, everything was up and running except for the kubectl service, which I enabled with sudo systemctl enable --now kubectl.

1.3 Remove Master Taints#

Generally, Kubernetes assumes a cluster is made of multiple worker nodes and a single master node (control plane). In this configuration, it makes sense for pods to be assigned only to workers while the master focuses on cluster management. This is the default behavior when installing k8s with kubeadm.

However, since I wanted to create a single-node cluster for testing, I needed the control plane to schedule pods on itself. This can be achieved by removing the control-plane:NoSchedule taint applied to the master by default:

kubectl taint nodes <master-node-name> node-role.kubernetes.io/control-plane:NoSchedule-

Now, the cluster is fully functional, as confirmed by the STATUS: Ready output:

k8s@k8s-master ~> kubectl get nodes
NAME        STATUS   ROLES                  AGE   VERSION
k8smaster   Ready    control-plane,master   1m    v1.33.6

2. Create Deployments#

2.1 YAML files and Helm#

After completing the installation and configuration, I began exploring how Kubernetes manages pods, services, and deployments. By this point, I understood what these components were and why they were necessary, but I had no idea how to configure them through YAML files.

A more practical person likely would have researched Helm a comprehensive package manager for Kubernetes that provides ready-made manifests for almost any service and called it a day. However, wanting to learn as much as possible, I decided to forego the convenience of Helm and take matters into my own hands by writing each YAML file manually.

2.1.1 Types of YAML Files#

Hosting a service on Kubernetes is significantly more complex than on Docker, even if the base concepts are similar.

In Docker, you can simply run a docker run command or use Docker Compose to manage a whole stack (volumes, networks, and containers) in a single file. With k8s, you can still define everything in one file, but you must define many more distinct components:

  • Workloads (Deployments/Sets): This section handles container specifics (names, images, volume mounts, and ports). You can define how the containers behave using different types: Deployment (for a specific number of pods), ReplicaSet, StatefulSet, Job, or DaemonSet (useful for running a pod as a daemon on every node).
  • Services: These manage how services are exposed. You can set up load balancers between nodes using the LoadBalancer type, expose a service only within the cluster using ClusterIP, or use a specific port on each node with NodePort.
  • Secrets: These act like .env files for your deployment. As the name suggests, they contain sensitive data that, much like any grandmother’s secret recipes, should never be left as plain text.

Kubernetes introduces so many concepts that I am still studying them. For those interested in a deeper dive, here is the link to the official documentation.

2.2 My First Deployment#

Coming from the Docker Compose world, the first thing I tried to spin up was Portainer.

This was not a particularly smart move for a beginner, as Portainer requires custom permissions to work properly. It was a tough challenge that I only managed to complete by studying the official Helm charts I had promised not to use. I analyzed how they functioned and then wrote my own configuration, using k8syaml.com to help with the new syntax.

After that, I found I actually enjoyed translating Docker Compose files into Kubernetes manifests. Within a week, I had working instances of Nextcloud, Immich, Wireguard, AdGuard, Jellyfin, Jellyseerr, and Navidrome.

While I was satisfied with my progress, I eventually realized my setup was a bit of a disaster. I hadn’t properly considered persistent volumes, followed a consistent logic for exposed ports, or organized my configuration and data files to allow for easy backups.

3. The End of the Experiment#

At that point, I decided that my first cluster, which was intended only for educational purposes, was a success, even if it was far from perfect. It was time to let it go and start from scratch. My next attempt will be more organized, allowing me to focus on other aspects of management while applying everything I learned from this first experience.

RIP cluster-1: 23/11/2025 - 27/12/2025