K3s Embedded Registry Integration

The implementation of a private container registry within a K3s ecosystem represents a critical architectural shift for organizations seeking to eliminate external dependencies and optimize image distribution. By deploying a local source for container images, K3s eliminates the latency associated with fetching assets from remote providers like Docker Hub. This architectural decision is particularly impactful for edge deployments where bandwidth may be constrained, air-gapped environments where external network access is strictly prohibited for security reasons, and rapid development cycles where image pull speeds directly correlate with deployment velocity. While K3s does not provide a built-in registry server as a native binary, it is engineered for seamless integration with any OCI-compliant registry. The most efficient approach for this integration involves deploying the lightweight Docker Distribution registry within the cluster, allowing the container runtime to pull images from a local endpoint rather than traversing the public internet.

Architectural Foundations of K3s Registry Configuration

K3s utilizes containerd as its primary container runtime. This choice is pivotal because containerd possesses native support for registry mirrors, which allows the system to redirect requests for images from a public registry to a specified local mirror. This redirection occurs at the runtime level, meaning the Kubernetes orchestration layer remains agnostic to where the image is physically stored, provided the runtime can resolve the endpoint.

The configuration process is centered around a specific file located at /etc/rancher/k3s/registries.yaml. Upon startup, K3s scans for the existence of this file; if found, the settings contained within are used to generate the underlying containerd configuration. This mechanism ensures that the cluster's image-pulling behavior is consistent across all nodes.

The configuration structure consists of two primary components:

  • Mirrors: This section defines a mapping between a public registry (such as docker.io) and a list of endpoints. When a request is made for an image from the mirrored registry, K3s attempts to pull from the listed endpoints in sequence. If all endpoints fail, it falls back to the default endpoint.
  • Configs: This section manages the operational parameters for the registry endpoints. It allows the administrator to define authentication credentials, custom TLS certificates, or instructions to skip TLS verification for insecure registries.

Deploying a Local Docker Distribution Registry

To establish a functional private registry, a registry server must be deployed within the K3s cluster. The Docker Distribution registry is the recommended choice due to its lightweight footprint and high reliability. The deployment process requires careful consideration of namespace isolation and persistent storage to ensure that image data is not lost during pod restarts or cluster updates.

The deployment process begins with the creation of a dedicated namespace to encapsulate the registry components.

```yaml

registry-namespace.yaml

Creates dedicated namespace for registry components

apiVersion: v1
kind: Namespace
metadata:
name: registry
labels:
app.kubernetes.io/name: registry
app.kubernetes.io/component: infrastructure
```

Once the namespace is established, persistent storage must be provisioned. Without a PersistentVolumeClaim (PVC), the registry would operate on ephemeral storage, meaning every image pushed to the registry would be deleted if the pod were rescheduled or restarted.

```yaml

registry-pvc.yaml

Persistent storage for registry images

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: registry-data
namespace: registry
spec:
accessModes:
# ReadWriteOnce is sufficient for single-node registry
- ReadWriteOnce
resources:
requests:
# Adjust size based on expected image storage needs
storage: 50Gi
```

The use of ReadWriteOnce is sufficient for a single-node registry deployment. The storage size (e.g., 50Gi) should be scaled based on the number of images and the size of the layers being stored. Administrators can specify a storageClassName: local-path to utilize the default K3s local storage provider.

For users seeking an alternative installation method via a Gist-based approach, the registry can be exposed through the default Traefik ingress controller on ports 80 and 443. This allows the registry to be accessed via a domain such as registry.localhost.

bash kubectl create namespace docker-registry kubectl apply -f docker-registry.yaml -n docker-registry

Configuring K3s for Private Registry Utilization

After the registry is deployed, K3s must be informed of its existence and how to interact with it. This is achieved by creating the registries.yaml file on every node in the cluster. This requirement is absolute; if a node is missing this file, it will continue to attempt pulls from the public internet, bypassing the local mirror.

