I wanted a Kubernetes lab that would still be there the next morning.
A temporary cluster on my laptop would be useful for testing manifests, but it would disappear when I closed the lid. Amazon EKS would give me the managed AWS experience I eventually want to test, but keeping it online just so I could practise Deployments and Services did not make sense for me. The EKS control plane alone is currently priced at $0.10 per cluster-hour under standard support, before worker nodes, storage, and networking.
I already had another option: an Intel N100 home server with 16 GiB of memory running Debian and OpenMediaVault.
It was already always on. It was also already busy.
At the time of my retained snapshot, the server was running 28 Docker containers. Kubernetes had to fit beside those workloads, not take over the machine. That constraint shaped the entire decision.
The Lab I Actually Needed
My goal was not to reproduce a production cluster at home. Four CPU cores cannot provide a convincing multi-node failure lab while also protecting the services already using the server.
I needed something narrower:
Existing Debian/OpenMediaVault server
├── Existing Docker workloads
└── Single-node Kubernetes lab
├── Always available
├── Privately administered
└── Small enough to share the host
Enter fullscreen mode Exit fullscreen mode
The home server would be my persistent learning environment. EKS could come later as a short-lived environment for AWS-specific behavior such as IAM, cloud load balancers, and managed control-plane operations.
That made the selection criteria practical:
- It had to run directly on Debian.
- It had to support a useful single-node cluster.
- I needed control over the resources left for Kubernetes.
- Bundled components could not claim ports already used by the server.
- The setup had to remain close enough to Kubernetes that the manifests could move to EKS later.
Why k3s Fit the Server
I considered MicroK8s, k0s, and EKS, but k3s matched this host and the way I wanted to operate the lab.
MicroK8s has a convenient add-on workflow and is a natural choice when Ubuntu and snap are already part of the environment. k0s is also lightweight and packages Kubernetes as a single binary with a configurable architecture.
Both were reasonable options. My server was already Debian-based, and I wanted a small native service with straightforward configuration and a large body of homelab examples. k3s gave me that operating model without asking me to rebuild the host around a different workflow.
The official k3s requirements list two CPU cores and 2 GB of memory for a server node. A single-server cluster can use embedded SQLite by default, while other datastore options remain available if the topology grows later.
On this server, the installed k3s binary measured about 78.2 MiB. More important than the number was what I could configure around it: resource reservations, eviction thresholds, and the bundled components I wanted to disable.
That was the real reason I selected k3s. It was not simply “lightweight Kubernetes.” It gave me a Kubernetes control plane I could shape around a server that already had a job.
Protecting a Server That Was Already Busy
Running the k3s installer was the easy part. The real risk was forgetting that this server already had a job.
Before installing anything, I wrote a host-audit script. It records CPU, memory, swap, disk space, listening ports, systemd services, Docker containers, and recent kernel warnings. This gave me a clear picture of the server before Kubernetes touched it.
The raw audit stays outside Git because it may contain hostnames, IP addresses, mount paths, and internal service names. Only reviewed and sanitized results belong in the repository.
I also made the installation script stop when it finds something unexpected. It refuses to continue when:
- it is not running with the required privileges;
- k3s is already running;
- an existing k3s installation or configuration is present; or
- the Kubernetes API or kubelet ports are already occupied.
The k3s configuration was deliberately conservative:
write-kubeconfig-mode: "0640"
write-kubeconfig-group: "users"
disable:
- traefik
- servicelb
node-label:
- "environment=homelab"
kubelet-arg:
- "system-reserved=cpu=1,memory=6Gi"
- "kube-reserved=cpu=250m,memory=512Mi"
- "eviction-hard=memory.available<1Gi,nodefs.available<10%"
Enter fullscreen mode Exit fullscreen mode
Ports 80 and 443 were already being used by services on the server. Because the bundled Traefik ingress controller and ServiceLB also expect to use those ports, I disabled both components.
The resource settings were equally important. Kubernetes was told to leave one CPU core and 6 GiB of memory available for the host. I also allowed another 250 millicores and 512 MiB for Kubernetes itself.
These settings are not a hard wall around the existing Docker containers. They simply reduce the resources Kubernetes can assign to Pods and give the kubelet thresholds for reacting to memory or disk pressure.
That leaves less capacity for the lab, but that is intentional. On a shared four-core server, protecting the existing workload matters more than giving Kubernetes every available resource.
Installing and Checking the Cluster
Once the checks passed and the configuration was written, the script used the official k3s installer:
curl --proto '=https' --tlsv1.2 -sfL https://get.k3s.io | sh -
Enter fullscreen mode Exit fullscreen mode
It then waited for the node to report that it was ready:
k3s kubectl wait \
--for=condition=Ready \
node --all \
--timeout=180s
Enter fullscreen mode Exit fullscreen mode
I kept installation and verification separate. A successful installer only proves that the installation command completed. It does not prove that the cluster is healthy or that the rest of the server survived the change.
The verification script checks:
- the k3s systemd service;
- the installed k3s version;
- the Kubernetes node and Pods;
- the resources available to workloads; and
- whether Traefik or ServiceLB appeared unexpectedly.
The installed server was running k3s v1.36.3+k3s1. At the time of the retained snapshot:
- Kubernetes could allocate 2750 millicores of CPU to Pods;
- about 7.9 GiB of memory was allocatable to Pods;
- all six observed Kubernetes Pods were running;
- all 28 existing Docker containers were still running;
- none of the Docker containers reported being unhealthy; and
- no systemd unit had failed.
The k3s service was using approximately 726 MiB of memory, with a recorded peak of 1.19 GiB. The host still had about 9.7 GiB of available memory and 296 GiB of available disk space.
One warning remained: 834 MiB of the server’s 976 MiB swap was in use. That does not automatically mean the server was under active memory pressure, but it was something I still needed to investigate.
These measurements are a snapshot, not a performance benchmark. They do not prove that the server will remain healthy under every workload.
They answer a smaller and more useful question: after adding k3s, was the cluster running without an obvious failure in the services already sharing the machine?
At that moment, yes.
Then I Made Kubernetes Do Real Work
A ready node proves that Kubernetes started. It does not prove that I can operate an application through it.
I deployed a small Nginx workload with three replicas, readiness and liveness probes, and CPU and memory constraints. A Service gave the replaceable Pods one stable internal address, and NodePort exposed the same application over my private network.
Private client
↓
Home server:30081
↓
Kubernetes Service
↓
EndpointSlice
↓
Three Nginx Pods
Enter fullscreen mode Exit fullscreen mode
I verified the Service from inside the cluster:
klab -n lab exec deployment/hello-nginx -- \
wget -qO- http://hello-nginx:80
Enter fullscreen mode Exit fullscreen mode
The request returned the expected page and identified the Pod that served it. The same application was reachable through the private NodePort path.
I then changed the page and watched the Deployment replace the Pods through a rolling update. Kubernetes created a new ReplicaSet, scaled it up, and scaled the previous one down while the Service remained stable.
Finally, I rolled the Deployment back and reconciled the YAML with the running version. A clean kubectl diff confirmed that the source and live cluster agreed again.
That sequence mattered more to me than a successful installer message. The server was now a persistent place to practise the controller relationships I wanted to understand: Deployment to ReplicaSet, Service to EndpointSlice, rollout to rollback, and desired state to live state.
Why EKS Still Belongs in the Plan
Choosing k3s did not remove EKS from the journey. It gave EKS a more focused role.
According to the current EKS pricing page, a standard-support cluster costs $0.10 per hour before the compute and supporting AWS resources are added. That is roughly $73 for a 730-hour month for the control plane alone.
I do not need to pay that continuously while learning what a ReplicaSet does or why a Service follows labels. The home k3s cluster can hold that state and remain available every day.
When I need to verify AWS-specific behavior, I can create EKS deliberately, capture the evidence, and remove it. The local cluster handles repetition; the cloud cluster handles cloud-specific proof.
What Comes Next
The first application lab exposed the next platform problem.
I rolled Kubernetes back to Version 2, but my YAML still declared Version 3. The cluster and source disagreed, and Kubernetes alone had no reason to repair that relationship.
The next milestone is GitOps with Argo CD: make Git the declared source of truth, reproduce the drift deliberately, and observe reconciliation instead of correcting it by hand.
That is where this stops being only a Kubernetes lab and starts becoming a delivery platform.
답글 남기기