§6 deploy nginx kubectl
Source: examples/06-deploy-nginx-kubectl/ · ← Back to examples index
First workload-deployment example in the tutorial. A smoke test that
builds a small nginx image with a two-stage Containerfile (UBI 10
builder → UBI 10 Minimal runtime) with docker build, loads it into the
cluster with minikube image load, deploys it with a Deployment + NodePort
Service, reaches the Service at http://127.0.0.1:18080/, validates the
response content, then scales.
This isn’t a cluster-startup test (that was §3’s
examples/03-driver-check/). The cluster needs to already be
running — the script will start it for you if not, but the focus
here is building an image and deploying it.
Why we build our own image
Red Hat publishes nginx images at registry.access.redhat.com/ubi10/nginx-126
(and -122), but those are s2i (source-to-image) builder images
designed for OpenShift, not plain Kubernetes. Their default CMD
invokes /usr/libexec/s2i/run and expects content baked in via the
s2i workflow; running them as-is in Kubernetes crashloops with no
document root.
Rather than coax the s2i image into running directly (possible but
fragile), the right pattern is to build our own image from a base
UBI image. The Containerfile in this directory does that: a
builder stage (ubi10/ubi) stages content, a runtime stage
(ubi10/ubi-minimal) installs nginx, configures it for non-root +
port 8080, copies the staged content in, and runs nginx in the
foreground.
Both base images are freely redistributable — no subscription-manager registration needed to install packages from the public UBI repos.
What it tests
Seven claims that span §6’s scope:
docker build -f Containerfile -t nginx-custom:v1 .followed byminikube -p minikube image load nginx-custom:v1produces an image visible to the cluster’s containerd- The two-stage Containerfile builds cleanly on Docker Engine
kubectl apply -f manifests/ships both files cleanly- The Deployment reaches
Availablecondition within 3 minutes - The Service’s selector matches the Pod labels, so the Service has live endpoints
- The Service is
type: NodePortwithnodePort: 30080, and the profile publishes it (--ports=127.0.0.1:18080:30080, set when the profile was created).curl http://127.0.0.1:18080/returns the sentinel string from our baked-inindex.html kubectl scale --replicas=3brings the count to 3 Running Pods and the Deployment staysAvailablethroughout
On success, the corresponding §6 reconciliation rows promote.
Running
./demo.sh
Expected duration:
- First run: 2-4 minutes (downloads
ubi10/ubiandubi10/ubi-minimalbase images, ~250 MB combined; runsmicrodnf install nginx; builds final image) - Subsequent runs: 25-40 seconds (image cached in cluster’s local image cache; rebuild skipped if Containerfile hasn’t changed)
To force a rebuild from scratch:
minikube -p minikube image rm nginx-custom:v1
./demo.sh
What you should see
A series of ==> step lines for each phase, with build output and
kubectl output inlined. The build step is the most verbose — you’ll
see microdnf installing nginx and a few hundred kilobytes of
package metadata. The script finishes with
✓ SUCCESS — image built + Deployment + NodePort Service + scaling all working
on a clean pass.
Cluster scope
This demo uses your default minikube cluster (the minikube
profile), not a dedicated one. It assumes you’ve completed §3’s
profile setup: --driver=docker --container-runtime=containerd on
Docker Engine, with the published-port map CORE_PORTS. The demo calls
ensure_profile, which creates the profile (6 CPUs, 16 GB) if it is
missing and fails with the delete-and-recreate command if the ports are
not published.
The built image nginx-custom:v1 persists in the cluster’s image
cache across runs. To remove it: minikube -p minikube image rm nginx-custom:v1.
Shared slot. §6, §8 and §9 each use nodePort 30080 (host
127.0.0.1:18080), one at a time. The demo calls
require_free_nodeport 30080 and fails with a “delete X first”
message if another Service holds it.
SELinux note
This example doesn’t bind-mount anything from the host. The build
context is read by docker build and the resulting image
is referenced by name, not via host path. So no :Z flag is
needed here. When §8 adds hostPath PersistentVolumes, that’s
where :Z (and the broader SELinux story) becomes relevant.
Cleanup
demo.sh installs a trap cleanup EXIT that runs whether the
script succeeds or fails:
- Deletes the Deployment and Service via
kubectl delete -f(this frees nodePort 30080) - Does not delete the built image (kept for fast re-runs)
- Leaves the cluster running
If something pathological happens (script killed during cleanup), manual recovery:
kubectl --context minikube delete -f manifests/ --ignore-not-found=true
Re-running
Idempotent. The pre-flight step deletes any pre-existing nginx
Deployment/Service and waits briefly for terminating Pods to
finish before applying fresh. Docker’s layer cache makes the build fast when
nothing changed; the image is reloaded into the cluster each run.
When this fails
The most likely failure modes, in order:
docker buildfails on UBI image pull — transient network issue reachingregistry.access.redhat.com. Retrymicrodnf install nginxfails inside the build — UBI repos transiently unreachable. Retry- Cluster not reachable —
minikube statuserrors. The script will tryminikube startautomatically; if that fails, work the failure like §3 (driver or runtime config issue) - Pods stuck in
CrashLoopBackOff— most likely a permissions issue inside the container. The demo’skubectl waitfailure path automatically dumps logs from current and previous containers, which usually tells you exactly what nginx is complaining about. Common causes: misconfigured port (trykubectl describe pod <name>for the actual port), permission denied on a directory the non-root user can’t write - Image-pull failure on
nginx-custom:v1— minikube’s image cache didn’t pick up the build for some reason. Confirm withminikube -p minikube image ls | grep nginx-custom— if absent, rerundocker build -f Containerfile -t nginx-custom:v1 .andminikube -p minikube image load nginx-custom:v1from this directory (the Deployment usesimagePullPolicy: Never) nodePort 30080 is held by Service ...— another example holds the shared slot. Run thekubectl delete svccommand the script printsdoes not publish nodePort 30080— the profile was created without the port map. Run the delete-and-recreate command the script prints
For any of these, copy the failing output and share it back so the
iteration can produce an r07b fix-up.