I'm building a small homelab CI/CD environment with Gitea 1.27.0 running on Kubernetes 1.34.10. The cluster has one control-plane node and two workers running Ubuntu 26.04, with containerd 2.2.2 as the runtime. Docker and nerdctl are not installed, although ctr and /run/containerd/containerd.sock are available. The Gitea Actions runner, using gitea/act_runner:latest, runs successfully as a Kubernetes Deployment and registers correctly.
At the moment, the runner uses the label ubuntu-latest:host, so jobs execute inside the runner container itself. A workflow that uses actions/checkout@v4 fails with "Cannot find: node in PATH" because the runner image does not include Node.
The usual alternative appears to be a label such as ubuntu-latest:docker://node:22-bookworm, which runs the job in a separate container. However, that execution mode expects Docker-compatible API access, while my Kubernetes nodes only expose containerd. I would prefer not to install Docker on the workers solely to support CI jobs when Kubernetes already creates containers through containerd.
The immediate requirement is to check out a repository and run tests in an isolated environment containing tools such as Node, Python, Git, and build dependencies. Later, the pipeline should build and push an OCI image, after which Argo CD will deploy it.
Is there a supported way for act_runner to create job containers directly through containerd? If not, is Docker on a worker the normal solution, or would Docker-in-Docker inside the runner pod be preferable? Would Kubernetes-native runner management, such as a Gitea-compatible runner controller or dynamically created runner pods, be a better fit? I'm looking for a clean Kubernetes-oriented architecture rather than just a workaround for the missing Node binary.
4 Answers
A Kubernetes-native runner design is probably the cleanest long-term direction if you want each job isolated in its own pod. A controller can provision short-lived runner pods, with the job image selected from the workflow or runner configuration. Those pods are created through the Kubernetes API, so the cluster can continue using containerd normally and no Docker daemon has to be installed on the workers.
Check which runner-controller integration is available for your Gitea edition and version. Some controller integrations are enterprise features, while other projects, such as GARM, may require additional setup and compatibility testing. If you use one, keep the runner pods ephemeral, restrict their service accounts, and separate privileged image-building jobs from ordinary test jobs.
Docker-in-Docker can work without installing Docker on the Kubernetes hosts, but it is a compromise rather than a particularly Kubernetes-native design. The runner pod needs a Docker daemon, usually with elevated privileges or carefully configured storage, and the job containers then run inside that daemon’s environment. It can be acceptable for a small, trusted homelab with a low build rate, but consider daemon startup time, storage cleanup, certificate configuration, cache management, and the security impact of privileged workloads.
For many builds, a dedicated runner pod or node with a Docker-compatible service is simpler than trying to make the host’s containerd socket behave like Docker.
For building OCI images, you do not necessarily need Docker-in-Docker. Tools such as a maintained Kaniko fork, BuildKit, or Dagger can build images from a Kubernetes job without requiring a Docker daemon. Kaniko is mainly a build tool, though—it does not replace a general-purpose job container runtime for arbitrary workflow steps. Dagger or BuildKit may be a better fit if the pipeline needs reproducible builds and broader OCI functionality.
A practical split is to run ordinary tests in purpose-built runner images and use a dedicated rootless or otherwise restricted image-builder step for the image build and push.
The standard container execution mode is built around a Docker-compatible API, not the Kubernetes node runtime socket directly. Having containerd.sock and ctr available does not automatically make it usable by act_runner: ctr is a low-level client, and the runner’s Docker backend expects Docker API behavior, including image handling, networking, mounts, and lifecycle operations. Kubernetes also normally keeps its container runtime interface separate from workloads, so exposing the host containerd socket to a runner would create significant privilege and security concerns.
For a straightforward setup, use a runner environment that already contains the required tools, or provide a Docker-compatible engine specifically for the runner. Installing Docker on a worker is possible, but it adds another runtime and should be isolated to a dedicated node if you choose that route.

Related Questions
Can't Load PhpMyadmin On After Server Update
Redirect www to non-www in Apache Conf
How To Check If Your SSL Cert Is SHA 1
Windows TrackPad Gestures