18.3.1 Kubernetes Image Compatibility
A focused guide to Kubernetes Image Compatibility, connecting core concepts with practical Docker and container operations.
Kubernetes image compatibility covers the practical translation gaps that appear when an image built and tested with Docker, including its Dockerfile-level configuration, actually gets deployed to a Kubernetes cluster, since several Docker-specific concepts have no automatic equivalent in Kubernetes and need to be explicitly re-expressed in the cluster's own manifest format.
HEALTHCHECK is silently ignored by Kubernetes
A HEALTHCHECK instruction baked into a Dockerfile has no effect at all once that image runs as a Kubernetes pod; Kubernetes uses its own, entirely separate probe configuration, defined in the pod manifest rather than the image itself, and an image relying solely on its Dockerfile's HEALTHCHECK for health monitoring will have no health checking at all once deployed to Kubernetes unless probes are explicitly added to the manifest:
HEALTHCHECK --interval=15s CMD curl -f http://localhost:3000/healthz || exit 1
livenessProbe:
httpGet:
path: /healthz
port: 3000
periodSeconds: 15
readinessProbe:
httpGet:
path: /healthz
port: 3000
The actual health check logic, the endpoint and what it verifies, can and should remain conceptually identical; what changes is where that check is configured, moving from the Dockerfile to the Kubernetes manifest's livenessProbe and readinessProbe fields specifically, which serve subtly different purposes, restart versus traffic routing, that Docker's single HEALTHCHECK instruction does not distinguish between at all.
Resource limit translation
Docker's --memory and --cpus flags correspond to Kubernetes's resources.limits and resources.requests fields, but the concepts are not perfectly identical, since Kubernetes additionally distinguishes between a requested (guaranteed minimum) amount and a limit (hard ceiling), a distinction Docker's simpler flag-based configuration does not make as explicitly:
docker run --memory=512m --cpus=1 my-api
resources:
requests:
memory: "256Mi"
cpu: "0.5"
limits:
memory: "512Mi"
cpu: "1"
Translating a Docker-tested memory and CPU configuration into Kubernetes requires deciding explicitly on both a request and a limit value, rather than assuming the single Docker figure maps directly onto just one of these two distinct Kubernetes concepts.
Image pull secrets versus docker login
Authentication to a private registry, handled through docker login for local development and CI, requires a distinctly configured Kubernetes secret referenced explicitly by the pod specification, since Kubernetes nodes do not automatically inherit or share credentials from wherever an image happened to be built:
docker login registry.example.com
kubectl create secret docker-registry my-registry-secret \
--docker-server=registry.example.com \
--docker-username=user \
--docker-password=pass
imagePullSecrets:
- name: my-registry-secret
Forgetting this step is a common cause of a pod failing to start specifically due to an image pull authentication failure, despite the same image pulling successfully through docker login during local development or CI.
Multi-architecture considerations for mixed-node clusters
A Kubernetes cluster with nodes of different CPU architectures, a mix of x86 and ARM-based nodes, for instance, requires the deployed image to actually have a manifest list covering every architecture present in the cluster, since Kubernetes will schedule a pod onto any node matching its other constraints and then fail if that specific node's architecture has no corresponding image variant available:
docker buildx build --platform=linux/amd64,linux/arm64 -t my-api --push .
nodeSelector:
kubernetes.io/arch: amd64
A nodeSelector constraint can restrict scheduling to nodes of a specific architecture if a genuinely multi-architecture image is not available, but building and publishing the actual multi-architecture manifest list is the more complete, generally preferable fix.
Entrypoint and command field mapping
Docker's ENTRYPOINT and CMD map to Kubernetes's command and args fields respectively, though the exact override semantics differ in subtle ways worth confirming directly rather than assuming an exact equivalence:
ENTRYPOINT ["node"]
CMD ["server.js"]
command: ["node"]
args: ["server.js"]
An image whose entrypoint and command behavior was only ever tested through docker run should have this translated mapping verified directly against the actual Kubernetes manifest, rather than assumed correct purely based on the conceptual similarity between the two systems' equivalent fields.
Common mistakes
- Assuming a Dockerfile's
HEALTHCHECKinstruction provides any health monitoring once deployed to Kubernetes, rather than configuring explicit liveness and readiness probes in the manifest. - Translating a single Docker resource flag directly into only one of Kubernetes's two distinct request and limit fields, without deciding deliberately on both.
- Forgetting to configure an
imagePullSecretfor a private registry image, assumingdocker logincredentials somehow carry over to the Kubernetes cluster automatically. - Deploying a single-architecture image to a cluster with mixed-architecture nodes without either building a genuine multi-architecture manifest or constraining scheduling with a node selector.
- Assuming Dockerfile
ENTRYPOINTandCMDbehavior translates identically to Kubernetescommandandargswithout verifying the actual override semantics directly.
Kubernetes image compatibility is generally strong at the level of the image's actual filesystem content, since the OCI standard ensures that, but several Docker-specific configuration concepts, health checks, resource limits, registry authentication, architecture targeting, entrypoint semantics, have no automatic translation and need to be explicitly, deliberately re-expressed in the cluster's own manifest format rather than assumed to carry over from Docker-level configuration.