Documentation

Container runtime

Embedded containerd and crun, a host runtime instead, running in a container, exclusive CPU cores and a Docker-compatible API.

Embedded containerd and crun

By default KubeSolo runs its own container runtime, bundled in the binary: containerd 2.2.5 with crun 1.26 as the OCI runtime and the CNI plugins v1.9.0. It keeps its state under /var/lib/kubesolo/containerd, listens on /var/lib/kubesolo/containerd/containerd.sock, and links /run/containerd/containerd.sock to it so standard tools find it.

Any OCI image that runs under Docker or Podman runs here. Inspect it with crictl or ctr pointed at that socket.

Why the installer refuses Docker: Docker brings its own containerd and rewrites the host's network rules, which conflicts with KubeSolo's embedded runtime and networking. The install script and kubesoloctl install stop if Docker is installed or running. For a laptop with Docker, use container mode.

Attaching a host runtime

If the host already manages containerd or CRI-O, KubeSolo can attach to it instead of starting its own:

yaml
1runtime:
2 endpoint: unix:///run/containerd/containerd.sock # or unix:///run/crio/crio.sock

The value must be an absolute socket path or a unix:// URL. With an endpoint set, the host owns the runtime, the OCI runtime, the CNI plugin binaries in its own plugin directory, the sandbox image and any registry configuration. KubeSolo still writes its bridge CNI configuration to /etc/cni/net.d/10-bridge.conflist, and warns at startup if the runtime reports that its network is not ready.

Install with kubesoloctl here. During conflict cleanup, the install script can stop processes that hold /run/containerd/containerd.sock, which includes a host containerd. kubesoloctl install leaves a host-owned socket alone. Install, set runtime.endpoint, then restart KubeSolo.

With an external runtime, containers keep running across a KubeSolo restart. That differs from the embedded runtime, where a restart recreates pods.

Container mode

KubeSolo can run inside a container instead of on the host. This is for development and CI. The simplest way is kubesoloctl install --run-mode=container, which also handles the kubeconfig and published ports.

Container mode turns on automatically when KubeSolo finds /.dockerenv, /run/.containerenv or a non-empty container environment variable. Force it on or off with runtime.containerMode. The published image sets --container-mode. In container mode KubeSolo:

  • uses the cgroupfs driver and sets up cgroup controller delegation;
  • remounts / as rshared so volume mounts propagate into pods;
  • disables per-QoS cgroups and relaxes eviction and image garbage collection thresholds, so the host's disk usage does not evict pods;
  • gives pods an empty node resolv.conf, and CoreDNS forwards to 1.1.1.1 and 8.8.8.8;
  • leaves conntrack sysctls alone, since /proc/sys is often read-only;
  • refuses the static CPU manager policy.

Running the image directly

Release images are portainer/kubesolo:<version>, for example portainer/kubesolo:v1.2.1, built for amd64, arm64, arm and riscv64. There is no latest tag.

bash
1$ docker run -d --privileged \
2 --hostname kubesolo \
3 --security-opt seccomp=unconfined \
4 --security-opt apparmor=unconfined \
5 --tmpfs /tmp --tmpfs /run \
6 -v /lib/modules:/lib/modules:ro \
7 -v kubesolo-data:/var/lib/kubesolo \
8 -p 6443:6443 \
9 --name kubesolo \
10 portainer/kubesolo:v1.2.1

The kubeconfig inside the container points at the container's own address. Copy it out and point it at the published port:

bash
1$ docker exec kubesolo cat /var/lib/kubesolo/pki/admin/admin.kubeconfig > kubeconfig
2$ sed -i 's|https://[^"]*:6443|https://127.0.0.1:6443|' kubeconfig # on macOS: sed -i ''
3$ export KUBECONFIG=$PWD/kubeconfig
4$ kubectl get nodes --watch

Publish any NodePort or LoadBalancer ports your workloads need with extra -p options when you create the container.

CPU pinning

By default every pod shares every CPU. Latency-sensitive workloads such as audio processing, motion control and machine vision see that as jitter. The kubelet's static CPU manager policy gives qualifying pods exclusive cores that no other pod may use:

