How to Deploy a Docker Container to Kubernetes

How to Deploy a Docker Container to Kubernetes

Reading time1 min
#kubernetes#docker#containers#devops

How to Deploy a Docker Container to Kubernetes

Docker and Kubernetes do different jobs. Docker builds images and runs containers on one machine. Kubernetes runs those images across a cluster and keeps the requested number of copies alive. You never deploy a Dockerfile or a container to Kubernetes. You deploy an image built from the Dockerfile, and Kubernetes creates the containers.

Since Kubernetes 1.24, nodes no longer use Docker Engine as their runtime. They run containerd or CRI-O. That changes nothing for you: images built with docker build are standard OCI images and run unchanged.

How Docker concepts map to Kubernetes

DockerKubernetes
docker run my-appA Pod, created and managed by a Deployment
--restart always, several copiesDeployment replicas, rescheduling on failure
-p 8080:80A Service (plus Ingress or Gateway for HTTP)
-e KEY=value, .env fileConfigMap and Secret
Named volumePersistentVolumeClaim
compose.yamlA set of manifests, or a Helm chart

Step 1: build and push the image

A cluster pulls images from a registry, so the image must be in one the nodes can reach:

docker build -t ghcr.io/example/my-app:1.0.0 .
docker push ghcr.io/example/my-app:1.0.0

Use a version tag, not latest. With latest, Kubernetes always pulls (imagePullPolicy: Always), you cannot tell which build is running, and rollbacks become guesswork. If your laptop is ARM and the nodes are x86, build with --platform linux/amd64.

Step 2: get a cluster

For local testing:

  • kind runs a cluster inside Docker containers: kind create cluster.
  • minikube: minikube start.
  • Docker Desktop can enable a single-node cluster in its settings.

Local clusters can use images you built locally without pushing them, once you load them in:

kind load docker-image ghcr.io/example/my-app:1.0.0
minikube image load ghcr.io/example/my-app:1.0.0

In the cloud, EKS, GKE, AKS and DOKS give you a managed control plane. kubectl talks to all of them the same way. Check which cluster you are pointed at with kubectl config current-context.

Step 3: the quick way

kubectl create deployment my-app --image=ghcr.io/example/my-app:1.0.0 --replicas=2 --port=8080
kubectl expose deployment my-app --port=80 --target-port=8080
kubectl get pods -l app=my-app
kubectl port-forward service/my-app 8080:80

Open http://localhost:8080. This is fine for experiments. For anything you keep, use manifest files in git, so the cluster state is reviewable and reproducible. Generate a starting point instead of typing YAML from memory:

kubectl create deployment my-app --image=ghcr.io/example/my-app:1.0.0 --port=8080 \
  --dry-run=client -o yaml > deployment.yaml

Step 4: manifests for real use

deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app
          image: ghcr.io/example/my-app:1.0.0
          ports:
            - containerPort: 8080
          envFrom:
            - configMapRef:
                name: my-app-config
            - secretRef:
                name: my-app-secrets
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              memory: 256Mi
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080

What each part buys you:

  • selector and labels connect the Deployment to its Pods and the Service to the same Pods. A mismatch is the most common reason a Service has no endpoints.
  • requests tell the scheduler how much room a Pod needs. The memory limit stops one Pod from starving the node.
  • The readiness probe keeps traffic away from a Pod until it answers, which makes rolling updates seamless. The liveness probe restarts a hung container. Keep it lenient, or it will restart containers that are merely slow.

Create the config and apply:

kubectl create configmap my-app-config --from-literal=LOG_LEVEL=info
kubectl create secret generic my-app-secrets --from-literal=DB_PASSWORD=change-me
kubectl apply -f deployment.yaml
kubectl rollout status deployment/my-app

Kubernetes Secrets are only base64-encoded. In production, feed them from a secrets manager (External Secrets Operator, Sealed Secrets or your cloud's CSI driver) instead of committing them.

Step 5: expose it

  • ClusterIP (the default above): reachable only inside the cluster. Use kubectl port-forward for testing.
  • LoadBalancer: on a cloud cluster, type: LoadBalancer provisions a cloud load balancer with a public IP. On minikube, run minikube tunnel or minikube service my-app.
  • Ingress or Gateway API: one entry point with HTTP routing by host and path, plus TLS. Most production HTTP apps use this.
  • NodePort: opens a port on every node. Mostly useful for local clusters.

Private registries

If the image is private, give the cluster credentials:

kubectl create secret docker-registry regcred \
  --docker-server=ghcr.io \
  --docker-username=my-user \
  --docker-password="$GHCR_TOKEN"

Then reference it in the Pod spec:

spec:
  imagePullSecrets:
    - name: regcred

On managed clusters pulling from the same cloud's registry (EKS from ECR, GKE from Artifact Registry), node IAM permissions usually replace this secret.

Updates and rollbacks

Change the image tag in deployment.yaml and apply it again, or do it imperatively:

kubectl set image deployment/my-app my-app=ghcr.io/example/my-app:1.1.0
kubectl rollout status deployment/my-app
kubectl rollout undo deployment/my-app      # back to the previous version
kubectl scale deployment/my-app --replicas=4

The Deployment replaces Pods gradually and waits for each new one to pass its readiness probe. A broken image therefore stalls the rollout while the old Pods keep serving traffic.

Coming from Docker Compose

Kompose converts a Compose file into Kubernetes manifests:

kompose convert -f compose.yaml

Treat the output as a draft. Add probes and resource requests, replace bind mounts with PersistentVolumeClaims or ConfigMaps, and drop depends_on. In Kubernetes, the app has to retry until its dependencies are up.

Troubleshooting

kubectl get pods
kubectl describe pod <pod-name>        # events at the bottom explain most failures
kubectl logs <pod-name> --previous     # logs of the last crashed container
kubectl get endpointslices -l kubernetes.io/service-name=my-app
  • ImagePullBackOff / ErrImagePull: wrong image name or tag, missing pull secret, or the node cannot reach the registry. With kind or minikube, the image was not loaded, or it is tagged latest and Kubernetes tries to pull it.
  • CrashLoopBackOff: the container starts and exits. Check kubectl logs --previous. Usually missing config or a wrong command.
  • Pending: no node has enough CPU or memory for the requests, or a PersistentVolumeClaim cannot bind.
  • Service returns nothing: labels do not match the selector, or targetPort is not the port the app listens on.

Quick reference

docker build -t REGISTRY/my-app:TAG . && docker push REGISTRY/my-app:TAG
kind load docker-image REGISTRY/my-app:TAG          # local clusters only
kubectl create deployment my-app --image=REGISTRY/my-app:TAG --port=8080 --dry-run=client -o yaml > deployment.yaml
kubectl apply -f deployment.yaml
kubectl expose deployment my-app --port=80 --target-port=8080
kubectl rollout status deployment/my-app
kubectl set image deployment/my-app my-app=REGISTRY/my-app:NEW_TAG
kubectl rollout undo deployment/my-app