I wanted a persistent Kubernetes environment that was always available for platform engineering work.

A temporary cluster on my laptop would be useful for testing manifests, but it would disappear when I closed the lid. EKS provides the managed AWS environment I need for cloud-specific validation, but keeping it online for routine Kubernetes operations did not make economic sense. 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 small home server with 16 GiB of memory running Debian.

It was already always on. It was also already busy.

At the time of my retained snapshot, the server was running bunch of Docker containers. Kubernetes had to fit beside those workloads, not take over the machine. That constraint shaped the entire decision.

The Operating Model I 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 Debianserver
        ├── Existing Docker workloads
        └── Single-node Kubernetes lab
                ├── Always available
                ├── Privately administered
                └── Small enough to share the host

The home server would provide the persistent engineering environment, while EKS would remain short-lived and purpose-built for AWS-specific validation.

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 mature ecosystem, clear operational documentation, and an architecture well suited to constrained edge environments. 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%"

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 -

It then waited for the node to report that it was ready:

k3s kubectl wait \
  --for=condition=Ready \
  node --all \
  --timeout=180s

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

I verified the Service from inside the cluster:

klab -n lab exec deployment/hello-nginx -- \
  wget -qO- http://hello-nginx:80

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 now provided a persistent environment for validating controller behavior and operational workflows: 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 keep a managed control plane running for routine platform validation. The home k3s cluster provides a persistent environment for testing deployment behavior, networking, rollback,

GitOps reconciliation, and operational guardrails.

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 application rollout surfaced the next platform concern.

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.

The next milestone extends the cluster into a Git-driven delivery platform with automated reconciliation.