K8s Is Overkill for 2-10 Servers — Build a Docker Cluster Without It
Kubernetes adds 3 control plane nodes before your first container runs. Container scheduling, health monitoring, and orchestration for small fleets without the complexity tax.
You can run containers across multiple servers without Kubernetes by using a control-plane-plus-worker pattern: one node schedules workloads, workers execute them, and a reverse proxy routes traffic. For 2–10 servers, this covers 100% of what most teams actually need from Kubernetes — without the etcd cluster, YAML manifests, or dedicated platform team.
This guide walks through the five things a multi-node Docker cluster actually needs, shows where DIY SSH breaks down, compares Docker Swarm and k3s as popular middle-ground options, and demonstrates how purpose-built tools handle the hard parts automatically.
TL;DR: A multi-node Docker cluster needs five things: container scheduling, health checks, networking, load balancing, and rolling updates. For 2–10 nodes, tools like Temps reduce this to two commands — temps serve on the control plane, temps join on each worker — while handling WireGuard-encrypted inter-node networking, automatic failover, and built-in observability. 40% of Kubernetes users report it is too complex for their workloads.
According to the CNCF Annual Survey, 40% of Kubernetes adopters cite complexity as their top challenge. That complexity makes sense when you are orchestrating hundreds of pods across dozens of nodes. It does not make sense for a SaaS startup running three servers.
Container orchestration, at its core, solves five problems. The Stack Overflow Developer Survey found that 72% of professional developers use Docker, but most never need more than these fundamentals to run containers across multiple machines.
Container scheduling decides which container runs on which node. For a 2–10 node cluster, you rarely need sophisticated scheduling. You need containers to spread across nodes so a single failure does not take everything down.
Health monitoring answers one question: is this container still working? Without it, a crashed container on a remote node stays crashed until someone notices — usually your users, filing support tickets at 2 AM.
Containers on different physical servers need to communicate. If your nodes share a private network (same VPC, same datacenter), containers can communicate via the host's private IP and exposed ports. For nodes on different networks, an encrypted tunnel (WireGuard) bridges the gap without overlay complexity.
Traffic from users needs to reach the right container. A reverse proxy on your control plane receives all incoming requests and forwards them to the correct container on the correct node.
Deploying a new version should not kill the old one until the new one is ready. The orchestrator starts the new container, waits for it to pass health checks, shifts traffic over, then stops the old container. Zero dropped requests.
Every multi-node orchestrator follows the same fundamental pattern. One node makes decisions, the rest execute them. Google's Borg paper, the predecessor to Kubernetes, established this architecture over 15 years ago.
Scheduling — decides which worker runs each container based on available resources
State management — stores the desired state (what should be running) and actual state (what is running)
Traffic routing — receives incoming HTTP/HTTPS requests and proxies them to the correct worker and container
Health aggregation — collects health reports from all workers and triggers rescheduling when something fails
In Kubernetes, the control plane consists of the API server, etcd, the scheduler, and the controller manager — four separate components. In simpler orchestrators, a single binary handles all of this.
Workers are simpler. Each worker runs an agent that:
Connects to the control plane on startup
Receives instructions: "run container X with these environment variables"
Pulls the image and starts the container using the local Docker daemon
Reports health status back to the control plane at regular intervals
Stops containers when told to
The worker does not make decisions. It executes. If the control plane goes down, workers keep running their existing containers — they just do not receive new instructions until the control plane recovers.
Yes, and it is a great way to understand what orchestrators actually do. The average Docker container starts in under 2 seconds, making SSH-based management surprisingly responsive for small clusters. But this approach hits a wall fast.
Both Docker Swarm and k3s occupy the middle ground between shell scripts and full Kubernetes. Each solves the core orchestration problem with different trade-offs.
Docker Swarm ships with every Docker installation and initializes in about 30 seconds:
# On the control plane node:docker swarm init --advertise-addr 10.0.0.1# Output includes a join token. On each worker:docker swarm join --token SWMTKN-1-xxxx 10.0.0.1:2377
Swarm distributes replicas across nodes, sets up an internal load balancer, and handles rolling updates. But development has slowed — Docker Inc. shifted focus to Docker Desktop and Hub. Swarm receives maintenance patches, not new features. It also has no built-in observability: it tells you if a container is running, not why it is slow or what errors it is throwing.
k3s is a certified Kubernetes distribution that strips out legacy features and bundles everything into a ~70MB binary. It is the right choice if you need Kubernetes API compatibility — for Helm charts, GitOps tools, or cloud-provider integrations that assume Kubernetes. The operational overhead is meaningfully lower than full Kubernetes, but you still get CRDs, RBAC, and etcd (swapped for SQLite by default). For teams that just want containers on multiple servers, that is still more than you need.
Docker Swarm minimizes operational overhead but lacks observability and has a shrinking ecosystem. k3s gives you Kubernetes compatibility but reintroduces complexity. Neither includes built-in analytics, error tracking, or session replay — you pay for those separately, or you self-host multiple additional tools.
Temps takes the control-plane-plus-worker pattern and reduces it to two commands. According to the Datadog Container Report, 54% of organizations run containers across multiple hosts — yet most still struggle with the operational complexity of doing so.
This binary runs the scheduler, the state database (backed by PostgreSQL/TimescaleDB), the reverse proxy (built on Pingora, Cloudflare's open-source proxy framework), and the health aggregation system. No collection of services to configure, no Helm charts to deploy.
The scheduler uses a LeastLoaded strategy by default: each replica is assigned to the node with the lowest combined CPU and memory utilization. A configurable max load threshold (default 90%) prevents overloading any single node, with graceful fallback to the next least-loaded node.
The control plane handles:
HTTPS termination with automatic Let's Encrypt certificates
Container scheduling across all connected workers (LeastLoaded or RoundRobin)
Health check aggregation with configurable alarm thresholds
Git-push deployments — push to a branch, Temps builds and deploys the container
Built-in analytics, error tracking, and session replay — no Sentry, no Plausible, no FullStory needed
If your nodes share a private network (same VPC, same datacenter, or Tailscale mesh), adding a worker takes one command on the worker machine:
temps join <control-plane-url> <join-token> --private-address 10.0.0.2
The worker connects to the control plane, registers itself, and starts accepting container assignments. No tokens to generate manually from a separate tool, no certificates to configure, no overlay network to debug.
What if your worker is behind a NAT or on a completely different network? Temps uses an embedded WireGuard implementation (via defguard_wireguard_rs with boringtun — Cloudflare's Rust WireGuard implementation) to create a secure tunnel:
temps join <control-plane-url> <join-token>
Without --private-address, relay mode establishes a WireGuard tunnel through api.temps.sh, creating a secure point-to-point connection between the worker and control plane. No external wireguard-tools package or kernel module required — the WireGuard protocol runs in-process. This means you can add a worker running on your home server, a Raspberry Pi, or a VPS in a completely different datacenter. No VPN setup, no firewall rules, no port forwarding.
Unlike Docker Swarm or Kubernetes, Temps does not create a virtual overlay network for container-to-container traffic. Containers communicate through the host's network using exposed ports. The Pingora-based reverse proxy on the control plane handles routing.
Overlay networks add latency, complicate debugging, and create failure modes that are hard to diagnose. With direct networking, you eliminate most of those failure modes.
Provision a new server (any cloud provider, bare metal)
Install Docker and the Temps binary
Run one command:
# Direct mode (private network):temps join <control-plane-url> <join-token> --private-address 10.0.0.4# Relay mode (different network or behind NAT):temps join <control-plane-url> <join-token>
The control plane discovers the new node's available resources and begins scheduling containers to it. Existing containers do not move unless you explicitly rebalance.
When you need to patch a server, upgrade hardware, or decommission a node:
temps node drain <node_id>
Draining stops new containers from being scheduled to the node and migrates existing containers to other healthy nodes. Containers are started on the new node before being stopped on the draining node — traffic shifts seamlessly.
The control plane detects missed health check reports from the worker
After a configurable timeout, the node is marked as unhealthy
The scheduler identifies all containers that were running on the failed node
Those containers are rescheduled onto healthy workers with available capacity
The reverse proxy updates its routing table to point to the new container locations
Your users may experience a brief interruption for containers that were on the failed node, but everything recovers automatically — without manual intervention.
Use a control-plane-plus-worker orchestrator built for small fleets instead of a general-purpose one built for hundreds of nodes. Concretely: one node runs the scheduler, state database, and reverse proxy; each additional server runs a lightweight agent that pulls images and reports health back. Temps packages this pattern into a single Rust binary — temps serve on the control plane, temps join on each worker — and adds WireGuard-encrypted networking (including relay mode for workers behind NAT, with no external wireguard-tools package needed), automatic failover, and built-in analytics, error tracking, and session replay, none of which Docker Swarm or k3s include. For 2–10 nodes this covers what most teams need from Kubernetes without the etcd cluster, YAML manifests, or dedicated platform team.
There is no hard technical limit. The constraint is operational, not architectural. For most lightweight orchestrators, 2–10 nodes is the sweet spot where you get meaningful redundancy without needing a dedicated platform team. Some teams run 20–30 nodes successfully on tools like Nomad or Temps. Beyond 50 nodes, the scheduling and networking complexity starts approaching what Kubernetes was designed to handle.
Docker Swarm is simpler to set up (built into Docker) and has lower operational overhead, but development has slowed and it lacks observability. k3s runs a full Kubernetes API in a ~70MB binary, which is the right choice if you need Helm chart or GitOps compatibility — but reintroduces Kubernetes complexity. For teams that just want containers on multiple servers with automatic failover, neither includes built-in analytics or error tracking.
The control plane detects missed health check reports and marks the node as unhealthy after a configurable timeout. All containers from the failed node are then rescheduled onto remaining healthy workers. The reverse proxy updates its routing automatically. In Temps, this process uses the node health check job that monitors worker heartbeats and triggers rescheduling when a node is marked stale.
Yes. A 2-vCPU worker can run lightweight services while an 8-vCPU worker handles compute-heavy containers. The scheduler accounts for available resources when placing containers. In Temps, each worker reports its capacity to the control plane, and the LeastLoaded scheduler distributes workloads based on actual available CPU and memory — preferring nodes with the lowest combined utilization.
There are two approaches in Temps. Direct mode uses the host's private IP and exposed ports for nodes on the same private network — no overlay required. Relay mode uses an embedded WireGuard tunnel (no external wireguard-tools needed) for nodes on different networks or behind NAT. Overlay networks like Docker Swarm's VXLAN add latency and create failure modes that are harder to diagnose.
No. Temps is Apache 2.0 and free to self-host. Temps Cloud (managed hosting on Hetzner) costs approximately $6/month — Hetzner cost plus 30% margin. There are no per-seat fees and no bandwidth bills.
Running containers across multiple servers does not require a PhD in distributed systems. The core pattern is straightforward: one control plane makes scheduling decisions, workers execute them, and a reverse proxy routes traffic. Kubernetes handles this at massive scale with massive complexity. For 2–10 nodes, you need something simpler.
We have covered the options, from DIY shell scripts to Docker Swarm to k3s to purpose-built tools. The right choice depends on your scale and operational tolerance:
Shell scripts: Fine for 2 servers and a weekend project. Breaks on node failures.
Docker Swarm: Zero-dependency, minimal setup. Good if Swarm's ecosystem fits your workflow.
k3s: Full Kubernetes API in a small binary. Right choice for Helm/GitOps compatibility.
Temps: Removes the observability gap — built-in analytics, error tracking, and session replay alongside orchestration.
To try Temps:
# Install on your control plane:curl -fsSL temps.sh/install.sh | bash# Set up the control plane (interactive wizard):temps setuptemps serve# Add workers (generates a join token in the dashboard):temps join <control-plane-url> <join-token> --private-address <WORKER_IP>
Your multi-node Docker cluster is running. No Kubernetes, no YAML manifests, no etcd cluster. Just containers, scheduled across your servers, with health monitoring and automatic failover built in.