K3s Embedded Registry Architecture and Implementation

The deployment of a private container registry within a K3s cluster represents a strategic architectural shift for infrastructure engineers. By integrating a local registry, organizations can effectively eliminate the systemic dependency on external registries such as Docker Hub, thereby mitigating the risks associated with network latency, external service outages, and rate-limiting policies. This architectural pattern is particularly critical for edge computing deployments, where bandwidth is often constrained, and air-gapped environments, where external network access is strictly prohibited for security reasons. The fundamental objective is to transform the image acquisition process from a remote-pull model to a local-access model, which significantly accelerates development cycles and enhances the reliability of pod scheduling across the cluster.

K3s utilizes containerd as its primary container runtime. Containerd is designed with native support for registry mirrors, allowing K3s to intercept requests for images from a remote source and redirect those requests to a local endpoint. While K3s does not provide a built-in registry server as part of its binary distribution, it is engineered for seamless integration with any OCI-compliant registry. The most common implementation utilizes the Docker Distribution registry, a lightweight and industry-standard tool that can be deployed as a pod within the cluster. This setup ensures that container images are stored in close proximity to the nodes that require them, reducing the time to "ready" for application pods.

K3s Registry Configuration Mechanics

The orchestration of image pulls in K3s is governed by the configuration of the container runtime. Because K3s relies on containerd, the mechanism for directing traffic to a local registry is handled through the definition of mirrors and configuration blocks. This configuration allows the cluster to treat a local registry as a proxy or a primary source, ensuring that the kubelet and containerd on every node are synchronized in how they resolve image endpoints.

The core of this system is the registries.yaml file, located at /etc/rancher/k3s/registries.yaml. This file acts as the source of truth for how K3s handles image resolution. The configuration is divided into two primary sections: mirrors and configs.

Mirrors allow the administrator to map a public registry hostname to one or more local endpoints. For example, when a deployment specifies an image from docker.io, K3s can be configured to first attempt a pull from a local IP address. The endpoints are processed as an ordered list; K3s attempts to pull the image from the first endpoint, and if that fails, it proceeds to the next in the sequence.

The configs section is where the specific properties of those endpoints are defined. This includes authentication credentials, such as usernames and passwords, and TLS settings. In internal or development environments, TLS verification is often disabled to simplify the setup of insecure registries. This is achieved using the insecure_skip_verify: true directive, which tells containerd to ignore the lack of a valid SSL certificate when communicating with the local registry.

Deploying a Local Registry in K3s

Since K3s does not include a built-in registry plugin, the Docker Distribution registry must be installed manually. This registry serves as the central repository for all container images used within the cluster. To ensure the registry is accessible and resilient, it must be deployed with proper namespace isolation and persistent storage.

The deployment process begins with the creation of a dedicated namespace. This ensures that registry-related resources are logically separated from application workloads.

```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
```

Persistent storage is a mandatory requirement for any production-grade registry. Without a PersistentVolumeClaim (PVC), any images pushed to the registry would be lost upon the restart of the registry pod. The registry stores all binary image layers and manifests in this volume. For a single-node registry setup, ReadWriteOnce access mode is sufficient.

```yaml

registry-pvc.yaml

Persistent storage for registry images

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: registry-data
namespace: registry
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
```

The requested storage size, such as 50Gi, should be adjusted based on the volume of images and the frequency of image updates. The PVC typically utilizes the cluster's default StorageClass, though specific classes like local-path can be defined for higher performance.

Once the infrastructure is in place, the registry is deployed. In many K3s environments, the registry is exposed via the default Traefik ingress controller. This allows the registry to be accessed via standard HTTP (port 80) and HTTPS (port 443).

Configuring K3s to Utilize the Local Registry

After the registry is deployed, the K3s nodes must be configured to recognize it. This involves creating the registries.yaml file on every single node in the cluster, including both server and agent nodes. Consistency across the cluster is vital; if one node lacks the configuration, it will attempt to pull images from the remote source, leading to inconsistent pull times and potential failures in air-gapped environments.

