# Overview > What hello-kubernetes is, what its page shows, and what people use it for. hello-kubernetes is a small web app to deploy on a Kubernetes cluster. Its page shows: - a message, **Hello world!** by default - the namespace, pod and node that answered - the container image and its architecture Refresh the page, and another pod can answer. That makes it a simple way to show how a Service spreads requests across pods and nodes, to test an Ingress or a Gateway, or to check a new cluster. ## What you get | Part | What it is | | - | - | | **The image** | `ghcr.io/paulbouwer/hello-kubernetes` on GHCR, which we recommend, and `paulbouwer/hello-kubernetes` on Docker Hub, for `linux/amd64` and `linux/arm64`. It runs as the non-root user 1000, with signed provenance and an SBOM. | | **The Helm chart** | `oci://ghcr.io/paulbouwer/charts/hello-kubernetes`. Secure by default, and it can create an Ingress or a Gateway API HTTPRoute for the app. | | **The release file** | `hello-kubernetes.yaml`: the chart rendered with its defaults, for `kubectl apply` or as a Kustomize base. | ## Where to start - [Quick start](https://paulbouwer.github.io/hello-kubernetes/docs/start-here/quick-start.md): deploy it, reach it and remove it, in three commands. - [With Helm](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/helm.md), [with kubectl](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/kubectl.md) or [with Kustomize](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/kustomize.md): every option for each way to deploy. - [Behind an Ingress](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/ingress.md) or [behind a Gateway](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/gateway.md): serve it under a path, such as `/app/hello`. - [Config builder](https://paulbouwer.github.io/hello-kubernetes/docs/configure/builder.md): choose your options, and get the commands and files, with a preview of the page. - [Show load balancing](https://paulbouwer.github.io/hello-kubernetes/docs/guides/load-balancing.md): watch a Service send requests to several pods. - [Upgrading from 1.10](https://paulbouwer.github.io/hello-kubernetes/docs/reference/upgrading.md): what changed, and how to upgrade. --- # Quick start > Deploy hello-kubernetes, reach it, and remove it, with kubectl or Helm. Deploy hello-kubernetes into your current namespace, with its default settings. You need `kubectl` and a cluster; Helm also needs [Helm 4](https://helm.sh/docs/intro/install/). **kubectl** ```bash kubectl apply -f https://github.com/paulbouwer/hello-kubernetes/releases/latest/download/hello-kubernetes.yaml kubectl rollout status deployment/hello-kubernetes-demo ``` **Helm** ```bash helm install demo oci://ghcr.io/paulbouwer/charts/hello-kubernetes --wait ``` It creates a ServiceAccount, a Service and a Deployment with 2 pods, all named `hello-kubernetes-demo`. ## Reach the app Forward a local port to the Service: ```bash kubectl port-forward service/hello-kubernetes-demo 8080:80 ``` Open `http://localhost:8080/`. You see **Hello world!**, and the namespace, pod and node that answered. Press Ctrl+C to stop. `port-forward` connects to one pod, so the pod name stays the same when you refresh. Through the Service’s external address, each request can reach a different pod. The Service is a `LoadBalancer`, so on many clusters it also gets an external address. See [where the external address works](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/kubectl.md#where-the-external-address-works). To watch requests reach different pods, see [Show load balancing](https://paulbouwer.github.io/hello-kubernetes/docs/guides/load-balancing.md). ## Remove it **kubectl** ```bash kubectl delete -f https://github.com/paulbouwer/hello-kubernetes/releases/latest/download/hello-kubernetes.yaml ``` **Helm** ```bash helm uninstall demo ``` --- # With Helm > Install hello-kubernetes from the OCI Helm chart, set values, pin a version, upgrade and uninstall. The chart is published on GHCR as an OCI artifact, with the same version as the app and the image. It is also listed on [Artifact Hub](https://artifacthub.io/packages/search?repo=hello-kubernetes). | Needs | Installs into | You get | | - | - | - | | `kubectl`, [Helm 4](https://helm.sh/docs/intro/install/) and a cluster | the namespace you choose | a ServiceAccount, a Service (`LoadBalancer` by default) and a Deployment with 2 pods, all named `hello-kubernetes-`. An Ingress or an HTTPRoute, if you turn one on. | ## Install Install the chart as the release `hello-world`, in a new `hello-kubernetes` namespace: ```bash helm install hello-world oci://ghcr.io/paulbouwer/charts/hello-kubernetes \ --namespace hello-kubernetes --create-namespace \ --wait ``` You see `STATUS: deployed`, and notes that say how to reach the app for the Service type you chose. To install a specific version, add `--version`, for example `--version 1.11.0`. Without it, Helm installs the newest release. ## Set values Set values with `--set`, or put them in a values file: **--set** ```bash helm install custom-message oci://ghcr.io/paulbouwer/charts/hello-kubernetes \ --namespace hello-kubernetes --create-namespace \ --set-string message="I just deployed this on Kubernetes!" \ --set service.type=ClusterIP \ --wait ``` **values.yaml** *File: values.yaml* ```yaml message: "I just deployed this on Kubernetes!" service: type: ClusterIP ``` With a values file, install with `--values values.yaml`. Use `--set-string` for the message, so Helm keeps it as text even if it looks like a number or `yes`. In a values file, quote it. `--set` and `--set-string` read some characters as syntax, even inside quotes: a comma starts the next value, braces make a list, and a backslash escapes the next character. Put a backslash before each of them, as in `--set-string message='Hello\, Kubernetes!'`, or put text like this in a values file. The [config builder](https://paulbouwer.github.io/hello-kubernetes/docs/configure/builder.md) escapes them for you. Helm checks your values against the chart’s schema on every install and upgrade. A misspelled or unknown key, a wrong type or a value that isn’t allowed stops the install, with a message that names the value, for example `additional properties 'pathPrefx' not allowed`. Editors that read JSON Schema show each value’s description and allowed values. Every value is in the [values reference](https://paulbouwer.github.io/hello-kubernetes/docs/configure/values.md). To see them from the chart itself: ```bash helm show values oci://ghcr.io/paulbouwer/charts/hello-kubernetes ``` ## Verify Forward a local port to the Service: ```bash kubectl port-forward service/hello-kubernetes-custom-message 8080:80 --namespace hello-kubernetes ``` Open `http://localhost:8080/`. You see **I just deployed this on Kubernetes!**. With a `LoadBalancer` Service, `kubectl get service` shows its external address; see [where the external address works](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/kubectl.md#where-the-external-address-works). To check that the chart is the one this repo published, verify its signed provenance. `helm pull` prints the chart’s digest: ```bash helm pull oci://ghcr.io/paulbouwer/charts/hello-kubernetes --version 1.11.0 gh attestation verify oci://ghcr.io/paulbouwer/charts/hello-kubernetes@ --repo paulbouwer/hello-kubernetes ``` ## Upgrade Change a value, or move to a newer chart, with `helm upgrade`. `--reset-then-reuse-values` starts from the new chart’s defaults, then applies the values you set before: ```bash helm upgrade custom-message oci://ghcr.io/paulbouwer/charts/hello-kubernetes \ --namespace hello-kubernetes --reset-then-reuse-values \ --set-string message="Upgraded!" \ --wait ``` The pods are replaced one at a time, and the page then shows **Upgraded!**. Use `--reset-then-reuse-values`, not `--reuse-values`. `--reuse-values` also reuses the old chart’s defaults, so a value that is new in the chart is missing, and the upgrade can fail. Moving from 1.10, it does. See [Upgrading from 1.10](https://paulbouwer.github.io/hello-kubernetes/docs/reference/upgrading.md). ## Uninstall ```bash helm uninstall hello-world --namespace hello-kubernetes helm uninstall custom-message --namespace hello-kubernetes kubectl delete namespace hello-kubernetes ``` `helm list --all-namespaces` shows the releases, if you don’t remember their names. ## Requirements - Helm 4. The chart is tested with Helm 4 only. - Kubernetes 1.35 or newer. CI tests 1.35 and the current release, on kind and k3d. - `linux/amd64` or `linux/arm64` nodes. --- # With kubectl > Deploy hello-kubernetes from the release file with kubectl alone, and reach it on any cluster. Each release has `hello-kubernetes.yaml`: the [Helm chart](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/helm.md) rendered with its default values and the release name `demo`. You don’t need Helm. | Needs | Installs into | You get | | - | - | - | | `kubectl` and a cluster | your current namespace, or the one you give with `--namespace` | the app with its default settings | The file creates: | Resource | Name | Notes | | - | - | - | | ServiceAccount | `hello-kubernetes-demo` | | | Service | `hello-kubernetes-demo` | `LoadBalancer`, port `80` | | Deployment | `hello-kubernetes-demo` | 2 pods, the image `paulbouwer/hello-kubernetes` at the release’s version, user `1000`, a read-only root filesystem, and the restricted Pod Security settings | ## Apply it ```bash kubectl apply -f https://github.com/paulbouwer/hello-kubernetes/releases/latest/download/hello-kubernetes.yaml kubectl rollout status deployment/hello-kubernetes-demo ``` *Output:* ```text serviceaccount/hello-kubernetes-demo created service/hello-kubernetes-demo created deployment.apps/hello-kubernetes-demo created deployment "hello-kubernetes-demo" successfully rolled out ``` `latest` follows the newest release. To keep one version, use its URL, for example `https://github.com/paulbouwer/hello-kubernetes/releases/download/v1.11.0/hello-kubernetes.yaml`. To change its settings, use it as a base [with Kustomize](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/kustomize.md). Without Kubernetes, see the [app settings](https://paulbouwer.github.io/hello-kubernetes/docs/reference/app-settings.md). ## Verify Get the external address. It can take a minute to appear: ```bash kubectl get service hello-kubernetes-demo ``` Open `http:///`. You see **Hello world!**. ### Where the external address works | Cluster | `LoadBalancer` Service | | - | - | | Cloud clusters, such as EKS, GKE and AKS | Works. You get a public address, and your cloud may charge for the load balancer. | | Docker Desktop, OrbStack | Works. The address is `localhost`. | | k3s, k3d, Rancher Desktop | `EXTERNAL-IP` stays ``, because their built-in Traefik already uses port 80. Move the Service to port 8080 [with Kustomize](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/kustomize.md#a-load-balancer-on-port-8080) or with Helm (`--set service.port=8080`). With k3d, also create the cluster with `-p "8080:8080@loadbalancer"`, and open `http://localhost:8080/`. | | kind, minikube, bare metal | `EXTERNAL-IP` stays ``. The app still runs. minikube also has `minikube tunnel`, and kind has `cloud-provider-kind`. | Where `EXTERNAL-IP` stays ``, forward a local port: ```bash kubectl port-forward service/hello-kubernetes-demo 8080:80 ``` Open `http://localhost:8080/`. You see **Hello world!**. Press Ctrl+C to stop. ## Upgrade Apply the newer release file. `kubectl apply` changes only what differs, and the pods are replaced one at a time: ```bash kubectl apply -f https://github.com/paulbouwer/hello-kubernetes/releases/download/v1.11.0/hello-kubernetes.yaml ``` ## Uninstall ```bash kubectl delete -f https://github.com/paulbouwer/hello-kubernetes/releases/latest/download/hello-kubernetes.yaml ``` ## Requirements - Kubernetes 1.35 or newer. CI tests 1.35 and the current release, on kind and k3d. - `linux/amd64` or `linux/arm64` nodes. --- # With Kustomize > Use the release file as a Kustomize base, and patch the namespace, message, Service and path prefix without Helm. Kustomize is built into `kubectl`. Use the [release file](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/kubectl.md) as a base, and patch what you need. You don’t need Helm. | Needs | Installs into | You get | | - | - | - | | `kubectl` and a cluster | the namespace in your `kustomization.yaml` | the app with your changes | Each example is a folder with a `kustomization.yaml`. Pin the release file to a version, so the base doesn’t change under you. ## A namespace, a message and ClusterIP *File: kustomization.yaml* ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: hello resources: - namespace.yaml - https://github.com/paulbouwer/hello-kubernetes/releases/download/v1.11.0/hello-kubernetes.yaml patches: - target: { kind: Service, name: hello-kubernetes-demo } patch: |- - op: replace path: /spec/type value: ClusterIP - target: { kind: Deployment, name: hello-kubernetes-demo } patch: |- - op: replace path: /spec/replicas value: 3 - op: add path: /spec/template/spec/containers/0/env/- value: { name: MESSAGE, value: "Deployed with Kustomize" } ``` *File: namespace.yaml* ```yaml apiVersion: v1 kind: Namespace metadata: name: hello ``` Apply it: ```bash kubectl apply -k . kubectl rollout status deployment/hello-kubernetes-demo --namespace hello ``` `op: add` with `env/-` adds to the end of the list. The release file already sets `KUBERNETES_NAMESPACE`, `KUBERNETES_POD_NAME`, `KUBERNETES_NODE_NAME` and `CONTAINER_IMAGE`. To change one of those, use `op: replace` on its position instead, or a strategic merge patch. ## A load balancer on port 8080 On k3s, k3d and Rancher Desktop, the built-in Traefik already uses port 80. Keep the `LoadBalancer` Service, and move it to port 8080: *File: kustomization.yaml* ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - https://github.com/paulbouwer/hello-kubernetes/releases/download/v1.11.0/hello-kubernetes.yaml patches: - target: { kind: Service, name: hello-kubernetes-demo } patch: |- - op: replace path: /spec/ports/0/port value: 8080 ``` Apply it with `kubectl apply -k .`. `kubectl get service hello-kubernetes-demo` then shows an `EXTERNAL-IP`, and the app is at `http://:8080/`. With k3d, create the cluster with `-p "8080:8080@loadbalancer"`, and open `http://localhost:8080/`. ## An Ingress The release file has no Ingress. Add your own, and patch the app to serve under the prefix. This passes the prefix on, as `rewritePath: false` does in the chart, so the Ingress needs no annotations. The same pattern adds an `HTTPRoute` instead. *File: ingress.yaml* ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: hello-kubernetes-demo spec: ingressClassName: traefik # or nginx, which also needs a host: below rules: - http: paths: - path: /app/hello pathType: Prefix backend: service: name: hello-kubernetes-demo port: number: 80 ``` *File: kustomization.yaml* ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: hello resources: - namespace.yaml - https://github.com/paulbouwer/hello-kubernetes/releases/download/v1.11.0/hello-kubernetes.yaml - ingress.yaml patches: - target: { kind: Service, name: hello-kubernetes-demo } patch: |- - op: replace path: /spec/type value: ClusterIP - target: { kind: Deployment, name: hello-kubernetes-demo } patch: |- - op: add path: /spec/template/spec/containers/0/env/- value: { name: HANDLER_PATH_PREFIX, value: /app/hello } - op: add path: /spec/template/spec/containers/0/env/- value: { name: RENDER_PATH_PREFIX, value: /app/hello } - op: replace path: /spec/template/spec/containers/0/livenessProbe/httpGet/path value: /app/hello/ - op: replace path: /spec/template/spec/containers/0/readinessProbe/httpGet/path value: /app/hello/ ``` Use the same `namespace.yaml` as the first example. Apply it with `kubectl apply -k .`, and open `http:///app/hello/`. ## What each chart value changes To make a change that the chart has a value for, change the YAML like this: | Chart value | What it changes in the YAML | | - | - | | `message` | Adds the `MESSAGE` env var to the container | | `pathPrefix` | Sets `RENDER_PATH_PREFIX`; the path on the Ingress or HTTPRoute | | `rewritePath: false` | Also sets `HANDLER_PATH_PREFIX` to `pathPrefix`; the probes use `/` | | `rewritePath: true` | Sets `HANDLER_PATH_PREFIX` to `""`; the HTTPRoute gets a `URLRewrite` filter; an Ingress needs your controller’s annotation | | `service.type`, `service.port` | The Service `spec.type` and port | | `ingress.*` | Creates an Ingress, with the class, host, TLS and annotations | | `httpRoute.*` | Creates an HTTPRoute, with the parent refs and host names | | `deployment.replicaCount` | The Deployment `spec.replicas` | | `deployment.container.image.*` | The container `image` and `imagePullPolicy`, and the `CONTAINER_IMAGE` env var | | `deployment.container.port` | The `containerPort`, the probes’ port, and a `PORT` env var when it isn’t `8080` | | `deployment.container.securityContext` | The container `securityContext` | | `deployment.podSecurityContext` | The pod `securityContext` | | `deployment.resources`, `nodeSelector`, `tolerations`, `affinity` | Copied into the pod spec as they are | The root filesystem is read-only, so the Deployment mounts an `emptyDir` at `/tmp`. Keep it if you change the security settings. The container also has a `preStop` sleep of 5 seconds, so that an upgrade doesn’t drop requests while the pods are replaced. ## Verify ```bash kubectl port-forward service/hello-kubernetes-demo 8080:80 --namespace hello ``` Open `http://localhost:8080/`. In the first example, you see **Deployed with Kustomize**, from one of 3 pods. ## Upgrade Change the version in the release file’s URL, and apply again with `kubectl apply -k .`. Your patches apply to the new base. ## Uninstall ```bash kubectl delete -k . ``` This also deletes the namespace, when the folder has `namespace.yaml`. ## Requirements - Kubernetes 1.35 or newer, and the `kubectl` that matches it. CI tests 1.35 and the current release, on kind and k3d. - `linux/amd64` or `linux/arm64` nodes. --- # Behind an Ingress > Serve hello-kubernetes under a path prefix behind an Ingress, with Traefik or the NGINX Ingress Controller, with and without rewriting. The chart can create an [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) for the app, under a path prefix such as `/app/hello`. Several releases can then share one address, each under its own path. | Needs | Installs into | You get | | - | - | - | | `kubectl`, [Helm 4](https://helm.sh/docs/intro/install/), and an Ingress controller | the namespace you choose | the app and a `networking.k8s.io/v1` Ingress for its path | You bring the Ingress controller, such as the [NGINX Ingress Controller](https://docs.nginx.com/nginx-ingress-controller/) or [Traefik](https://doc.traefik.io/traefik/providers/kubernetes-ingress/). Traefik is built into k3s and k3d. The chart doesn’t install one, because the right one depends on your cluster. The community Ingress-NGINX controller (`kubernetes/ingress-nginx`) was retired in March 2026. The Ingress API itself is stable and still supported. For a new cluster, consider a [Gateway](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/gateway.md). ## Path prefixes and rewriting Behind a prefix, either the proxy strips the prefix before it passes a request on, or the app serves under the prefix itself: | `rewritePath` | The proxy | The app | Works with | | - | - | - | - | | `false` (recommended) | passes `/app/hello/...` on unchanged | serves under `/app/hello` | every Ingress controller and Gateway, with no annotations | | `true` (the 1.x default) | strips `/app/hello` | serves at `/`, and links its assets under `/app/hello` | an HTTPRoute, which uses the standard `URLRewrite` filter; an Ingress needs your controller’s rewrite annotation | Set `rewritePath=false` unless you have a reason not to. The default changes to `false` in 2.0. Set `service.type=ClusterIP` too. The Ingress or the Gateway reaches the Service inside the cluster, and a `LoadBalancer` Service would give the app a second, public address that bypasses them. ## Install without rewriting Install two releases, each under its own path. Set `ingress.className` to your controller’s class: `nginx` for the NGINX Ingress Controller, or `traefik` for Traefik. These use Traefik. ```bash helm install hello-world oci://ghcr.io/paulbouwer/charts/hello-kubernetes \ --namespace hello-kubernetes --create-namespace \ --set ingress.enabled=true --set ingress.className=traefik \ --set pathPrefix=/app/hello-world --set rewritePath=false \ --set service.type=ClusterIP \ --wait helm install custom-message oci://ghcr.io/paulbouwer/charts/hello-kubernetes \ --namespace hello-kubernetes \ --set ingress.enabled=true --set ingress.className=traefik \ --set pathPrefix=/app/custom-message --set rewritePath=false \ --set service.type=ClusterIP \ --set-string message="This is my custom message!" \ --wait ``` The NGINX Ingress Controller needs a host on each Ingress, and allows one Ingress per host. With it, also add a different `--set ingress.host=…` to each release. Traefik has neither limit. To use a host name, add `--set ingress.host=hello.example.com`. For TLS, set `ingress.tls` in a values file, as in the Ingress `spec.tls` field. ## Install with rewriting With `rewritePath=true`, the controller must strip the prefix. That needs an annotation, which is different for each controller. **Traefik** ```bash kubectl apply -f - <<'EOF' apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: strip-hello namespace: hello-kubernetes spec: stripPrefix: prefixes: - /app/hello EOF helm install hello oci://ghcr.io/paulbouwer/charts/hello-kubernetes \ --namespace hello-kubernetes \ --set ingress.enabled=true --set ingress.className=traefik \ --set pathPrefix=/app/hello \ --set-string 'ingress.annotations.traefik\.ingress\.kubernetes\.io/router\.middlewares=hello-kubernetes-strip-hello@kubernetescrd' \ --set service.type=ClusterIP \ --wait ``` **NGINX** ```bash helm install hello oci://ghcr.io/paulbouwer/charts/hello-kubernetes \ --namespace hello-kubernetes \ --set ingress.enabled=true --set ingress.className=nginx \ --set ingress.host=hello.example.com \ --set pathPrefix=/app/hello \ --set-string 'ingress.annotations.nginx\.org/rewrites=serviceName=hello-kubernetes-hello rewrite=/' \ --set service.type=ClusterIP \ --wait ``` Traefik strips a prefix with a `Middleware`, which the Ingress names in an annotation as `-@kubernetescrd`. The NGINX Ingress Controller strips it with its `nginx.org/rewrites` annotation, which names the chart’s Service, `hello-kubernetes-`. With NGINX, `rewritePath=false` is still simpler: it needs no annotation. ## Verify ```bash kubectl get ingress --namespace hello-kubernetes ``` You see an Ingress for each release, such as `hello-kubernetes-hello-world`, with your controller’s address. Open: - `http:///app/hello-world/`: **Hello world!** - `http:///app/custom-message/`: **This is my custom message!** - `http:///app/hello/`: **Hello world!**, with the prefix stripped With a host name, open `http://hello.example.com/app/hello/`. ## Upgrade Change a value with `helm upgrade --reset-then-reuse-values`, as [with Helm](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/helm.md#upgrade). Changing `pathPrefix` moves the Ingress path and the app’s own path together. If you set the old `ingress.configured`, `ingress.pathPrefix` or `ingress.rewritePath` values in 1.10, see [the old path values](https://paulbouwer.github.io/hello-kubernetes/docs/reference/upgrading.md#the-old-path-values). ## Uninstall Uninstall every release in the namespace, then delete it. This also removes the Traefik `Middleware`: ```bash helm list --namespace hello-kubernetes --short | xargs helm uninstall --namespace hello-kubernetes kubectl delete namespace hello-kubernetes ``` ## Requirements - An Ingress controller that supports `networking.k8s.io/v1` and `ingressClassName`. CI tests Traefik on k3d and the NGINX Ingress Controller on kind. - Kubernetes 1.35 or newer, and Helm 4. --- # Behind a Gateway > Serve hello-kubernetes under a path prefix with a Gateway API HTTPRoute, with Traefik or NGINX Gateway Fabric. The chart can create a [Gateway API](https://gateway-api.sigs.k8s.io/) `HTTPRoute` for the app, under a path prefix such as `/app/hello`. Several releases can then share one Gateway, each under its own path. | Needs | Installs into | You get | | - | - | - | | `kubectl`, [Helm 4](https://helm.sh/docs/intro/install/), and a Gateway | the namespace you choose | the app and an `HTTPRoute` attached to your Gateway | You bring the Gateway API implementation, such as [NGINX Gateway Fabric](https://docs.nginx.com/nginx-gateway-fabric/) or [Traefik](https://doc.traefik.io/traefik/providers/kubernetes-gateway/), and a `Gateway`. The chart doesn’t install either, because the right one depends on your cluster. The Gateway’s listener must allow routes from the app’s namespace (`allowedRoutes`). ## Path prefixes and rewriting | `rewritePath` | The Gateway | The app | | - | - | - | | `false` (recommended) | passes `/app/hello/...` on unchanged | serves under `/app/hello` | | `true` (the 1.x default) | strips `/app/hello` with the standard `URLRewrite` filter | serves at `/`, and links its assets under `/app/hello` | With `rewritePath=true`, the route needs no settings for your implementation. But `URLRewrite` is an extended Gateway API feature, so check that your implementation supports it. `rewritePath=false` works everywhere. The default changes to `false` in 2.0. Set `service.type=ClusterIP` too. The Gateway reaches the Service inside the cluster, and a `LoadBalancer` Service would give the app a second, public address that bypasses it. ## Install Name your Gateway in `httpRoute.parentRefs`: ```bash helm install hello-gateway oci://ghcr.io/paulbouwer/charts/hello-kubernetes \ --namespace hello-kubernetes --create-namespace \ --set httpRoute.enabled=true \ --set 'httpRoute.parentRefs[0].name=my-gateway' \ --set 'httpRoute.parentRefs[0].namespace=gateway-system' \ --set pathPrefix=/app/hello-gateway \ --set service.type=ClusterIP \ --wait ``` Add `--set rewritePath=false` to pass the prefix on, and `--set 'httpRoute.hostnames[0]=hello.example.com'` for a host name. ### Example: Traefik’s Gateway on k3s or k3d k3s and k3d include the Gateway API resources, and Traefik can serve them. Turn on Traefik’s Gateway API provider, and let its Gateway accept routes from every namespace: ```bash kubectl apply -f - <<'EOF' apiVersion: helm.cattle.io/v1 kind: HelmChartConfig metadata: name: traefik namespace: kube-system spec: valuesContent: |- providers: kubernetesGateway: enabled: true gateway: listeners: web: namespacePolicy: from: All EOF until kubectl get gateway traefik-gateway --namespace kube-system >/dev/null 2>&1; do sleep 5; done kubectl wait gateway/traefik-gateway --namespace kube-system --for=condition=Programmed --timeout=180s ``` *Output:* ```text gateway.gateway.networking.k8s.io/traefik-gateway condition met ``` Then install the chart with `httpRoute.parentRefs[0].name=traefik-gateway` and `httpRoute.parentRefs[0].namespace=kube-system`. With k3d, create the cluster with `-p "8080:80@loadbalancer"`, and the Gateway’s address is `localhost:8080`. ## Verify ```bash kubectl get httproute --namespace hello-kubernetes ``` You see the route `hello-kubernetes-hello-gateway`. Open `http:///app/hello-gateway/`. You see **Hello world!**. If the page doesn’t load, check that the Gateway accepted the route. `kubectl describe httproute --namespace hello-kubernetes` shows the `Accepted` and `ResolvedRefs` conditions for each parent. ## Upgrade Change a value with `helm upgrade --reset-then-reuse-values`, as [with Helm](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/helm.md#upgrade). Changing `pathPrefix` moves the route’s path and the app’s own path together. ## Uninstall ```bash helm list --namespace hello-kubernetes --short | xargs helm uninstall --namespace hello-kubernetes kubectl delete namespace hello-kubernetes ``` ## Requirements - A Gateway API implementation that serves `gateway.networking.k8s.io/v1` HTTPRoutes, and supports `URLRewrite` if you keep `rewritePath=true`. CI tests Traefik on k3d and NGINX Gateway Fabric on kind. - Kubernetes 1.35 or newer, and Helm 4. --- # Config builder > Choose the options you need, and get the helm install command, a values.yaml or a Kustomize setup, with a live preview of the page. Choose the options you need. The output updates as you type, and the preview shows the page your pods will serve. The builder only writes the output: run it yourself, on your cluster. This page has an interactive tool that works only in a browser. An agent can build the same output from the Values reference and values.schema.json, listed in llms.txt. --- # Values reference > Every value in the hello-kubernetes Helm chart, with its type, default and what it does. Helm checks these against the chart’s `values.schema.json` on every install and upgrade, so a misspelled key or a wrong type stops the install. To see them from the chart itself, run `helm show values oci://ghcr.io/paulbouwer/charts/hello-kubernetes`. For the values most people change, the [config builder](https://paulbouwer.github.io/hello-kubernetes/docs/configure/builder.md) writes the command or the file for you. | Value | Type | Default | Description | | - | - | - | - | | `message` | string | `""` | A message to show instead of **Hello world!**. It must be a string: quote it in a values file, or use `--set-string`. | | `pathPrefix` | string | `""` | Serve the app under a path prefix, such as `/app/hello`, behind an Ingress, an HTTPRoute or your own proxy. Leading and trailing slashes are optional. | | `rewritePath` | bool | `true` | `false` (recommended): the proxy passes the prefix on, and the app serves under it. `true`: the proxy strips the prefix, and the app serves at `/`. See [path prefixes and rewriting](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/ingress.md#path-prefixes-and-rewriting). | | `ingress.enabled` | bool | `false` | Create a `networking.k8s.io/v1` Ingress for `pathPrefix`. | | `ingress.className` | string | `""` | The Ingress class, such as `nginx` or `traefik`. Empty uses the cluster’s default class. | | `ingress.annotations` | object | `{}` | Annotations for the Ingress, such as your controller’s rewrite annotation when `rewritePath` is `true`. | | `ingress.host` | string | `""` | The host name for the Ingress rule. Empty matches all hosts, but the NGINX Ingress Controller needs a host. | | `ingress.tls` | list | `[]` | The Ingress TLS settings, as in the Ingress `spec.tls` field. | | `httpRoute.enabled` | bool | `false` | Create a Gateway API `HTTPRoute` for `pathPrefix`. With `rewritePath` `true`, it strips the prefix with the standard `URLRewrite` filter. | | `httpRoute.parentRefs` | list | `[]` | The Gateways to attach to. Required when `httpRoute.enabled` is `true`. | | `httpRoute.hostnames` | list | `[]` | The host names for the route. Empty matches the Gateway’s host names. | | `service.type` | string | `LoadBalancer` | `ClusterIP`, `NodePort` or `LoadBalancer`. Use `ClusterIP` behind an Ingress or a Gateway. | | `service.port` | int | `80` | The port the Service exposes. | | `deployment.replicaCount` | int | `2` | The number of pods. | | `deployment.container.image.repository` | string | `paulbouwer/hello-kubernetes` | The image, such as `ghcr.io/paulbouwer/hello-kubernetes`. | | `deployment.container.image.tag` | string or number | `""` | The image tag, such as `1`, `1.11` or `1.11.0`. Empty uses the chart’s `appVersion`. In a values file, quote it: YAML reads `1.10` as the number 1.1. | | `deployment.container.image.pullPolicy` | string | `IfNotPresent` | `Always`, `IfNotPresent` or `Never`. | | `deployment.container.port` | int | `8080` | The port the app listens on. The probes and the Service follow it. | | `deployment.container.securityContext` | object | no privilege escalation, all capabilities dropped, read-only root filesystem | The container security context. With `deployment.podSecurityContext`, it meets the restricted Pod Security Standard. | | `deployment.podSecurityContext` | object | `runAsNonRoot`, user and group `1000`, `RuntimeDefault` seccomp | The pod security context. | | `deployment.nodeSelector` | object | `kubernetes.io/os: linux` | The node selector. The image runs on `amd64` and `arm64` nodes. | | `deployment.resources` | object | `{}` | The container resource requests and limits. | | `deployment.tolerations` | list | `[]` | The pod tolerations. | | `deployment.affinity` | object | `{}` | The pod affinity. | | `ingress.configured`, `ingress.pathPrefix`, `ingress.rewritePath` | | | Deprecated. They still work in 1.x, and the install notes list them. Use `pathPrefix` and `rewritePath`. They are removed in 2.0. | --- # Path prefixes > What pathPrefix and rewritePath do, and which to choose behind an Ingress, a Gateway or your own proxy. Behind an Ingress or a Gateway, several apps often share one address, each under its own path. `pathPrefix` puts hello-kubernetes under a path such as `/app/hello`. `rewritePath` says which side removes that path: the proxy, or nobody. ## What each setting does `rewritePath: false` (recommended) 1. Browser: asks for `/app/hello/` 2. Ingress or Gateway: passes on `/app/hello/` 3. App: serves at `/app/hello/`, and links its assets as `/app/hello/css/main.css` `rewritePath: true` 1. Browser: asks for `/app/hello/` 2. Ingress or Gateway: strips the prefix, and passes on `/` 3. App: serves at `/`, and links its assets as `/app/hello/css/main.css` In both cases, the page links its stylesheet and images under the prefix, so the browser asks the proxy for them by the full path. | `rewritePath` | The proxy | Works with | | - | - | - | | `false` (recommended) | passes the path on unchanged | every Ingress controller and Gateway, with no annotations | | `true` (the 1.x default) | strips the prefix | an HTTPRoute, which uses the standard `URLRewrite` filter. An Ingress needs your controller’s rewrite annotation. | Choose `rewritePath=false` unless you have a reason not to. The default changes to `false` in 2.0, so set it now and nothing changes for you then. ## Set them ```bash helm install hello oci://ghcr.io/paulbouwer/charts/hello-kubernetes \ --set pathPrefix=/app/hello --set rewritePath=false \ --set ingress.enabled=true --set ingress.className=traefik \ --set service.type=ClusterIP ``` Leading and trailing slashes are optional: `app/hello`, `/app/hello` and `/app/hello/` are the same prefix. The chart sets the Ingress or HTTPRoute path, the app’s own path and the probes’ path together. For the full steps, see [Behind an Ingress](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/ingress.md) or [Behind a Gateway](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/gateway.md). Without the chart, the app reads two [settings](https://paulbouwer.github.io/hello-kubernetes/docs/reference/app-settings.md): `HANDLER_PATH_PREFIX`, the path it serves under, and `RENDER_PATH_PREFIX`, the path its links use. ## Your own proxy With your own proxy or Ingress, set `pathPrefix` and `rewritePath`, but leave `ingress.enabled` and `httpRoute.enabled` off. Point your proxy at the Service `hello-kubernetes-`, port `80`. --- # Security defaults > How the chart and the image run hello-kubernetes with no privileges, and meet the restricted Pod Security Standard. hello-kubernetes needs no privileges. With the chart’s defaults, its pods meet the [restricted Pod Security Standard](https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted), so they run in a namespace that enforces it. ## The chart | Setting | Value | Why | | - | - | - | | `runAsNonRoot` | `true` | The kubelet refuses to start the container as root. | | `runAsUser`, `runAsGroup` | `1000` | The image’s user, as a number, so the kubelet can check it. Older images name the user, so setting it here lets them pass too. | | `seccompProfile` | `RuntimeDefault` | The container runtime’s default system call filter. | | `allowPrivilegeEscalation` | `false` | A process can’t gain more privileges than its parent. | | `capabilities` | drop `ALL` | The app needs no Linux capabilities. | | `readOnlyRootFilesystem` | `true` | Nothing can change the app’s files. The chart mounts an `emptyDir` at `/tmp`, the only place the app writes. | The pod settings are in `deployment.podSecurityContext`, and the container settings in `deployment.container.securityContext`. If you change them, keep the `/tmp` mount, and check your namespace’s Pod Security level. The container also waits 5 seconds before it stops (`preStop`), so that a rolling upgrade doesn’t drop requests while the Service and the Ingress or Gateway stop sending it traffic. Like every pod, it mounts its ServiceAccount’s token by default. The app doesn’t use it, and the chart gives the ServiceAccount no permissions. ## The image - **The user:** `1000:1000`, set as a number in the image, not a name. - **The files:** owned by root, so the app can read them but not change them. - **Nothing extra:** npm, npx, corepack and yarn are removed from the image. The app runs with `node server.js`. - **The base:** `node:24-alpine`, pinned by digest, and updated by Dependabot. - **Scanned:** CI scans the image on every PR, and fails on a fixable HIGH or CRITICAL vulnerability. The published images have an SBOM and signed provenance. To check them, see [Container image](https://paulbouwer.github.io/hello-kubernetes/docs/reference/container-image.md#check-an-image). ## Check it on your cluster Label a namespace to enforce the restricted level, and install into it: ```bash kubectl create namespace hello-restricted kubectl label namespace hello-restricted pod-security.kubernetes.io/enforce=restricted helm install hello oci://ghcr.io/paulbouwer/charts/hello-kubernetes \ --namespace hello-restricted --set service.type=ClusterIP --wait kubectl exec --namespace hello-restricted deployment/hello-kubernetes-hello -- id ``` *Output:* ```text uid=1000(node) gid=1000(node) groups=1000(node) ``` The install shows no Pod Security warnings, and the app runs as user 1000. Remove it with `helm uninstall hello --namespace hello-restricted` and `kubectl delete namespace hello-restricted`. --- # Show load balancing > Run several hello-kubernetes pods across your nodes, and watch a Service send requests to each of them. Each page shows the pod and the node that answered. With several pods, you can watch a Service spread requests across them. This guide installs four pods, sees where they run, and sends requests to them from inside the cluster. You need `kubectl`, [Helm 4](https://helm.sh/docs/intro/install/), and a cluster. With more than one node, the pods spread across them. ## Install four pods ```bash helm install spread oci://ghcr.io/paulbouwer/charts/hello-kubernetes \ --namespace hello-lb --create-namespace \ --set deployment.replicaCount=4 \ --set service.type=ClusterIP \ --wait kubectl get pods --namespace hello-lb --output wide ``` The `NODE` column shows where each pod runs. The scheduler spreads them across nodes where it can. ## Send requests through the Service Run a short-lived pod that asks the Service for the page 10 times, and prints the pod that answered each time: ```bash kubectl run lb-test --namespace hello-lb --rm -i --quiet --restart=Never --image=busybox:1.37 -- \ sh -c 'for i in $(seq 1 10); do wget -qO- http://hello-kubernetes-spread | grep -A1 "pod:" | tail -n1; done' ``` *Output:* ```text hello-kubernetes-spread-7798544b7b-fvsmz hello-kubernetes-spread-7798544b7b-s9bfh hello-kubernetes-spread-7798544b7b-fvsmz hello-kubernetes-spread-7798544b7b-lqzrb ... ``` Each request opens a new connection, and the Service sends each connection to one of the pods. ## In a browser Reach the app through a `LoadBalancer` Service, an [Ingress](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/ingress.md) or a [Gateway](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/gateway.md), and refresh the page. The pod name changes, but not on every refresh. A browser keeps its connection open, and the Service balances connections, not requests. An Ingress controller or a Gateway balances each request, so the change is easier to see through one. `kubectl port-forward` always connects to one pod, so the name never changes through it. ## Clean up ```bash helm uninstall spread --namespace hello-lb kubectl delete namespace hello-lb ``` --- # Test your Ingress or Gateway > Use hello-kubernetes to check that an Ingress controller or a Gateway routes paths, strips prefixes and serves assets. A new Ingress controller or Gateway needs a quick check: does it route a path to the right Service, strip a prefix when asked, and pass assets through? hello-kubernetes answers all three on one page. The message says which release answered, and the page’s stylesheet and logo load only if the paths are right. You need `kubectl`, [Helm 4](https://helm.sh/docs/intro/install/), and the Ingress controller or Gateway to test. ## Install two releases Install two releases under different paths, one that passes the prefix on and one that has it stripped. For an Ingress, set your class: ```bash helm install pass-on oci://ghcr.io/paulbouwer/charts/hello-kubernetes \ --namespace routing-test --create-namespace \ --set ingress.enabled=true --set ingress.className= \ --set pathPrefix=/test/pass-on --set rewritePath=false \ --set service.type=ClusterIP --set-string message="pass-on answered" --wait ``` For a Gateway, use `--set httpRoute.enabled=true` and `--set 'httpRoute.parentRefs[0].name='` in place of the two `ingress` values. Then install a second release with `--set pathPrefix=/test/stripped --set rewritePath=true` and the message `stripped answered`. With an Ingress, a stripped prefix also needs your controller’s annotation; see [Behind an Ingress](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/ingress.md#install-with-rewriting). ## Check each result | Open | You should see | If not | | - | - | - | | `http://
/test/pass-on/` | **pass-on answered** | The route doesn’t reach the Service. Check the class or the Gateway’s `allowedRoutes`. | | `http://
/test/stripped/` | **stripped answered** | The prefix wasn’t stripped, so the app returned 404. Check the rewrite annotation or `URLRewrite` support. | | Both pages | the Kubernetes logo and the dark theme | A plain, unstyled page means the assets didn’t load: the proxy routes the page, but not the paths under it. | | `http://
/test/other/` | a 404 from your controller | Something matches paths too widely. | The table on each page also shows the pod and node that answered, so you can check that the controller reaches pods on every node. ## Clean up ```bash helm uninstall pass-on stripped --namespace routing-test kubectl delete namespace routing-test ``` The e2e tests use these same checks on Traefik, the NGINX Ingress Controller and NGINX Gateway Fabric. --- # Container image > The hello-kubernetes image's registries, tags and platforms, and how to check its signature and SBOM. ## Where it is | Registry | Image | | - | - | | GitHub Container Registry (recommended) | `ghcr.io/paulbouwer/hello-kubernetes` | | Docker Hub | `paulbouwer/hello-kubernetes` | Both have the same images, with the same SBOM and signed provenance. We recommend GHCR: it has no Docker Hub pull limits, and its images sit next to the source and the release that built them. The chart uses Docker Hub by default in 1.x. To use GHCR, set `deployment.container.image.repository` to `ghcr.io/paulbouwer/hello-kubernetes`, or choose it in the [config builder](https://paulbouwer.github.io/hello-kubernetes/docs/configure/builder.md). In 2.0, GHCR becomes the default. ## Tags Each release publishes three tags: | Tag | Follows | Use it to | | - | - | - | | `1.11.0` | this release only | pin an exact version | | `1.11` | the newest `1.11.x` | get fixes, but no new features | | `1` | the newest `1.x` | stay on the current major version | There is no `latest` tag. The chart installs the exact version in its `appVersion`, so one chart version always installs the same image. ## Platforms Every tag is one image index for `linux/amd64` and `linux/arm64`. Your node pulls its own architecture, and the page shows which one it runs, such as `linux/arm64`. ## What’s in it - Node.js 24 on Alpine Linux, with only the app’s production dependencies. - The app runs as the numeric user `1000`, with no package manager in the image. See [Security defaults](https://paulbouwer.github.io/hello-kubernetes/docs/configure/security.md#the-image). - It listens on port `8080`, set with [`PORT`](https://paulbouwer.github.io/hello-kubernetes/docs/reference/app-settings.md). - OCI labels for the version, the source and the revision it was built from. ## Check an image Each published image has an SBOM and SLSA provenance for each platform. Its provenance is signed with GitHub artifact attestations, so you can check that this repo’s release workflow built it: ```bash digest=$(docker buildx imagetools inspect ghcr.io/paulbouwer/hello-kubernetes:1.11.0 --format "{{json .Manifest}}" | jq -r .digest) gh attestation verify oci://ghcr.io/paulbouwer/hello-kubernetes@$digest --repo paulbouwer/hello-kubernetes ``` Check the image by its digest, so the tag can’t move between the two commands. You need Docker with Buildx, `jq`, and the GitHub CLI. To list the packages in the SBOM of one platform: ```bash docker buildx imagetools inspect paulbouwer/hello-kubernetes:1.11.0 --format "{{json .SBOM}}" | jq -r '.["linux/amd64"].SPDX.packages[] | "\(.name) \(.versionInfo)"' ``` The chart is signed the same way. See [With Helm](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/helm.md#verify). ## Build your own To build the image yourself, or push it to your own registry, see [Build and push container images](https://github.com/paulbouwer/hello-kubernetes/blob/main/docs/build-and-push-container-images.md) in the repo. --- # App settings > The environment variables hello-kubernetes reads, for running the image without the Helm chart. The app reads its settings from environment variables. The Helm chart sets them for you from its [values](https://paulbouwer.github.io/hello-kubernetes/docs/configure/values.md). Use them directly when you run the image another way: with `docker run`, in your own manifests, or by patching the release file [with Kustomize](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/kustomize.md#what-each-chart-value-changes). | Variable | Default | What it does | | - | - | - | | `MESSAGE` | `Hello world!` | The message on the page. Chart value: `message`. | | `PORT` | `8080` | The port the app listens on. Chart value: `deployment.container.port`. | | `HANDLER_PATH_PREFIX` | none | The path the app serves under, such as `/app/hello`. Set it when the proxy passes the prefix on. Chart value: `pathPrefix`, when `rewritePath` is `false`. | | `RENDER_PATH_PREFIX` | none | The path the page’s links use, so the browser asks the proxy for the stylesheet and images under the prefix. Chart value: `pathPrefix`. | | `KUBERNETES_NAMESPACE` | `-` | The namespace shown on the page. The chart sets it from the pod. | | `KUBERNETES_POD_NAME` | the host name | The pod shown on the page. The chart sets it from the pod. | | `KUBERNETES_NODE_NAME` | `-` | The node shown on the page, with its operating system and kernel. The chart sets it from the pod. | | `CONTAINER_IMAGE` | the app’s image | The image shown on the page. Chart value: `deployment.container.image.*`. | | `LOG_LEVEL` | `info` | The log level, such as `debug`, `info`, `warn` or `error`. At `debug`, the app logs its settings when it starts. | Leading and trailing slashes in the two path prefixes are optional. The page also shows the image’s architecture, such as `linux/amd64`, which the app reads from itself. ## The pod details The chart sets the three `KUBERNETES_` variables with the [Downward API](https://kubernetes.io/docs/concepts/workloads/pods/downward-api/). In your own manifest: *File: deployment.yaml (part)* ```yaml env: - name: KUBERNETES_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: KUBERNETES_POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: KUBERNETES_NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName ``` ## Run it with Docker ```bash docker run --rm -p 8080:8080 -e MESSAGE="Hello from Docker" paulbouwer/hello-kubernetes:1.11.0 ``` Open `http://localhost:8080/`. You see **Hello from Docker**, with `-` for the namespace and node, and the container ID as the pod. Press Ctrl+C to stop. ## What the app serves | Path | What | | - | - | | `/` | The page. Under a prefix, `/`. | | `/css/`, `/images/`, `/fonts/`, `/favicon.ico` | The stylesheet, the logo, the Ubuntu font and its licence, and the icon. | There is no separate health endpoint. The chart’s probes ask for the page. --- # Upgrading from 1.10 > What changes from hello-kubernetes 1.10 to 1.11, and how to upgrade a release, the image or your own manifests. 1.11 keeps working with your 1.10 settings. The old value names still work, the Service is still a `LoadBalancer` by default, and the page looks the same. The changes are in how the chart is published, how the pods run, and which nodes they run on. ## Upgrade a Helm release In 1.10, you installed the chart from a copy of the repo. From 1.11, it is published on GHCR. Upgrade your release from there: ```bash helm upgrade oci://ghcr.io/paulbouwer/charts/hello-kubernetes \ --namespace --version 1.11.0 --reset-then-reuse-values --wait ``` Use `--reset-then-reuse-values`. It starts from the 1.11 defaults, then applies the values you set in 1.10. With `--reuse-values`, the upgrade fails, because the 1.10 values have none of the new settings: *Output:* ```text Error: UPGRADE FAILED: hello-kubernetes/templates/httproute.yaml:1:14 ... ``` The release keeps its name, its Service and its address. The pods are replaced one at a time. ## What changed in the chart | Change | What you do | | - | - | | **Published** on GHCR as `oci://ghcr.io/paulbouwer/charts/hello-kubernetes`, and on Artifact Hub. | Install and upgrade from there. | | **Path values renamed:** `ingress.pathPrefix` and `ingress.rewritePath` are now `pathPrefix` and `rewritePath`. `ingress.configured` is no longer needed. | Nothing yet: the old names still work in 1.x, and the install notes list them as deprecated. Move to the new names before 2.0. See [the old path values](#the-old-path-values). | | **The chart can create the Ingress** (`ingress.enabled`) or a Gateway API **HTTPRoute** (`httpRoute.enabled`). | Optional. Your own Ingress still works. | | **Secure by default:** the pods run as user 1000, with no privileges and a read-only root filesystem, and meet the restricted Pod Security Standard. | Nothing, unless your own sidecars or mounts need to write. The chart mounts `/tmp`. See [Security defaults](https://paulbouwer.github.io/hello-kubernetes/docs/configure/security.md). | | **Any architecture:** the pods are no longer pinned to `amd64` nodes. | Nothing. They can now run on `arm64` nodes too. | | **Values are checked** against a schema on every install and upgrade. | Fix any value the upgrade names, such as a misspelled key. In 1.10, Helm ignored it. | | **The image is the exact version,** `1.11.0`, from the chart’s `appVersion`. In 1.10, it was the `1.10` tag. | Nothing. To follow a moving tag, set `deployment.container.image.tag`. | | **The message is quoted,** so a message with `"` or `:` installs. | Nothing. | ## What changed in the image | Change | What you do | | - | - | | **Node.js 24** on Alpine, in place of Node.js 16. | Nothing. | | **`linux/amd64` and `linux/arm64`** in one tag. | Nothing. Your node pulls its own. | | **The user is the number `1000`,** in place of the name `node`. It is the same user. | Nothing. A cluster can now check `runAsNonRoot` without a `runAsUser`. | | **No npm in the image.** The app starts with `node server.js`. | If your manifest overrides the command with `npm start`, use `node server.js`. | | **Also on GHCR** as `ghcr.io/paulbouwer/hello-kubernetes`, with an SBOM and signed provenance. | Optional. See [Container image](https://paulbouwer.github.io/hello-kubernetes/docs/reference/container-image.md). | | **No requests to other sites:** the page uses a bundled font, not Google Fonts. | Nothing. The page now loads everything from the app itself. | ## The old path values Before 1.11, the chart didn’t create an Ingress: you wrote your own, and set `ingress.configured`, `ingress.pathPrefix` and `ingress.rewritePath`. These still work in 1.x, and the install notes list them as deprecated. They are removed in 2.0. | Old value | New value | | - | - | | `ingress.configured=true` | Not needed. Set `pathPrefix`, and `ingress.enabled=true` or `httpRoute.enabled=true` for the chart to create the route. | | `ingress.pathPrefix` | `pathPrefix` | | `ingress.rewritePath` | `rewritePath` | If you keep your own Ingress, set only `pathPrefix` and `rewritePath`, and point your Ingress at the Service `hello-kubernetes-`, port `80`. ## Your own manifests If you deploy 1.10 with your own YAML, change the image to `paulbouwer/hello-kubernetes:1.11.0`. The [app settings](https://paulbouwer.github.io/hello-kubernetes/docs/reference/app-settings.md) are the same as in 1.10. To get the new defaults too, start from the [release file](https://paulbouwer.github.io/hello-kubernetes/docs/deploy/kubectl.md), which is the chart rendered with its defaults. --- # For AI agents > How an AI agent can read these docs as Markdown, find the right page, and check chart values against the schema. The docs are written for people, and published for agents too. An agent can read them as plain Markdown, without the site’s layout, tabs or scripts. ## What’s published | File | What it has | | - | - | | [`/llms.txt`](https://paulbouwer.github.io/hello-kubernetes/llms.txt) | An index in the [llms.txt](https://llmstxt.org/) format: one line for each page, with its description and a link to its Markdown copy. | | A `.md` copy of each page | The page as Markdown, next to it: `/docs/deploy/helm/` has `/docs/deploy/helm.md`. Each code group lists every tab, such as **kubectl** and **Helm**, with its own code block. | | [`/llms-full.txt`](https://paulbouwer.github.io/hello-kubernetes/llms-full.txt) | Every page in one file, in sidebar order. | | [`/values.schema.json`](https://paulbouwer.github.io/hello-kubernetes/values.schema.json) | The JSON Schema that Helm checks the chart’s values against. It has a description of every value. | Every page also has a **Copy page as Markdown** button under its title, to paste a page into a chat. ## Using them from an agent - **Start from `llms.txt`.** Open only the pages the task needs, and come back to the index for others. It is much smaller than `llms-full.txt`. - **Check values against the schema** before running `helm install`. Helm rejects unknown keys and wrong types, so a value that isn’t in the schema fails the install. - **Mind the version.** The site has the docs for the latest release only. `llms.txt` names it. For an older release, read the docs in the repo at that release’s tag. For example, ask your agent: ```text Read https://paulbouwer.github.io/hello-kubernetes/llms.txt, then deploy hello-kubernetes behind my Traefik Ingress at /app/hello. ``` ## Agents that work on the repo These docs are for using hello-kubernetes. An agent that changes the app, the chart or the site should read [`AGENTS.md`](https://github.com/paulbouwer/hello-kubernetes/blob/main/AGENTS.md) in the repo first. It has the scope, the rules, and how each change is planned, built and checked. --- # Licences > The fonts, icons and code this site uses from other projects, and their licences. This site serves every asset itself. These come from other projects, each under its own licence. ## Fonts The site and the live preview use these fonts, from the [Fontsource](https://fontsource.org/) packages. Each licence is served next to the fonts. | Font | Licence | | - | - | | Archivo | [SIL Open Font Licence 1.1](https://paulbouwer.github.io/hello-kubernetes/licences/fonts/archivo.txt) | | Inter | [SIL Open Font Licence 1.1](https://paulbouwer.github.io/hello-kubernetes/licences/fonts/inter.txt) | | JetBrains Mono | [SIL Open Font Licence 1.1](https://paulbouwer.github.io/hello-kubernetes/licences/fonts/jetbrains-mono.txt) | | Ubuntu | [Ubuntu Font Licence 1.0](https://paulbouwer.github.io/hello-kubernetes/licences/fonts/ubuntu.txt) | ## Icons | Icons | Source | Terms | | - | - | - | | Helm and Kubernetes, in the sidebar | [CNCF artwork](https://github.com/cncf/artwork) | The [Linux Foundation trademark usage guidelines](https://www.linuxfoundation.org/trademark-usage/). Helm and Kubernetes are trademarks of The Linux Foundation. They are used here only to name the tool a page uses. | | Every other icon | [Starlight](https://starlight.astro.build/reference/icons/) | MIT | hello-kubernetes is not endorsed by The Linux Foundation, the CNCF, or the Helm or Kubernetes projects. ## Code The site is built with [Astro](https://astro.build/) and [Starlight](https://starlight.astro.build/), both under the MIT licence. Its sidebar is a changed copy of Starlight’s own. The site’s source is in [the repo](https://github.com/paulbouwer/hello-kubernetes/tree/main/site), under the repo’s licence.