March 12, 2026 (3mo ago)
Written by Temps Team
Last updated March 12, 2026 (3mo ago)
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 serveon the control plane,temps joinon 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.
| Metric | Lightweight Cluster | Kubernetes Territory |
|---|---|---|
| Servers | 2–10 | 10–1,000+ |
| Containers | 5–50 | 50–10,000+ |
| Team size | 1–5 engineers | Dedicated platform team |
| Deploy frequency | Daily to weekly | Continuous (multiple/hour) |
| Services | 3–20 | 20–500+ |
If you fall in the left column, you need orchestration, not Kubernetes. The distinction matters.
Kubernetes includes dozens of abstractions designed for large-scale operations. For small clusters, most create overhead without benefit:
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.
+-----------------------+
| CONTROL PLANE |
| |
| - Scheduler |
| - State database |
| - Reverse proxy |
| - Health aggregator |
| - API server |
+-----------+-----------+
|
+--------------+--------------+
| | |
+------+------+ +----+----+ +-------+-----+
| WORKER 01 | | WORKER 02| | WORKER 03 |
| | | | | |
| - Docker | | - Docker | | - Docker |
| - Agent | | - Agent | | - Agent |
| - Containers| | - Contrs | | - Containers |
+-------------+ +----------+ +--------------+
The control plane handles four responsibilities:
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:
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.
The communication channel between control plane and workers is where orchestrators diverge:
For a 2–10 node cluster, an HTTP API or persistent connection works perfectly.
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.
1. Deploy a container to a remote node:
#!/bin/bash
# deploy.sh — push a container to a specific node
NODE_IP=$1
IMAGE=$2
CONTAINER_NAME=$3
ssh root@$NODE_IP "docker pull $IMAGE && \
docker stop $CONTAINER_NAME 2>/dev/null; \
docker rm $CONTAINER_NAME 2>/dev/null; \
docker run -d --name $CONTAINER_NAME \
--restart unless-stopped \
-p 3000:3000 \
$IMAGE"
2. Simple round-robin scheduling:
#!/bin/bash
# schedule.sh — distribute containers across nodes
NODES=("10.0.0.2" "10.0.0.3" "10.0.0.4")
NODE_INDEX=0
deploy_container() {
local image=$1
local name=$2
local node=${NODES[$NODE_INDEX]}
./deploy.sh "$node" "$image" "$name"
NODE_INDEX=$(( (NODE_INDEX + 1) % ${#NODES[@]} ))
}
3. Basic health checking:
#!/bin/bash
# healthcheck.sh — check containers across all nodes
NODES=("10.0.0.2" "10.0.0.3" "10.0.0.4")
for node in "${NODES[@]}"; do
echo "--- $node ---"
ssh root@$node "docker ps --format 'table {{.Names}}\t{{.Status}}'"
done
This works for a weekend project with two servers. Here is where it falls apart:
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.
| Feature | Docker Swarm | k3s | Temps |
|---|---|---|---|
| Setup time | ~2 min | ~5 min | ~15 min |
| Kubernetes API | No | Yes (full) | No |
| Overlay network | VXLAN (built-in) | Flannel (built-in) | WireGuard (relay) / direct |
| Built-in observability | No | No | Yes (analytics, errors, sessions) |
| Encrypted inter-node traffic | TLS on control channel | mTLS via ServiceAccount tokens | WireGuard (relay mode) |
| Nodes behind NAT | Needs firewall rules | Needs firewall rules | Built-in (relay via api.temps.sh) |
| License | Apache 2.0 | Apache 2.0 | Apache 2.0 |
| Managed cloud option | Docker Swarm on AWS/GCP | Various | Temps Cloud (~$6/mo) |
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.
The Temps control plane is a single Rust binary:
temps serve
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:
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.
# 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.
Your users may experience a brief interruption for containers that were on the failed node, but everything recovers automatically — without manual intervention.
For each node, you want to know:
Temps surfaces all of this through its built-in dashboard. No Grafana stack to deploy, no Prometheus to configure.
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:
To try Temps:
# Install on your control plane:
curl -fsSL temps.sh/install.sh | bash
# Set up the control plane (interactive wizard):
temps setup
temps 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.