Create and deploy a Helm chart with nginx application using secure nginxinc image and resource configurations. Complete this lab in 10-15 minutes.
Prerequisites
Kubernetes cluster running (minikube, kind, or cloud cluster)
helm v3+ installed
kubectl configured to access the cluster
Setup: Create your namespace
On a shared cluster, work in your own namespace to avoid colliding with other
users. We name it after your numeric user id:
# A namespace unique to your user, e.g. 1000-helmexport NS="$(id -u)-helm"# Create it if it does not exist yet (idempotent)kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f -
Keep this NS variable exported for the rest of the lab: every helm and
kubectl command below targets it with -n "$NS".
Setup: Install metrics-server
Later steps (e.g. kubectl top) rely on the Kubernetes Metrics Server. Install it
only if it is not already present on the cluster:
# Check whether metrics-server is already installedif kubectl get deployment metrics-server -n kube-system &>/dev/null; then echo "metrics-server is already installed, skipping"else helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
helm repo update
helm upgrade --install metrics-server metrics-server/metrics-server \
-n kube-system \
--set "args={--kubelet-insecure-tls}" kubectl rollout status deployment/metrics-server -n kube-system --timeout=120s
fi
Note: --kubelet-insecure-tls is required on local clusters (kind, minikube)
whose kubelet serving certificates are self-signed.
Step 1: Create and examine the Helm chart
# Generate the base charthelm create demo-app
# Examine structurels demo-app/
# Check default templateshelm template demo-app | head -20
To understand the role of Helm, open one of the generated templates, for example
demo-app/templates/service.yaml:
cat demo-app/templates/service.yaml
Notice the {{ ... }} placeholders: a Helm template is not plain Kubernetes
YAML. Helm renders these templates by injecting the values defined in
values.yaml (and any -f override file or --set flag), then sends the
resulting manifests to the cluster. This separation between templates (the
structure) and values (the configuration) is the core idea behind Helm.
Step 2: Configure the unprivileged nginxinc image
We will deploy the nginxinc/nginx-unprivileged image instead of the standard
nginx image. The official nginx image runs as root and binds to port 80,
which is rejected by a hardened cluster (restricted Pod Security Standards,
read-only root, non-root user). The nginxinc/nginx-unprivileged variant runs
as a non-root user and listens on port 8080, so it works without elevated
privileges — a security best practice.
Rather than overwriting the chart’s default demo-app/values.yaml, we create a
small override file demo-app/values-nginxinc.yaml. An override file only lists
the values we actually change and inherits everything else from the chart
defaults. Keeping it minimal makes the demo much easier to maintain across Helm
chart version bumps (you diff only your own overrides, not a full copy of
values.yaml):
# Create a minimal override file with the unprivileged nginxinc imagecat > demo-app/values-nginxinc.yaml << 'EOF'
# Only the values we override; everything else comes from the chart defaults.
replicaCount: 1
image:
repository: nginxinc/nginx-unprivileged
pullPolicy: IfNotPresent
tag: "1.28.0-alpine3.21-perl"
service:
type: ClusterIP
port: 8080
livenessProbe:
httpGet:
path: /
port: 8080
readinessProbe:
httpGet:
path: /
port: 8080
EOF
Step 3: Deploy with configured values
# Install the chart with configured nginxinc image and resourceshelm install my-nginx ./demo-app -f demo-app/values-nginxinc.yaml -n "$NS"# List your Helm releases in the namespacehelm list -n "$NS"# Verify deploymentkubectl get pods,svc -n "$NS"kubectl describe pod -l app.kubernetes.io/name=demo-app -n "$NS" | grep -A5 "Limits\|Requests"
Step 4: Test the application
# Port forward to testkubectl port-forward service/my-nginx-demo-app 8080:8080 -n "$NS" &
# Test the nginx welcome pagecurl http://localhost:8080
# Stop port forwardingpkill -f "kubectl port-forward"
Step 5: Scale and update resources using –set
# Scale to 3 replicas with higher resourceshelm upgrade my-nginx ./demo-app -f demo-app/values-nginxinc.yaml -n "$NS"\
--set replicaCount=3\
--set resources.limits.cpu=200m \
--set resources.limits.memory=256Mi
# Verify scalingkubectl get pods -n "$NS"kubectl describe pod -l app.kubernetes.io/name=demo-app -n "$NS" | grep -A5 "Limits\|Requests"
# Deploy production configuration (layer prod values on top of the nginxinc base)helm upgrade my-nginx ./demo-app -f demo-app/values-nginxinc.yaml -f values-prod.yaml -n "$NS"# Verify configurationkubectl get pods -n "$NS"kubectl describe pod -l app.kubernetes.io/name=demo-app -n "$NS" | grep -A5 "Limits\|Requests"
Step 7: Validate and inspect
# Check current valueshelm get values my-nginx -n "$NS"# List releaseshelm list -n "$NS"# View generated manifestshelm template my-nginx ./demo-app -f demo-app/values-nginxinc.yaml -f values-prod.yaml | grep -A10 "kind: Deployment"# Check resource usagekubectl top pods -n "$NS"|| echo "Metrics server not available"
Step 8: Cleanup
# Remove the releasehelm uninstall my-nginx -n "$NS"# Verify cleanupkubectl get all -n "$NS"helm list -n "$NS"# Remove your namespacekubectl delete namespace "$NS"
Key Takeaways
In this 10-15 minute lab, you learned:
Templates vs Values: Inspected templates/service.yaml to see how Helm renders templates using values
Secure Image: Used the non-root nginxinc/nginx-unprivileged instead of standard nginx for better security
Resource Management: Configured CPU and memory limits/requests for efficient resource usage
Override Files: Kept the chart defaults untouched and layered values-nginxinc.yaml / values-prod.yaml for maintainable, upgrade-friendly configuration
Helm CLI: Used --set flags for quick configuration changes
Validation: Inspected generated manifests and applied resources