Running Kubernetes on a dedicated server gives you something that cloud-managed clusters and deployments cannot: uncontested hardware access from the first control-plane process to the last worker pod. No hypervisor layer caps your CPU scheduling. No shared tenant competes for bandwidth during a rolling deployment.
Dedicated hardware may provide more predictable resource availability, but it does not remove the single point of failure created by running the control plane and workloads on one host. This guide walks you through deploying a single-node Kubernetes cluster using kubeadm on a dedicated server running Ubuntu.
You will understand why the environment changes several default assumptions kubeadm makes, which pre-flight conditions to verify before initialization, and how to configure networking so the cluster starts cleanly and stays stable. The walkthrough covers each phase in sequence — from kernel parameter preparation through node readiness verification — so you can follow it on a freshly provisioned machine without needing to piece together separate documentation sources.
A single-node cluster is a legitimate production choice for teams that need orchestration benefits — rolling updates, declarative workload management, self-healing restarts — without the operational overhead of a multi-node topology.
Why a Dedicated Server Gives kubeadm a Reliable Foundation
A dedicated server gives kubeadm a reliable foundation because the hardware is yours alone — no hypervisor schedules CPU time across competing tenants, and no shared memory bus throttles your control-plane processes during peak load. That physical exclusivity means kubeadm's preflight checks run against the actual resources available to Kubernetes, not a capped slice of a shared host.
The initialization sequence either passes or fails based on real conditions, not on what another tenant happens to be doing at that moment. On a VPS, the gap between allocated and delivered compute can be wide enough to cause the API server to miss its readiness threshold during initialization — not because your configuration is wrong, but because the underlying host is saturated by a neighboring workload. CPU steal time is the specific metric that exposes this problem.
When a hypervisor reclaims cycles from your instance to serve another tenant, the kernel reports those cycles as stolen.
Kubernetes components — particularly etcd and the API server — are sensitive to the latency spikes that steal time produces. etcd relies on a Raft consensus protocol that requires consistent, low-latency disk writes; when steal time pushes fsync latency above roughly 500 milliseconds, heartbeat timeouts expire, leader elections fail, and the cluster can enter an unstable state before a single workload is scheduled.
You will see this in etcd logs as election timeout or failed to send out heartbeat on time entries — not as a kubeadm error, which makes the root cause difficult to diagnose on shared infrastructure. On bare metal, steal time is structurally zero because no hypervisor layer exists between your processes and the physical cores.
That structural absence is what makes kubeadm initialization predictable rather than contingent on shared-host conditions you cannot observe or control. The same advantage carries forward through every subsequent step in this walkthrough — container runtime installation, kernel parameter configuration, and kubeadm init all behave consistently because the hardware responds to your processes alone.

Before kubeadm touches a single component, it runs a preflight sequence that will halt installation the moment your live server state falls short of Kubernetes minimum thresholds for CPU, memory, or swap configuration.
How to Verify Your Server Meets the Minimum Requirements for Kubernetes
Kubernetes imposes hard minimum thresholds that kubeadm enforces through its preflight check sequence before any component is installed. Meeting those thresholds on paper is not enough — you need to verify them against the live state of your server, because a misconfigured kernel or an active swap partition will halt the process immediately.
Catching these conditions before you begin saves you from a partial installation that leaves the system in an inconsistent state. The hardware minimums are concrete: at least two CPU cores and two gigabytes of RAM are required for the control plane to initialize.
A single-core instance will fail the preflight check outright. In practice, a control plane running etcd, the API server, the scheduler, and the controller manager under real workload pressure benefits significantly from four or more cores and at least four gigabytes of RAM — the minimums represent the floor for initialization, not a comfortable operating baseline. You can confirm your core count with the nproc command and your available memory with the free command.
Both checks take seconds and should be the first thing you run after provisioning. Swap must be disabled before kubeadm will proceed. Kubernetes assumes it has full control over memory allocation; an active swap partition undermines that assumption and causes the preflight check to fail with an explicit error. Disable swap with the swapoff command and remove or comment out the relevant entry in the fstab file to prevent it from re-enabling after a reboot.
Verify the result with the swapon command — the output should be empty. Kernel version and required kernel modules are equally important. The br_netfilter module must be loaded, and the sysctl settings for IP forwarding and bridge traffic must be enabled. You can confirm module state with the lsmod command and verify sysctl values directly from the proc filesystem.
How to Install a Container Runtime on Bare Metal Before Running kubeadm
Before kubeadm initializes the cluster, the server needs a that conforms to the Interface specification. Without a correctly installed and configured runtime, the kubeadm init command will fail at the preflight stage — not partway through, but immediately. On a bare Ubuntu or Debian server, containerd is the runtime that aligns most cleanly with what kubeadm expects to find. Install containerd from the official package repository for your distribution rather than from a generic binary archive.
Setting Systemd Cgroup to true is the single configuration change that prevents the most common silent cluster failures.
This ensures the version is compatible with your kernel and that the service integrates correctly with systemd. Once installed, generate the default configuration file using containerd's built-in config command and write it to the standard path under /etc/containerd. The default configuration is not sufficient on its own: you must locate the runc options block within that file and set SystemdCgroup = true for containerd. Your kubeadm/kubelet configuration must also use cgroupDriver: systemd.
This pairing is the most common source of silent failure during cluster initialization. When the cgroup driver used by containerd does not match the driver kubeadm configures for the kubelet — and on systemd-managed servers that driver must be systemd. After editing the configuration file, restart the containerd service and verify it is active and enabled to start on boot. The systemctl status command confirms both.
You should also confirm that the containerd socket exists at its expected path, because kubeadm uses that socket path to communicate with the runtime during initialization. If you are configuring this as part of a broader bare-metal provisioning workflow, cgroup driver alignment is the single configuration detail that warrants a deliberate check before proceeding — everything else in the runtime setup is straightforward package installation.
A well-structured dedicated server guide covers this runtime configuration step within a sequenced provisioning checklist, so each dependency is confirmed before the next command runs.

