Build, Tag and Push Container Images for Kubernetes

Build a container image with Podman or Docker, tag and push it to a registry, and reference it from a Kubernetes Pod or Deployment with imagePullPolicy.

Published

Updated

Read time 18 min read

Reviewed byDeepak Prasad

Build tag and push a container image for a Kubernetes Deployment
Tested on Rocky Linux 10.2 (Red Quartz) workstation
Package podman 5.8.2
kubectl 1.36.3
Applies to Any Linux host with Podman or Docker; any Kubernetes cluster
Cert prep CKAD · CKS
Lab environment Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm
Privilege Normal user for Podman and kubectl; sudo may be required to install Podman or configure node registry trust
Scope Containerfile basics, build, tag, push, ENTRYPOINT and CMD, multi-stage builds, image tags and digests, imagePullPolicy, deploying a custom image, optional commit patch, and preload with ctr import. Does not cover registry installation, imagePullSecrets, full ImagePullBackOff remediation, scanning, signing, or a full image-optimization guide.
Related guides Podman command cheat sheet
Deployments and rolling updates
Kubernetes pods

Kubernetes runs containers from images you publish to a registry—it does not build them for you. This guide walks through one demo-app nginx image: build on the workstation, push to a registry, deploy from spec.containers[].image, and optionally patch with commit for a quick lab iteration.


Quick workflow

The table below maps the workstation commands in this article. The imagePullPolicy section explains IfNotPresent before you deploy. Deploy and verify steps are in Use the image in Kubernetes.

Replace REGISTRY with a registry-qualified prefix such as docker.io/myuser, quay.io/myorg, or 192.168.56.108:5001 on the lab network.

Phase What it does Podman Docker (equivalent syntax)
Build Create an image from a Containerfile podman build -t demo-app:1.0.0 . docker build -f Containerfile -t demo-app:1.0.0 .
Test locally Confirm the image runs before you push podman run -d --name demo-test -p 8080:80 localhost/demo-app:1.0.0 then curl docker run -d --name demo-test -p 8080:80 demo-app:1.0.0 then curl
Tag Add a registry-qualified name to the local image podman tag localhost/demo-app:1.0.0 "${IMAGE_REF}" docker tag demo-app:1.0.0 "${IMAGE_REF}"
Push Upload the tagged image to a registry nodes can pull podman push --tls-verify=false "${IMAGE_REF}" docker push "${IMAGE_REF}" after insecure-registry config for HTTP labs
Deploy Run the pushed image in the cluster kubectl apply -f demo-app-deployment.yaml Same
Verify image Confirm Pods use ${IMAGE_REF} and the pulled digest kubectl get pods -n image-demo -l app=demo-app -o custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image Same

Build the container image

A build reads a Containerfile and produces a local container image stored on your workstation in Podman or Docker image storage. Podman finds Containerfile in the build context automatically. With Docker, pass -f Containerfile.

Instruction What it does Why it matters
FROM Choose the base image for the first layer Sets OS, runtime, and starting size
WORKDIR Set the default directory inside the image Keeps later COPY and RUN paths predictable
COPY Copy files from the build context into the image Ships your app files into the image
RUN Execute a command during the build Installs packages, compiles code, sets permissions
EXPOSE Document a port the container listens on Documents intent; does not publish the port on the host
USER Run later steps and the default container process as this user Avoids running as root when not required
ENTRYPOINT Set the main executable for the container Defines what process starts when the container runs
CMD Provide default arguments to ENTRYPOINT Supplies defaults the user or orchestrator can override

Create a project directory and application file:

bash
mkdir demo-app && cd demo-app

Create and save as index.html:

html
<!DOCTYPE html>
<html><body><h1>demo-app 1.0.0</h1></body></html>

Create Containerfile:

dockerfile
FROM docker.io/library/nginx:1.28.3-alpine

WORKDIR /usr/share/nginx/html
COPY index.html .
RUN chmod 644 index.html
EXPOSE 80
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]

Line by line:

  • FROM pulls a small nginx base image that already includes the web server binary.
  • WORKDIR sets the directory nginx serves by default.
  • COPY places your index.html into that directory at build time.
  • RUN chmod 644 index.html sets file permissions inside the image during the build. Every RUN creates a new image layer.
  • EXPOSE 80 documents that the container listens on port 80.
  • ENTRYPOINT ["nginx"] starts nginx as the main process.
  • CMD ["-g", "daemon off;"] keeps nginx in the foreground so the container stays running.

