Self-Contained CI/CD: Images That Never Leave the Cluster

Mar 17, 2026

The standard CI/CD setup for a Kubernetes workload involves GitHub Actions building an image, pushing to DockerHub or ECR, and ArgoCD pulling from there. That works, but it means images live outside the cluster, you need registry credentials in multiple places, and every build consumes GitHub Actions minutes.

There’s a tighter setup where images never leave the cluster network. GitHub triggers the pipeline but never touches an image.


The Components

The Flow

1. Developer pushes code to GitHub
2. GitHub Actions workflow triggers
3. Job is claimed by ARC runner pod (inside the cluster)
4. Runner builds: go build ./... && go test ./...
5. Runner pushes image to Gitea OCI registry
   docker push gitea.internal.svc:3000/admin/myapp:v0.2
6. ArgoCD Image Updater detects new tag in Gitea registry
7. Image Updater commits updated manifest back to Gitea
   cluster/apps/myapp/deployment.yaml: image tag v0.1 → v0.2
8. ArgoCD detects manifest change, deploys new version

GitHub receives a push event and fires the workflow. After that, everything runs inside the cluster. The runner pod has direct access to the cluster network — it can push to Gitea’s OCI registry at an internal address, no tunnels or credentials needed.

Why ARC Instead of GitHub-Hosted Runners

GitHub-hosted runners run on GitHub’s infrastructure. They have no network access to your cluster. To push an image from a GitHub-hosted runner to an in-cluster registry, you’d need to expose the registry externally, add TLS, manage credentials. That’s the opposite of what we want.

ARC runs the runner as a pod inside the cluster. The runner IS on the cluster network. It can reach internal services by their Kubernetes DNS names. No external exposure needed.

ARC Setup

ARC uses a ScaleSet that registers with GitHub as a self-hosted runner for a specific repository:

# helm values for actions-runner-controller
githubConfigUrl: "https://github.com/myorg/myrepo"
githubConfigSecret: arc-github-pat

controllerServiceAccount:
  name: arc-controller
  namespace: arc-systems

The GitHub PAT only needs repo and workflow scopes. It’s stored in a K8s Secret (or ExternalSecret from Vault), never in the repository.

Workflows reference the runner with:

jobs:
  build:
    runs-on: self-hosted

Gitea as OCI Registry

Gitea supports OCI-compatible container registries on the same port as the Git API. No separate registry service needed.

# Build in the runner pod
FROM golang:1.22 AS builder
COPY . .
RUN go build -o /app ./cmd/myapp

FROM gcr.io/distroless/static
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
docker build -t gitea-http.gitea.svc.cluster.local:3000/admin/myapp:${TAG} .
docker push gitea-http.gitea.svc.cluster.local:3000/admin/myapp:${TAG}

The push target is a cluster-internal DNS name. No credentials needed from the runner because it authenticates using a token stored in a mounted Secret.

ArgoCD Image Updater

Image Updater watches the Gitea OCI registry for new tags matching a pattern. When it finds one, it updates the image tag in the manifest and commits back to Gitea. ArgoCD picks up the commit and deploys.

# Annotation on the ArgoCD Application
annotations:
  argocd-image-updater.argoproj.io/image-list: myapp=gitea-http.gitea.svc.cluster.local:3000/admin/myapp
  argocd-image-updater.argoproj.io/myapp.update-strategy: latest
  argocd-image-updater.argoproj.io/write-back-method: git

The entire promotion from “image pushed” to “running in cluster” is automated with no external systems involved.

What This Costs

GitHub Actions minutes: zero. The job is claimed by the self-hosted runner immediately. GitHub records a workflow run but the compute happens inside your cluster.

External registry: none. Gitea serves both Git and OCI on the same hostname.

Registry credentials to manage: one K8s Secret (or ExternalSecret from Vault) shared between the runner and Image Updater.

Takeaway

The self-hosted runner pod is the key piece. Once the runner has direct cluster network access, every other step becomes simpler — the registry can be internal, credentials stay inside the cluster, no external exposure needed. GitHub becomes a trigger and a source of truth for code, not a participant in the build or deployment process.