All three binaries — kubeadm, kubelet, and kubectl — must be installed together from the official Kubernetes repository in a coordinated step, because kubelet must be present on the host before the control plane can be bootstrapped.
How to Install kubeadm, kubelet, and kubectl on Your Dedicated Server
Installing kubeadm, kubelet, and kubectl on a dedicated server means adding the official Kubernetes package repository, importing its signing key, and then installing all three binaries in a single coordinated step. The order matters: kubelet must be present before kubeadm runs, and kubectl must match the same minor version as the control plane components or API calls will behave unpredictably. Start by adding the Kubernetes apt repository for your target minor version.
Kubernetes publishes separate repository paths per minor release — for example, a dedicated path for version 1.30 and a separate path for 1.31. Pinning your repository to a specific minor version path is the correct approach because it prevents the package manager from silently upgrading to the next minor release during a routine system update.
After adding the repository and importing its GPG signing key, install all three packages in one command and immediately mark them as held using the package manager's hold mechanism. This hold prevents unintended upgrades from breaking a running cluster — a detail that matters far more on a long-lived bare-metal node than on a short-lived virtual instance that is regularly replaced. Once installed, verify that all three binaries report the same version.
A mismatch between the kubelet version and the kubeadm version is one of the few preflight conditions that produces an explicit error rather than a silent misconfiguration. Run the version flag on each binary and confirm the output before proceeding. On a dedicated server, you have full control over the package state without a platform layer silently managing updates on your behalf — version pinning discipline is therefore an operational responsibility you own directly.
How to Initialize a Single-Node Cluster with kubeadm init
Running kubeadm init on a dedicated server launches the control plane and produces a fully functional single-node cluster in a single command — provided you supply the correct flags for your network configuration.
The two flags that matter most at initialization are the pod network CIDR and the API server advertise address. Getting these right before the command runs avoids a class of networking errors that are difficult to correct without tearing down and reinitializing the cluster from scratch.
The pod network CIDR defines the IP address range that the cluster assigns to pods.
This range must not overlap with your server's existing network interfaces or your local subnet.A common choice is 10.244.0.0/16 when using a Flannel-compatible network plugin, or 192.168.0.0/16 when using Calico — each plugin expects a specific default range, so select the CIDR that matches the network plugin you intend to deploy immediately after initialization.
The API server advertise address should be set to the server's primary private IP address, not a loopback address. On a dedicated server, that primary interface address is stable and predictable, which eliminates a common source of ambiguity that arises on virtual instances where interface names and addresses shift between reboots.
When kubeadm init runs, it executes a preflight sequence before writing any configuration. Read that output carefully: warnings are informational, but errors halt initialization and must be resolved before retrying. Once initialization completes, the terminal prints a kubeadm join token and a block of instructions for configuring kubectl.
Copy the admin configuration file to your home directory's .kube directory and set the correct file permissions — kubectl access configuration is the step most commonly skipped, and without it, every subsequent kubectl command will fail with an authentication error.
After initialization, confirm the control plane is healthy by running the node status command. On a single-node cluster, the node will commonly remain NotReady until a compatible CNI plugin is installed and initialized.
How to Deploy a Pod Network Add-On and Untaint the Control Plane Node
Before applying any network plugin, confirm that the pod network CIDR in the plugin manifest matches exactly what you passed to --pod-network-cidr during kubeadm init. A mismatch does not produce a clean error — pods reach a broken network layer rather than an absent one, which makes the root cause significantly harder to isolate than a straightforward misconfiguration. Check the CIDR value in the plugin’s configuration file before running kubectl apply, not after.
Switching CNI plugins after workloads are running is disruptive enough that one careful choice upfront saves hours of remediation.
Two plugins suit single-node clusters without introducing unnecessary complexity. Flannel implements a simple overlay network and is applied by passing its manifest directly to kubectl apply -f with the manifest URL or local path. Calico provides granular network-policy control and is the better choice when you expect to enforce pod-level traffic-isolation rules. Migrating between CNI plugins after workloads are running is disruptive enough that the decision is worth making once.
A common CIDR pairing is 10.244.0.0/16 with Flannel and 192.168.0.0/16 with Calico; whichever you choose, the value must be consistent across the kubeadm init invocation and the plugin manifest. After applying the manifest, watch the node status with kubectl get nodes --watch until it transitions to Ready.
On a dedicated server with full CPU and memory access, this transition is typically clean — unlike on a shared or virtualized host where CPU steal time can delay the API server readiness check or cause etcd to miss its election window, both of which surface as opaque timeout errors in kubeadm init output rather than as resource warnings.
Once the node reaches Ready, application workloads still cannot be scheduled by default because the control plane node carries a node-role.kubernetes.io/control-plane: taint. Remove it with kubectl taint nodes <node-name> node-role.kubernetes.io/control-plane:-, then confirm scheduling is active by deploying a minimal test pod — for example, kubectl run probe --image=nginx:stable-alpine — and verifying it reaches Running state.
If the pod stalls in Pending, inspect CNI plugin logs before assuming a resource constraint.