A common configuration pattern is to mirror multiple public registries to a single local endpoint. This concentrates image traffic through one point of entry.

```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 configuration, any request for images from docker.io, gcr.io, ghcr.io, or quay.io is redirected to the local address 127.0.0.1:30500. The insecure_skip_verify setting is critical here, as it allows the cluster to communicate with the local registry over HTTP without requiring complex certificate management.

Registry Authentication and TLS Configuration

Security for a local registry can be handled through authentication and TLS. While insecure registries are acceptable for local development, production environments require strict access controls.

Authentication is defined within the configs section of the registries.yaml file. This ensures that only authorized nodes or users can pull images from the private registry.

```yaml

Example authentication config in registries.yaml

configs:
"registry.local:5000":
auth:
username: admin
password: secure-password
tls:
insecureskipverify: true
```

The authentication credentials provided here are used by containerd when it attempts to authenticate with the registry endpoint. If the registry is hosted externally or in a different namespace, these credentials ensure that the image pull request is authorized.

Regarding TLS, K3s allows for the skipping of verification for internal registries. However, for full security, certificates should be issued and trusted by all nodes. If TLS is not configured, the registry must be explicitly marked as an insecure registry in the Docker or Podman configuration on the host machine to allow image pushes.

Registry Mirroring and Pull-Through Caching

A pull-through cache is a specialized registry configuration where the local registry acts as an intermediary. When a node requests an image, the local registry checks if it already possesses a copy. If the image exists, it is served immediately. If not, the registry fetches the image from the upstream source, caches it locally, and then serves it to the node.

This process is managed via a ConfigMap that defines the registry's behavior.

```yaml

registry-config.yaml

ConfigMap for registry pull-through cache configuration

apiVersion: v1
kind: ConfigMap
metadata:
name: registry-config
namespace: registry
data:
config.yml: |
version: 0.1
log:
level: info
fields:
service: registry
storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
cache:
blobdescriptor: inmemory
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
proxy:
# Enable pull-through cache for Docker Hub
remoteurl: https://registry-1.docker.io
```

The remoteurl parameter is the critical component here, as it defines the upstream registry. In the example above, the local registry is configured to proxy requests to the Docker Hub registry.

For organizations needing to mirror multiple upstream registries, deploying a single registry instance is insufficient because a standard Docker Distribution registry typically supports only one upstream proxy. To solve this, administrators must either deploy separate registry instances for each upstream source (e.g., one for Docker Hub, one for Quay.io) or utilize a more robust registry proxy such as Harbor.

Integrating with CI/CD Pipelines

A local K3s registry is most effective when integrated into a CI/CD pipeline. This allows images built in a CI environment to be pushed directly into the cluster's local registry, bypassing the need to push to a public repository first.

To enable this, the developer's environment or the CI runner must be configured to communicate with the local registry. If the registry is exposed via Traefik, the runner can push images using the registry.localhost hostname.

The workflow typically follows these steps:

  1. Build the image locally.
  2. Tag the image for the local registry.
  3. Push the image to the local registry.

For Docker users:

bash docker build -t registry.localhost/test:latest . docker push registry.localhost/test:latest

For Podman users, who must explicitly handle TLS verification for insecure registries:

bash podman build -t registry.localhost/test:latest . podman push --tls-verify=false registry.localhost/test:latest

To ensure these tools can communicate with the registry, the registry.localhost hostname must be resolvable. 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.

Automating Configuration Deployment

Manually creating the registries.yaml file on every node is error-prone and inefficient. A bash script utilizing SSH is the recommended method for deploying these configurations across a K3s cluster.

```bash

!/bin/bash

deploy-registry-config.sh

Deploys registries.yaml to all K3s nodes

List of all K3s nodes (servers and agents)