Create .containerignore when you build with Podman. When you use Docker, save the same patterns in .dockerignore. Docker reads .dockerignore to exclude files from the build context. Podman reads .containerignore (and also accepts .dockerignore).

text
.git
*.log
README.md

Do not put these in the build context:

  • Registry passwords or API tokens
  • kubeconfig files
  • Private keys or TLS material you only need at runtime

In a Pod spec:

  • command replaces Docker ENTRYPOINT
  • args replaces Docker CMD

For example, command: ["nginx"] and args: ["-g", "daemon off;"] mirror the Containerfile. For overrides, probes, and environment wiring, see Commands, args and environment variables.

Build from a Containerfile

Your project directory should look like this before you build:

bash
ls -la
output
total 12
drwxr-xr-x. 2 user user 4096 Jul 25 19:00 .
drwxr-xr-x. 8 user user 4096 Jul 25 18:55 ..
-rw-r--r--. 1 user user   24 Jul 25 18:56 .containerignore
-rw-r--r--. 1 user user  186 Jul 25 18:58 Containerfile
-rw-r--r--. 1 user user   58 Jul 25 18:57 index.html

With Docker, use .dockerignore instead of .containerignore and keep the same patterns.

Run the build from that directory:

bash
docker build -f Containerfile -t demo-app:1.0.0 .
podman build -t demo-app:1.0.0 .
  • -t (--tag) names the image as repository:tag — here demo-app is the image name and 1.0.0 is the tag
  • . is the build context. The COPY index.html . instruction reads index.html from this directory.

With Docker, pass -f Containerfile because Docker does not look for that filename by default.

The result is a local image on your workstation. Podman lists it as localhost/demo-app:1.0.0. The next section runs it locally before you tag and push.

Sample output (trimmed):

output
STEP 1/7: FROM docker.io/library/nginx:1.28.3-alpine
...
Successfully tagged localhost/demo-app:1.0.0

List matching images and confirm architecture before you push to amd64 cluster nodes:

bash
podman images demo-app
podman image inspect --format '{{.Id}} {{.Architecture}}' localhost/demo-app:1.0.0

Sample output:

output
REPOSITORY          TAG       IMAGE ID      CREATED        SIZE
localhost/demo-app  1.0.0     1f4c09d19c7b  1 second ago   63.5 MB

Sample output from podman image inspect (trimmed):

output
1f4c09d19c7bebce6aad4caf29fed6c0b8f4e03bfb7487c0d2238a1f660b3a7f amd64

That value is the local image ID ({{.Id}}). It is not the registry manifest digest you get from podman push --digestfile.

Docker equivalents: docker images demo-app and docker image inspect.


Test the image locally

Before you tag and push, confirm the image runs on your workstation. A quick local run catches wrong ENTRYPOINT/CMD or missing files before anything reaches the registry.

Publish port 80 from the container to a host port:

bash
podman run -d --name demo-test -p 8080:80 localhost/demo-app:1.0.0

Docker equivalent:

bash
docker run -d --name demo-test -p 8080:80 demo-app:1.0.0

Request the page with the curl command:

bash
curl -s http://127.0.0.1:8080/

Sample output:

output
<!DOCTYPE html>
<html><body><h1>demo-app 1.0.0</h1></body></html>

That response matches the index.html you copied in the Containerfile. Stop and remove the test container when you finish:

bash
podman stop demo-test
bash
podman rm demo-test

Docker equivalents: docker stop demo-test and docker rm demo-test.


Tag the image

A tag is the version label after the colon in an image reference, such as 1.0.0 in demo-app:1.0.0. Tags are human-readable pointers. They help you and Kubernetes refer to a specific build without typing the full image ID.

Before you push, the image exists only under a local name such as localhost/demo-app:1.0.0. Tagging adds another name that includes the registry hostname and repository path. It does not duplicate image layers on disk; it creates an additional reference to the same local image.

Set the registry variables once. You reuse ${IMAGE_REF} when you push and in the Kubernetes manifest:

bash
REGISTRY="192.168.56.108:5001"
IMAGE_REF="${REGISTRY}/demo-app:1.0.0"

A full registry reference looks like this:

text
${IMAGE_REF}

Expanded on the lab network:

text
192.168.56.108:5001/demo-app:1.0.0
Part Example Purpose
Registry host registry.example.com or 192.168.56.108:5001 Where nodes pull the image
Repository path team/demo-app or demo-app Groups related images in the registry
Tag 1.0.0 Identifies this build or release