Securing a bare-metal Kubernetes deployment requires hardening both the API server's authentication and authorization settings and the host-level firewall rules that determine which traffic is ever allowed to reach the node.
How to Harden and Expose Your Cluster Safely on Bare Metal
Hardening a single-node Kubernetes cluster on bare metal means securing two distinct layers simultaneously: the Kubernetes API server itself, and the host firewall that controls which traffic ever reaches it.
A cluster that passes its own internal health checks can still be fully exposed to the public internet if the underlying server's firewall rules are absent or overly permissive.
Both layers must be addressed before the cluster handles any real workload. On a dedicated server with a public IP address, that port is reachable from anywhere unless a firewall rule explicitly restricts it.
The correct approach is to allow inbound connections on 6443 only from the IP addresses or ranges you control — your administrative workstation, a bastion host, or a VPN gateway.
All other sources should be dropped at the firewall level, not at the application level. The same principle applies to the kubelet port and any services you expose: restrict by source, not just by destination.
For the underlying host-level controls that complement this Kubernetes-layer configuration, How to Harden SSH Access on Your Dedicated Server covers the SSH hardening sequence that should already be in place before the cluster is initialized.
Within Kubernetes itself, the RBAC baseline is the next control to establish. By default, kubeadm applies a reasonable starting policy, but you should review whether anonymous API authentication is required. If it is unnecessary, disable it through the API-server authentication configuration. If it remains enabled, verify that anonymous users receive no unintended RBAC permissions. Any service accounts that do not require cluster-wide permissions should be bound to namespace-scoped roles only.
Audit logging at the API server level provides the visibility needed to detect unexpected access attempts. A least-privilege role structure — where each workload identity receives only the permissions its pods require — limits the blast radius of any compromised container.
A structured dedicated server walkthrough consolidates these firewall rules, RBAC bindings, and audit settings into a sequenced hardening checklist so no control is applied in isolation.
How to Validate the Cluster and Deploy a Test Workload
Run kubectl get nodes, inspect all system pods with kubectl get pods -A, query the API-server readiness endpoint, and deploy a test workload that validates scheduling, DNS resolution, and pod networking.
kubectl get nodes
kubectl get pods -A
kubectl get --raw='/readyz?verbose'With that baseline confirmed, deploy a single-replica workload using a lightweight image such as nginx:stable-alpine: it exercises the full cycle — the scheduler assigns the pod, the kubelet pulls the image, and the CNI plugin wires up the cluster IP — giving you a concrete pass/fail signal rather than an ambiguous component status.
A single-replica deployment using a lightweight public image such as nginx:stable-alpine exercises every layer: the scheduler assigns the pod to the node, the kubelet pulls the image, the CNI plugin assigns a cluster IP, and the pod transitions from Pending to Running. Use kubectl get pods -o wide to confirm the assigned IP and node name.
Pod scheduling and CNI assignment together form the only reliable end-to-end integration test at this stage — component-level health checks alone cannot surface a misconfigured pod CIDR or a missing CNI route. If the pod stalls in Pending, run kubectl describe pod <pod-name> to read the exact event that blocked scheduling, whether that is an unresolved taint, a missing CNI route, or an image pull failure.

