DEV Community

Cover image for From Docker Build to Kubernetes Deploy on Ubuntu: The Image Workflow That Never Changed
James Joyner
James Joyner

Posted on

From Docker Build to Kubernetes Deploy on Ubuntu: The Image Workflow That Never Changed

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"]
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode
spec:
  imagePullSecrets:
    - name: regcred
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)