bash
1$ curl -sfL https://get.kubesolo.io | sudo sh -s -- \
2 --cpu-manager-policy=static \
3 --cpu-manager-policy-options=full-pcpus-only=true,strict-cpu-reservation=true \
4 --reserved-cpus=0
SettingDefaultMeaning
kubernetes.kubelet.cpuManager.policynonestatic enables exclusive cores.
kubernetes.kubelet.cpuManager.reservedCPUs"0" under staticCpuset of CPU indexes held back for the host and KubeSolo. 0-1 reserves two CPUs.
kubernetes.kubelet.cpuManager.policyOptions{}full-pcpus-only, strict-cpu-reservation, distribute-cpus-across-numa, prefer-align-cpus-by-uncorecache. The last two cannot be combined.
kubernetes.kubelet.systemReserved{}Quantities instead of indexes, such as cpu=1. The kubelet then chooses which cores. If both are set, reservedCPUs wins and KubeSolo warns.

A pod gets exclusive cores only if every container has Guaranteed QoS (CPU and memory requests equal to limits) and a whole-number CPU value. A pod that does not qualify still runs, silently in the shared pool, so verify from inside the container:

yaml
1resources:
2 requests: { cpu: "2", memory: "512Mi" }
3 limits: { cpu: "2", memory: "512Mi" }
bash
1$ kubectl exec audio-processor -- grep Cpus_allowed_list /proc/self/status

The static policy needs a host with at least two CPUs and is not supported in container mode. KubeSolo has no scheduler, so a pod that does not fit is rejected by the kubelet rather than rescheduled. Changing the policy only needs a restart; KubeSolo removes the kubelet's cpu_manager_state file for you.

Exclusive cores stop other pods interfering, not the kernel. For deterministic latency also isolate the cores (isolcpus, nohz_full, rcu_nocbs), steer IRQs away from them, and confine KubeSolo itself with CPUAffinity=. The full guide is in the repository: docs/configuration/cpu-pinning.md.

d2k: a Docker-compatible API

KubeSolo can embed d2k, Portainer's Docker-to-Kubernetes API translator. With it enabled, the node exposes a Docker-compatible API over mTLS on port 2376, and Docker calls become Kubernetes resources in one namespace. Existing Docker CLIs, scripts and CI jobs can target a KubeSolo node unchanged.

bash
1$ curl -sfL https://get.kubesolo.io | sudo sh -s -- --d2k=true --d2k-namespace=workloads

KubeSolo reuses its own CA to mint a d2k server and client certificate under /var/lib/kubesolo/pki/d2k/, deploys d2k 1.2.3 with its RBAC and a d2k-tls Secret, and exposes it through a LoadBalancer Service.

  • Architectures: amd64 and arm64 only. On arm and riscv64, d2k disables itself with a warning.
  • Needs the LoadBalancer: d2k.enabled with network.loadBalancer.enabled: false is rejected at startup.
  • Namespace is fixed after first start: the server certificate names the namespace. To change it, delete /var/lib/kubesolo/pki/d2k/server.crt and server.key before restarting.

Connecting from your machine

bash
1$ CERT_DIR="$HOME/.config/d2k"
2$ mkdir -p "$CERT_DIR"
3$ scp user@NODE_IP:/var/lib/kubesolo/pki/ca/ca.crt "$CERT_DIR/ca.crt"
4$ scp user@NODE_IP:/var/lib/kubesolo/pki/d2k/client.crt "$CERT_DIR/client.crt"
5$ scp user@NODE_IP:/var/lib/kubesolo/pki/d2k/client.key "$CERT_DIR/client.key"
6$ docker context create kubesolo \
7 --docker "host=tcp://NODE_IP:2376,ca=$CERT_DIR/ca.crt,cert=$CERT_DIR/client.crt,key=$CERT_DIR/client.key"
8$ docker --context kubesolo ps

The files are root-owned on the node, so copy them as a user who can read them. In container mode, kubesoloctl d2k fetch does all of this for you. docker ps lists the pods in the d2k namespace as containers; the d2k README has the full translation table.