Amid all the noise about dockershim, one thing got lost: the everyday workflow of building an image with Docker and running it on Kubernetes never changed. Docker is still an excellent build tool, Kubernetes still runs OCI images, and on Ubuntu the loop is clean. Here it is end to end.
1. A build-friendly Dockerfile
Multi-stage keeps the runtime image small and the attack surface low — this matters more on Kubernetes, where you pull the image onto every node that schedules the pod:
# build stage
FROM golang:1.22 AS build
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/api ./cmd/api
# runtime stage — distroless, no shell, tiny
FROM gcr.io/distroless/static:nonroot
COPY --from=build /out/api /api
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/api"]
2. Build and push with Docker on Ubuntu
Use buildx (bundled with modern Docker) so you can build multi-arch — worth it if any nodes are arm64:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry.example.com/api:1.4.2 \
--push .
Tag with an immutable version, never rely on :latest. Kubernetes caches images per node; :latest makes "which build is actually running?" unanswerable and breaks rollbacks.
3. A deployment that behaves in production
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector: { matchLabels: { app: api } }
template:
metadata: { labels: { app: api } }
spec:
containers:
- name: api
image: registry.example.com/api:1.4.2 # the exact tag you pushed
imagePullPolicy: IfNotPresent
ports: [{ containerPort: 8080 }]
resources:
requests: { cpu: "100m", memory: "128Mi" }
limits: { memory: "256Mi" }
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 3
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 10
The readinessProbe is the piece people skip and regret: without it, Kubernetes sends traffic to a pod before your app is listening, and you get intermittent 502s during every rollout.
kubectl apply -f deployment.yaml
kubectl rollout status deployment/api
4. Pulling from a private registry
If your registry needs auth, the cluster needs a pull secret — this is the same regardless of whether nodes run containerd or cri-dockerd:
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=ci \
--docker-password="$REGISTRY_TOKEN"
spec:
imagePullSecrets:
- name: regcred
Miss this and pods sit in ImagePullBackOff with pull access denied.
5. Ship a new build
docker buildx build -t registry.example.com/api:1.4.3 --push .
kubectl set image deployment/api api=registry.example.com/api:1.4.3
kubectl rollout status deployment/api
# rollback is one command because you used immutable tags:
kubectl rollout undo deployment/api
The mental model
- Docker = how you build and push images. Unchanged, still great.
- containerd / cri-dockerd = how the node runs them. This is what dockershim's removal was about.
- Your manifests = don't care which runtime is underneath.
If you keep those three separate in your head, none of the 2022 runtime drama affects your daily loop.
When a rollout goes wrong it's usually the image pull or the probe: ImagePullBackOff, and the wider Docker runtime error guides for build-side failures.
Last in the series: when the node or the runtime itself is the problem — troubleshooting kubelet and containerd on Ubuntu.
Top comments (0)