Tag the built image for the lab registry:

bash
podman tag localhost/demo-app:1.0.0 "${IMAGE_REF}"

Docker equivalent:

bash
docker tag demo-app:1.0.0 "${IMAGE_REF}"

Prefer 1.0.0, 1.0.1, or a Git commit id over latest. latest is a normal tag, not a promise of recency, and Kubernetes treats it differently for default pull policy.

Do not confuse these three identifiers:

  • Tag — movable label such as 1.0.0 in ${IMAGE_REF}
  • Local image ID — short hash from podman images; unique to each build on your workstation
  • Registry digest — immutable manifest hash from podman push --digestfile

Capture the registry digest when you push in the next section if you need an exact, pinned reference.

Reference by digest in a manifest when you need immutability:

yaml
image: ${REGISTRY}/demo-app@sha256:1b2df623153d38d6845a4b71c3d55bc6347a7212365fdd6ffa326b1c13a79e57

Push the image to a registry

A registry is the shared store cluster nodes pull from. Pushing uploads the tagged ${IMAGE_REF} image so worker nodes can download the same layers when a Pod starts.

Logging in on your workstation does not authenticate cluster nodes. Nodes pull with their own credentials or anonymous access rules.

  • Lab HTTP registry (192.168.56.108:5001): no login; use podman push --tls-verify=false and configure node trust — see Private registry and imagePullSecrets
  • HTTPS registry (Docker Hub, Quay.io): run podman login first; omit --tls-verify=false on push

For Docker Hub, Quay.io, or another authenticated private registry:

bash
podman login quay.io

Docker equivalent: docker login quay.io.

Push the tagged image and capture the manifest digest:

bash
podman push --tls-verify=false --digestfile=demo-app.digest "${IMAGE_REF}"

For HTTPS registries such as Docker Hub or Quay.io, omit --tls-verify=false.

Docker equivalent for HTTPS registries:

bash
docker push "${IMAGE_REF}"

For the HTTP lab registry on port 5001, docker push "${IMAGE_REF}" works only after you configure Docker Engine to allow 192.168.56.108:5001 as an insecure registry. Docker does not support Podman's --tls-verify=false flag on docker push.

Sample output (trimmed):

output
Copying blob sha256:a2738b08d711...
Copying config sha256:1f4c09d19c7b...
Writing manifest to image destination

Read the digest and build a digest-pinned reference:

bash
DIGEST=$(cat demo-app.digest)
echo "${REGISTRY}/demo-app@${DIGEST}"

Sample output:

output
192.168.56.108:5001/demo-app@sha256:1b2df623153d38d6845a4b71c3d55bc6347a7212365fdd6ffa326b1c13a79e57

List tags through the registry API to confirm the push:

bash
curl -s "http://${REGISTRY}/v2/demo-app/tags/list"

Sample output:

output
{"name":"demo-app","tags":["1.0.0"]}

For registry setup, authentication, and imagePullSecrets, use Private registry and imagePullSecrets instead of repeating that workflow here.


imagePullPolicy

imagePullPolicy tells the kubelet whether to contact the registry when a Pod starts, or to use a copy already cached on the node. The Deployment below sets imagePullPolicy: IfNotPresent.

IfNotPresent cache-found vs cache-miss branches plus Always and Never pull paths

For IfNotPresent, the kubelet checks the node cache first:

  • Cache found — start the Pod from the local copy; no registry contact
  • Cache miss — pull from the registry, then start the Pod

With Always, the kubelet asks the container runtime to resolve the image name through the registry whenever the container starts. If the resolved image layers already exist locally, the runtime can reuse them instead of downloading them again. Never uses only the local copy and does not contact the registry.

Policy What the kubelet does When to use it
Always Resolves the image through the registry on every container start; reuses local layers when they already match Moving tags such as latest you want re-resolved
IfNotPresent Pulls only when the image is not already cached on the node Version tags such as 1.0.0 on multi-node clusters
Never Does not pull; the image must already exist on the node Preloaded or ctr-imported images on the node

When imagePullPolicy is omitted, Kubernetes sets a default when the Pod template is first created:

  • Image tag latest or no tag: Always
  • Other tags: IfNotPresent
  • Image referenced by digest: IfNotPresent

Changing the image tag on an existing Deployment does not automatically change a stored imagePullPolicy. Set the policy explicitly in course examples so behaviour stays visible.