NODES=(
"k3s-server-1"
"k3s-server-2"
"k3s-server-3"
"k3s-agent-1"
"k3s-agent-2"
)

Registry configuration content

REGISTRYCONFIG='mirrors:
"docker.io":
endpoint:
- "http://127.0.0.1:30500"
"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:
"127.0.0.1:30500":
tls:
insecure
skip_verify: true'

for NODE in "${NODES[@]}"; do
echo "Configuring registry on $NODE..."
# Create directory if it does not exist
ssh "$NODE" "sudo mkdir -p /etc/rancher/k3s"
# Write configuration file
echo "$REGISTRY_CONFIG" | ssh "$NODE" "sudo tee /etc/rancher/k3s/registries.yaml > /dev/null"
# Restart K3s to apply configuration
# For server nodes
ssh "$NODE" "sudo systemctl restart k3s 2>/dev/null || true"
# For agent nodes
ssh "$NODE" "sudo systemctl restart k3s-agent 2>/dev/null || true"
echo "Completed $NODE"
done

echo "Registry configuration deployed to all nodes"
```

This script ensures that the directory /etc/rancher/k3s exists, writes the configuration, and restarts the K3s service (or agent) to ensure the changes are loaded into the container runtime.

Verifying and Troubleshooting the Registry

Once the configuration is deployed and the services are restarted, verification is required to ensure that images are actually being pulled from the local registry.

The first step is to verify that K3s has loaded the registry configuration. This can be checked using the crictl tool, which provides a command-line interface for CRI-compatible container runtimes.

```bash

Check that K3s loaded the registry configuration

sudo k3s crictl info | grep -A 20 "registry"
```

To test the mirror in action, attempt to pull a common image.

```bash

Pull an image to test the mirror works

sudo k3s crictl pull nginx:alpine
```

If the mirror is configured correctly, the image will be pulled from the local endpoint. To verify the image has been cached in the registry's catalog, a curl request can be sent to the registry API.

Common issues include DNS resolution failures where registry.localhost does not resolve to the correct IP, and permission errors when pushing images. If a push fails, verify that the host's Docker or Podman configuration includes the registry in the insecure-registries list.

Detailed Analysis of Registry Impact

The transition to an embedded registry within K3s is not merely a technical convenience but a strategic optimization of the container lifecycle. By analyzing the data flow, it becomes evident that the "Deep Drilling" of image acquisition removes the most significant bottleneck in K3s cluster scalability: the external network.

In a standard configuration, every node in a cluster pulls images independently. In a 10-node cluster, pulling a 500MB image results in 5GB of external network traffic. With a local registry acting as a pull-through cache, only the first node to request the image triggers an external pull. The subsequent nine nodes pull the image from the local network, reducing external bandwidth usage by 90%. This has a direct impact on the "Cold Start" time of applications, as local network speeds are orders of magnitude faster than internet-based pulls.

Furthermore, the use of registries.yaml allows for a tiered approach to image sourcing. By providing an ordered list of endpoints, administrators can implement a "Local -> Regional -> Global" pull strategy. This is particularly useful in geo-distributed K3s clusters, where a regional registry can serve multiple local clusters, further reducing latency.

The reliance on insecure_skip_verify represents a calculated trade-off. While it introduces a security gap by allowing unencrypted traffic, it removes the operational overhead of managing a Private Key Infrastructure (PKI) for internal tools. For organizations moving toward production, this should be replaced by the integration of a Certificate Authority (CA) that is trusted by the root store of all K3s nodes.

Ultimately, the K3s embedded registry pattern enables a truly autonomous cluster. Whether deployed on a Raspberry Pi cluster at the edge or a set of virtual machines in a private cloud, the ability to host, cache, and manage container images locally ensures that the cluster remains operational regardless of the state of the external internet.

Sources

  1. OneUptime Blog
  2. GitHub Gist - gdamjan

Related Posts