I'm setting up a small homelab CI/CD environment with Gitea Actions running on Kubernetes. The cluster has one control-plane node and two workers, uses Ubuntu 26.04, Kubernetes 1.34.10, and containerd 2.2.2. Gitea 1.27.0 and the act_runner 0.6.1 are both running in Kubernetes Deployments, with Longhorn for persistent storage. Docker and nerdctl are not installed on the worker nodes, although ctr and /run/containerd/containerd.sock are available.
The runner registers and works when configured with the host label, but a workflow using actions/checkout@v4 fails with "Cannot find: node in PATH" because the runner image does not include Node. A label such as "ubuntu-latest:docker://node:22-bookworm" appears to be the intended way to execute jobs inside a separate environment, but that model expects Docker-compatible access.
I would prefer not to install Docker on the Kubernetes nodes just to provide job containers when the cluster already uses containerd. Is there a supported way for act_runner to launch job containers directly through containerd? If not, should I use Docker-in-Docker, install Docker on a worker, or use Kubernetes-native ephemeral runners? I'm looking for a clean architecture that supports checkout and tests now, and eventually building and pushing OCI images for deployment through Argo CD.
3 Answers
Docker-in-Docker can work and is probably the quickest way to keep the existing docker:// labels without installing Docker on the host. Put a Docker daemon in a dedicated runner pod, give it the required privileges, and point the runner at that daemon. However, it adds nested-container overhead, more complicated storage and networking, and significant security concerns. It makes sense for a small number of trusted builds, but I would not treat it as the cleanest long-term Kubernetes architecture or expose a privileged Docker socket casually.
If the workload is only a few builds per day, this may be an acceptable pragmatic solution. For many concurrent or untrusted jobs, ephemeral Kubernetes runners are preferable.
The standard container execution mode is built around a Docker-compatible API, not the containerd socket or the Kubernetes runtime directly. Having containerd available on the node does not automatically make it usable by act_runner, and using ctr would not provide the API, networking, volume handling, and lifecycle behavior the runner expects.
For Kubernetes, a better design is usually to run isolated, short-lived runners as Kubernetes workloads rather than making one permanent runner create sibling containers. A runner-controller or another Kubernetes-native runner implementation can provision a pod per job, with the required Node, Python, Git, and build tools supplied by the job image. Whether that option is available depends on the Gitea edition and the controller’s maturity, so it is worth checking the current product and compatibility documentation.
GARM is another Kubernetes-friendly option to investigate. It can manage ephemeral runners, but it is a separate project and would need to be evaluated for compatibility with the exact Gitea version and workflow features you use.
Separate the concerns of running a job and building an image. A job image can provide Node, Python, Git, and test dependencies without requiring Docker at all. For OCI image builds inside Kubernetes, use a daemonless builder such as Kaniko or another actively maintained fork, Buildah, or a tool such as Dagger. These build images without requiring access to the host Docker daemon, though each has different support for caching, multi-stage builds, registries, and running containers.
A builder like Kaniko is mainly for building and pushing images; it is not a general replacement for a runtime that starts arbitrary service containers. If the workflow needs to run additional containers for integration tests, use Kubernetes services or a purpose-built runner environment rather than assuming the image builder can do that.
Dagger is useful when the pipeline needs portable build and test steps, and its engine can work with OCI-compatible runtimes. It is worth comparing with a simpler Kubernetes-native job design before adding another layer.

The right choice depends heavily on build volume and trust boundaries. A handful of trusted jobs can tolerate a privileged DinD pod, while frequent or untrusted builds should use isolated pods and carefully scoped service accounts.