Versioned tags such as 1.0.0 pair naturally with IfNotPresent on nodes that already cached the image. Use Always when you intentionally re-resolve a moving tag. For pull failures and private registry auth, see Private registry and imagePullSecrets.


Use the image in Kubernetes

After ${IMAGE_REF} is in the registry, reference that same value from spec.containers[].image in a Pod or Deployment manifest. The kubelet on each scheduled node pulls the image (according to imagePullPolicy) and starts the container.

Save demo-app-deployment.yaml with the image reference you pushed:

bash
cat > demo-app-deployment.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-app
  namespace: image-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: demo-app
  template:
    metadata:
      labels:
        app: demo-app
    spec:
      containers:
        - name: demo-app
          image: ${IMAGE_REF}
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
EOF

The image field uses the same ${IMAGE_REF} you tagged and pushed (192.168.56.108:5001/demo-app:1.0.0 on the lab network).

containerPort documents the port for Services and some health checks. Omitting it does not stop a process from listening inside the container.

Create the namespace and apply the manifest:

bash
kubectl create namespace image-demo
kubectl apply -f demo-app-deployment.yaml
kubectl rollout status deployment/demo-app -n image-demo --timeout=120s

kubectl apply creates the Deployment, ReplicaSet, and Pods. kubectl rollout status waits until the Pods are ready so the next checks do not show Pending or ContainerCreating.

Confirm the Pods are Running and reference the image you pushed. Check these in order:

  • IMAGE column matches ${IMAGE_REF}
  • imageID shows the registry digest the kubelet pulled
  • curl confirms the built file responds (below)
bash
kubectl get pods -n image-demo -l app=demo-app -o custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image,STATUS:.status.phase,NODE:.spec.nodeName

Sample output:

output
NAME                        IMAGE                                    STATUS    NODE
demo-app-7c4fffd5b5-bpr58   192.168.56.108:5001/demo-app:1.0.0     Running   worker01
demo-app-7c4fffd5b5-jx8cp   192.168.56.108:5001/demo-app:1.0.0     Running   worker01

The IMAGE column should match ${IMAGE_REF}. To see the digest the kubelet resolved at runtime:

bash
kubectl get pods -n image-demo -l app=demo-app -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[0].imageID}{"\n"}{end}'

Sample output (trimmed):

output
demo-app-7c4fffd5b5-bpr58   192.168.56.108:5001/demo-app@sha256:1b2df623153d...
demo-app-7c4fffd5b5-jx8cp   192.168.56.108:5001/demo-app@sha256:1b2df623153d...

Both Pods should report the same imageID digest. That confirms the node pulled the manifest you pushed, not a different local tag.

In this lab run both Pods landed on worker01. Every node that may run this workload must be able to pull ${IMAGE_REF}.

Verify the image serves your files

Forward a local port to the Deployment and request the page from your build:

bash
kubectl port-forward -n image-demo deployment/demo-app 8080:80

In another terminal:

bash
curl -s http://127.0.0.1:8080/

Sample output:

output
<!DOCTYPE html>
<html><body><h1>demo-app 1.0.0</h1></body></html>

That response comes from index.html in your Containerfile build. Together with the IMAGE and imageID checks above, that confirms the cluster is running the image you pushed. Stop port-forward with Ctrl+C when you finish.


Build with multiple stages

This section is optional. The demo-app nginx walkthrough above is already a single-stage build. Use multiple stages when you compile code or need build tools that should not ship in the final image.

Do not confuse stages with the steps Podman prints during a build (STEP 1/7, STEP 2/7, and so on). Each line in a Containerfile is one instruction and one build step. A stage starts only when you add another FROM instruction.

Term What it means Example in this article
Step One instruction in the Containerfile COPY, RUN, EXPOSE — the nginx demo has 7 steps in one stage
Stage One FROM block through the next FROM (or end of file) The Go multi-stage example has 2 stages: builder and the final Alpine stage
Layer A filesystem change stored in the image Each RUN, COPY, and ADD usually adds a layer

A single-stage build has one FROM. Every package you install with RUN stays in the final image you push to production, even when you only needed those tools during the build.

A multi-stage build has two or more FROM instructions. Earlier stages can compile code or prepare files. The final stage becomes the image you tag and push. Use COPY --from=builder (or --from=0) to copy only the files you need. Compilers, headers, and package managers stay in the builder stage and do not ship to the cluster.

Combining commands in one RUN with && reduces layers inside a stage. It does not create or remove stages. Multi-stage builds and fewer layers solve different problems.