The vast majority of kubeadm failures on bare metal trace back to four root causes — active swap, a missing br_netfilter module, a mismatched container runtime socket path, or a cgroup driver mismatch between your container runtime and kubelet configuration.
Common kubeadm Failures on Bare Metal and How to Fix Them
When kubeadm init fails on bare metal, the error is almost always one of four conditions: swap is still active, the br_netfilter kernel module is not loaded, the container runtime socket path does not match what kubeadm expects, or the cgroup driver in your containerd configuration conflicts with the driver kubeadm passes to the kubelet. Each produces a preflight error that names the problem directly — read the output carefully before attempting any fix.
A cgroup driver mismatch can produce a cluster that initialises cleanly yet fails silently at runtime — dry-run first to catch it.
The kubelet journal is the authoritative source for narrowing down runtime-socket, cgroup-driver, and startup-flag mismatches.
The subtler failure modes are timing and state persistence. Running swapoff -a disables swap for the current session, but if a systemd swap unit remains enabled it will re-activate on the next reboot regardless of /etc/fstab edits — verify with systemctl --type swap and disable any listed units explicitly.
The cgroup driver conflict is the failure most likely to produce a cluster that initialises without error but then behaves incorrectly at runtime; confirm the socket path and driver kubeadm resolves by running kubeadm init --dry-run before committing to a full initialization.
For the container runtime socket, confirm the path kubeadm is using by running kubeadm init --dry-run and checking which socket it resolves to; if containerd is installed but the socket sits at a non-default path, pass --cri-socket explicitly to avoid a mismatch error at init time.
The cgroup driver conflict is the failure most likely to produce a cluster that initialises without error but then behaves incorrectly. If containerd is configured with SystemdCgroup = true in /etc/containerd/config.toml, your kubelet configuration must specify cgroupDriver: systemd — a mismatch causes the kubelet to enter a crash loop shortly after the control plane comes up.
On a dedicated server this is easier to diagnose than on a VPS, because you have direct, uncontested access to system logs without CPU steal time distorting timing-sensitive output from journalctl -u kubelet. On virtualised infrastructure, CPU steal can delay API server readiness responses past kubeadm's internal timeout threshold, producing failures that look like configuration errors but are caused entirely by resource contention from other tenants.
Kubernetes Core Components: kubeadm vs kubelet vs kubectl
| Criterion | kubeadm | kubelet | kubectl |
|---|---|---|---|
| Primary Role | Bootstraps and initializes the cluster control plane | Runs pods and manages containers on each node | Sends commands to the API server from the CLI |
| Runs On | Invoked directly on the server during setup | Runs as a system service on every node continuously | Runs on any machine with API server access |
| When It Executes | Used during cluster init, join, and upgrade phases | Active continuously after node joins the cluster | Used on demand by operators managing workloads |
| Cluster Lifecycle Scope | Handles preflight checks, certificates, and control-plane config | Enforces pod specs and reports node health to API server | Applies manifests, inspects resources, and drains nodes |
| Failure Impact | Failure during init leaves cluster in inconsistent state | Node marked NotReady; pods evicted or rescheduled | No cluster impact; only operator visibility is lost |
Conclusion – Your Single-Node Cluster Is Running, Now Keep It Healthy
For provider fit and procurement context, see our guide to choosing a dedicated server provider and the honest recommendation overview.
With your control-plane node initialized, a CNI plugin installed, and the taint removed, you have a fully operational single-node Kubernetes cluster on hardware that belongs entirely to you. Every scheduling decision, every cgroup allocation, and every network packet travels through resources no other tenant can claim — a guarantee that VPS and shared infrastructure structurally cannot offer.
From this point, your operational focus shifts to sustainability. Monitor etcd disk latency, rotate kubeconfig credentials on a defined schedule, and keep containerd and kubeadm versions aligned across upgrades to avoid preflight regressions. Audit API server logs periodically to catch permission drift before it becomes a security event.
A single-node cluster rewards disciplined maintenance; the absence of redundant nodes means each configuration change carries direct consequence. A single-node cluster has no control-plane or node-level high availability. A server, storage, or control-plane failure can make the entire cluster unavailable, so it should not be described as resilient production infrastructure without an external recovery design.