This applies to server nodes as well. Because server nodes in K3s are schedulable by default, they will run workloads and pull images. If server nodes have not been tainted to prevent pod scheduling, they must also possess the registries.yaml configuration.

Implementing the Mirror Configuration

The registries.yaml file defines which public registries should be mirrored. For instance, to mirror Docker Hub, GCR, GHCR, and Quay, the configuration would look as follows:

```yaml

/etc/rancher/k3s/registries.yaml

Configure K3s to use local registry as mirror for Docker Hub

mirrors:
# Mirror Docker Hub
"docker.io":
endpoint:
# Use NodePort to access registry from any node
- "http://127.0.0.1:30500"
# Also mirror common registries
"gcr.io":
endpoint:
- "http://127.0.0.1:30500"
"ghcr.io":
endpoint:
- "http://127.0.0.1:30500"
"quay.io":
endpoint:
- "http://127.0.0.1:30500"
configs:
# Configuration for the local registry endpoint
"127.0.0.1:30500":
tls:
# Allow insecure connections for local registry
insecureskipverify: true
```

In this scenario, the endpoint points to a NodePort (http://127.0.0.1:30500), allowing the runtime on the node to communicate with the registry service via a local loopback. The configs section is critical here; because the local registry is often deployed without a trusted SSL certificate, insecure_skip_verify: true is required to prevent the runtime from rejecting the connection.

Image Migration and Mirroring Procedures

Moving images from a public registry to a private K3s registry requires a host equipped with Docker or another OCI-compatible tool capable of pulling and pushing images. The process involves a three-step workflow: pulling, retagging, and pushing.

The workflow is as follows:

  1. Obtain the k3s-images.txt file from the official GitHub repository for the specific K3s release version.
  2. Pull the images listed in the text file.
    docker pull docker.io/rancher/mirrored-pause:3.6
  3. Retag the image to point to the private registry endpoint.
    docker tag docker.io/rancher/mirrored-pause:3.6 registry.example.com:5000/rancher/mirrored-pause:3.6
  4. Push the retagged image to the private registry.
    docker push registry.example.com:5000/rancher/mirrored-pause:3.6

This process ensures that all required K3s system images are available locally, which is a mandatory step for air-gapped environments.

Advanced Registry Configurations and Scaling

For larger environments, a single registry instance may become a bottleneck or a single point of failure. Implementing specialized pull-through caches or high-availability registries is necessary to maintain performance.

Pull-Through Cache Deployment

A pull-through cache acts as an intermediary. When an image is requested, the cache checks if it has the image; if not, it pulls it from the upstream source, stores it locally, and serves it to the client. This significantly reduces repeated external traffic.

Example of a Docker Hub pull-through cache deployment:

```yaml

registry-dockerhub.yaml

Dedicated pull-through cache for Docker Hub

apiVersion: apps/v1
kind: Deployment
metadata:
name: registry-dockerhub
namespace: registry
spec:
replicas: 1
selector:
matchLabels:
app: registry-dockerhub
template:
metadata:
labels:
app: registry-dockerhub
spec:
containers:
- name: registry
image: registry:2.8
ports:
- containerPort: 5000
env:
- name: REGISTRYPROXYREMOTEURL
value: "https://registry-1.docker.io"
volumeMounts:
- name: data
mountPath: /var/lib/registry
volumes:
- name: data
persistentVolumeClaim:

claimName: registry-dockerhub-data

apiVersion: v1
kind: Service
metadata:
name: registry-dockerhub
namespace: registry
spec:
type: ClusterIP
ports:
- port: 5000
targetPort: 5000
selector:
app: registry-dockerhub
```

When using multiple specialized mirrors, the registries.yaml file must be updated to map each public registry to its corresponding internal service.

```yaml

/etc/rancher/k3s/registries.yaml

Multiple registry mirrors configuration

mirrors:
# Docker Hub mirror
"docker.io":
endpoint:
- "http://registry-dockerhub.registry.svc.cluster.local:5000"
# GitHub Container Registry mirror
"ghcr.io":
endpoint:
- "http://registry-ghcr.registry.svc.cluster.local:5000"
# Google Container Registry mirror
"gcr.io":
endpoint:
- "http://registry-gcr.registry.svc.cluster.local:5000"
# Private registry - direct access
"registry.company.com":
endpoint:
- "https://registry.company.com"
```

Handling Insecure Registries and Localhost Resolution

In many local development setups, the registry is accessed via registry.localhost. This requires the host operating system to resolve the domain correctly. On most Linux distributions, resolvectl query registry.localhost should return 127.0.0.1. If this is not the case, systemd-resolved must be enabled and configured.

When pushing images to an insecure local registry, the client tool must be configured to ignore TLS requirements.

For Docker users:
The http://registry.localhost endpoint must be added to the insecure registries list in the Docker daemon configuration.

For Podman users:
The --tls-verify=false flag must be used during the push operation.
podman push --tls-verify=false registry.localhost/test:latest

Troubleshooting and Log Analysis

When images fail to pull from a private registry, the primary point of failure is usually the pod's scheduling location. Because K3s is a distributed system, logs are stored on the specific node where the pod was scheduled.

To identify the correct node for log analysis, use the following command:
kubectl get pod -o wide -n NAMESPACE POD

By checking the NODE column, administrators can SSH into the specific machine to inspect the containerd logs and verify if the registries.yaml configuration is being applied.

Default Endpoint Fallback Logic

It is important to understand that containerd employs an implicit "default endpoint" for all registries. This default endpoint is always attempted as a last resort, regardless of the endpoints listed in registries.yaml.

Key characteristics of the default endpoint include:
- It is only tried if all mirrored endpoints fail.
- Rewrites are not applied to pulls against the default endpoint.
- For docker.io, the default endpoint is https://index.docker.io/v2.
- For a custom registry like registry.example.com:5000, the default endpoint is https://registry.example.com:5000/v2.

Registry Specification Comparison

The following table compares the different deployment strategies for registries within a K3s environment.

Feature Local Docker Distribution Pull-Through Cache Enterprise Registry (Harbor)
Complexity Low Medium High
Storage Persistent Volume Persistent Volume Distributed Storage
Bandwidth Impact Low (Local) Medium (Cached) Low (Local)
Air-Gap Support Full Partial Full
Auth Support Basic Basic Advanced (RBAC)
Setup Time Minutes Minutes Hours

Analysis of Registry Implementation

The integration of a private registry into K3s is not merely a matter of convenience but a strategic necessity for production-grade edge and air-gapped environments. The reliance on containerd for mirror management allows K3s to maintain a lean architecture while providing the flexibility to route traffic to any OCI-compliant endpoint.

The most significant risk in this architecture is the configuration drift between nodes. Since registries.yaml must be present on every node (including servers), any discrepancy in the mirror list or the TLS settings will lead to inconsistent pod startup times or image pull failures. This necessitates the use of configuration management tools like Ansible or Terraform to ensure uniform deployment of the registries.yaml file across the entire cluster.

Furthermore, the shift from public registries to internal mirrors transforms the image-pulling process from a network-bound operation to a disk-I/O-bound operation. While this dramatically increases speed, it shifts the burden of availability to the registry's persistent storage. If the registry-data PVC fails or the underlying storage is slow, the entire cluster's ability to scale or recover from pod failures is compromised. Therefore, the use of high-performance storage classes (such as NVMe-backed local paths) is highly recommended for the registry's persistent volume.

Ultimately, the ability to mirror multiple upstream sources (docker.io, gcr.io, ghcr.io) into a single internal endpoint creates a centralized hub for container assets. This centralization simplifies security auditing, as administrators can scan images within the local registry before they are distributed to the nodes, thereby enhancing the overall security posture of the K3s cluster.

Sources

  1. OneUptime
  2. K3s Documentation
  3. GitHub Gist - gdamjan

Related Posts