Approach Final image contains Best for
Single-stage Runtime plus every build-time package Quick demos and simple static sites
Multi-stage Only the last stage (runtime files you copied in) Compiled apps and smaller production images

Compare image size with a compiled app

The nginx demo-app above is already small. To see a large size gap, compare a Go binary built in a full toolchain image against a multi-stage copy into Alpine.

Save main.go:

go
package main
import "fmt"
func main() { fmt.Println("demo-app") }
Output

Single-stage build (toolchain stays in the final image):

dockerfile
FROM docker.io/library/golang:1.23-bookworm
WORKDIR /src
COPY main.go .
RUN go mod init demo && go build -o /demo-app .
CMD ["/demo-app"]

Save as Containerfile.go-single and build:

bash
podman build -f Containerfile.go-single -t demo-go-single:1.0.0 .

Multi-stage build (two stages: compile in builder, ship only the binary in the final Alpine stage):

dockerfile
FROM docker.io/library/golang:1.23-bookworm AS builder
WORKDIR /src
COPY main.go .
RUN go mod init demo && go build -o /demo-app .

FROM docker.io/library/alpine:3.21
COPY --from=builder /demo-app /demo-app
CMD ["/demo-app"]

Save as Containerfile.go-multistage and build:

bash
podman build -f Containerfile.go-multistage -t demo-go-multistage:1.0.0 .

Compare sizes:

bash
podman images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep -E 'demo-go-single|demo-go-multistage'

Sample output:

output
localhost/demo-go-multistage:1.0.0 10.3 MB
localhost/demo-go-single:1.0.0 895 MB

The single-stage image keeps the entire Go compiler and Debian base. The multi-stage image has only two stages; Podman may print more than two step numbers because it counts instructions in each stage. The final image copies only the static binary into Alpine.


Modify an existing image and commit

The main path in this article is Containerfile → build → tag → push → deploy. This section continues with the same localhost/demo-app:1.0.0 image you built and deployed above. Use podman commit (or docker commit) only when you need a quick lab patch and cannot rebuild from a Containerfile right away.

podman run starts a container from a local image. The image layers stay read-only; changes you make inside the running container live in a thin writable layer on top. commit saves that writable layer as a new image tag. Stopping or removing the container does not change the committed image.

Start a container you can edit

Start from the same local image you built in Build from a Containerfile. Run nginx with its default ENTRYPOINT and CMD so the committed image still starts the web server normally:

bash
podman run -d --name demo-edit localhost/demo-app:1.0.0

Docker equivalent:

bash
docker run -d --name demo-edit demo-app:1.0.0

Runtime --entrypoint replaces the image entrypoint and clears its default command. A commit can preserve those overrides in the new image, so avoid them when you want nginx to start as defined in the Containerfile.

Images without a shell (for example scratch or distroless) cannot be patched this way. Rebuild from a Containerfile or use a debug stage in a multi-stage build.

Patch files inside the container

Attach an interactive shell:

bash
podman exec -it demo-edit sh

Docker equivalent: docker exec -it demo-edit sh.

Alpine-based images ship sh, not always bash. Use bash only when the image includes it.

Patch a file from the host without an interactive session:

bash
podman exec demo-edit sh -c 'echo patched > /usr/share/nginx/html/patch.txt'

If the image runs as a non-root user and you need root to edit system paths, add --user 0 to podman run or podman exec:

bash
podman exec -u 0 -it demo-edit sh

Commit, then stop and remove the container

Save the writable layer as a new local image:

bash
podman commit demo-edit localhost/demo-app:1.0.1

Docker equivalent:

bash
docker commit demo-edit demo-app:1.0.1

commit creates the image. It does not stop the editing container. Clean up the container in separate steps:

bash
podman stop demo-edit
podman rm demo-edit

Docker equivalent:

bash
docker stop demo-edit
docker rm demo-edit

Tag and push the patched image

Tag and push the committed image with a new reference. Keep ${REGISTRY} from earlier and set a separate variable for the patched tag:

bash
PATCH_IMAGE_REF="${REGISTRY}/demo-app:1.0.1"
podman tag localhost/demo-app:1.0.1 "${PATCH_IMAGE_REF}"
podman push --tls-verify=false "${PATCH_IMAGE_REF}"

Docker equivalent:

bash
docker tag demo-app:1.0.1 "${PATCH_IMAGE_REF}"
docker push "${PATCH_IMAGE_REF}"

Confirm both tags are in the registry:

bash
curl -s "http://${REGISTRY}/v2/demo-app/tags/list"

Sample output:

output
{"name":"demo-app","tags":["1.0.0","1.0.1"]}

Redeploy and verify the patch

Update the Deployment to use the patched image:

bash
kubectl set image deployment/demo-app -n image-demo demo-app="${PATCH_IMAGE_REF}"
kubectl rollout status deployment/demo-app -n image-demo --timeout=120s

Confirm the Pods now reference 1.0.1:

bash
kubectl get pods -n image-demo -l app=demo-app -o custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image,STATUS:.status.phase

Sample output:

output
NAME                        IMAGE                                STATUS
demo-app-774b4b6b8d-6mmzf   192.168.56.108:5001/demo-app:1.0.1   Running
demo-app-774b4b6b8d-7dpm7   192.168.56.108:5001/demo-app:1.0.1   Running

Check the patched file inside a running Pod:

bash
kubectl exec -n image-demo deploy/demo-app -- cat /usr/share/nginx/html/patch.txt

Sample output:

output
patched

The original index.html from your 1.0.0 build is still present. Only patch.txt was added by the commit step. Prefer updating the Containerfile and rebuilding when you need a repeatable production image.


Preload an image without a registry

The main path in this article is build → tag → push → deploy. When worker nodes cannot reach a registry, for example in an air-gapped lab, export the image on your workstation and import it into containerd on each node. The lab cluster uses containerd, so podman load on a node does not make the image visible to the kubelet.

On your workstation:

bash
podman save -o demo-app-1.0.0.tar "${IMAGE_REF}"

Copy the archive to every eligible worker node, then on each containerd node:

bash
sudo ctr -n k8s.io images import demo-app-1.0.0.tar
sudo crictl images | grep demo-app

Containerd's Kubernetes CRI plugin uses the k8s.io containerd namespace. Deploy with imagePullPolicy: Never or IfNotPresent so the kubelet uses the imported copy.

Docker equivalents on the workstation: docker save and copy the tar. Import on the node still uses ctr -n k8s.io images import when the cluster runtime is containerd.


Troubleshoot container image problems

Use this table when a build, commit, push, or cluster pull fails.

Symptom Likely cause Fix
Image builds but the container exits immediately Wrong ENTRYPOINT/CMD or missing foreground process Override entrypoint for debugging; check podman logs
Application port is not reachable Application is not listening on the expected port, or no Kubernetes Service or port-forward is configured Confirm the process listens inside the container; check Pod events and Service config
Incorrect ENTRYPOINT or CMD Process path or arguments wrong in the image podman inspect and a local podman run
Registry push is denied Not logged in, wrong repository name, or insufficient token scope podman login and full IMAGE_REF

References


Summary

Build an image from a Containerfile, tag it with a registry-qualified name your nodes can reach, push it, and reference that value in spec.containers[].image. Set imagePullPolicy before you deploy so kubelet behaviour matches how you publish updates: IfNotPresent for explicit version tags from a private registry, Always when the cluster must re-resolve a tag, or IfNotPresent/Never after you preload the image on each node with podman save and ctr -n k8s.io images import.

Verify the running workload with pod IMAGE and imageID columns, then curl or exec against the Service—not rollout status alone. For a quick lab iteration you can podman commit a running container into a new local tag, push it, and redeploy; prefer rebuild-and-push for changes you intend to keep. Registry authentication and repeated pull failures are covered in the private registry and imagePullSecrets guide.


Frequently Asked Questions

1. Can Kubernetes use an image that exists only on my workstation?

Not directly from your workstation's Podman or Docker storage. For a multi-node cluster, normally push the image to a registry. Another option is to preload the image on every eligible node and use IfNotPresent or Never.

2. Do I need Docker to build images for Kubernetes?

No. Kubernetes runs OCI-compatible images built with Podman, Docker, or other tools. This walkthrough uses Podman on Rocky Linux; Docker commands appear where syntax matches.

3. Should I deploy workloads with the latest image tag?

Prefer explicit version tags such as 1.0.0. When imagePullPolicy is omitted and a Pod template is first created with the latest tag, Kubernetes defaults to Always. That policy does not change automatically if you later edit the tag on an existing workload.

4. Where do registry credentials belong for a private repository?

On the cluster, not on your laptop login alone. Create imagePullSecrets in the workload namespace or link a ServiceAccount. See the private registry guide for setup and troubleshooting.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)