Skip to content

Installation

This page enables Brewlet on a Kubernetes cluster: the operator, the node provisioner, and the admission webhook. After this, any pod with runtimeClassName: brewlet runs a Java application (packaged as an OCI artifact) directly on a node JDK.

There are two paths:

⚠️ Node provisioning is privileged and mutates the host (installs a shim, JDK roots, and edits /etc/containerd/config.toml). Provision only nodes your platform team controls; on mixed clusters scope with named NodeProfiles (§5.6) rather than the all-nodes default profile. See Security.


Prerequisites

Requirement Why
Kubernetes with containerd as the CRI runtime The shim is a containerd Runtime v2 shim.
cgroup v2 on nodes Brewlet requires it; the provisioner refuses cgroup v1-only nodes.
Nodes you control Provisioning is privileged and host-mutating.
kubectl + helm (for the Helm path) To install and manage node provisioning.
A reachable OCI registry Where developers push OCI artifacts (and where component + vendor JDK images live).
Node access to JDK images Vendor JDK/launcher images (Temurin, MS OpenJDK) pulled copy-from-image via the host containerd; mirror them for air-gapped clusters.

Released components

Brewlet publishes version-aligned multi-architecture component images and an OCI Helm chart. Installing chart 0.1.0 selects image tag 0.1.0 automatically. Pin to your own registry or immutable digests in production.

To build the components from source instead, use the Kubernetes component Makefile:

git clone https://github.com/brewlet/brewlet.git
cd brewlet
docker buildx build --platform linux/amd64,linux/arm64 \
  -t <registry>/operator:<tag> --push kubernetes
docker buildx build --platform linux/amd64,linux/arm64 \
  --build-arg CMD=admission -t <registry>/admission:<tag> --push kubernetes
make provisioner-image-push \
  PROVISIONER_IMAGE=<registry>/node-provisioner:<tag>

These commands build multi-arch (linux/amd64,linux/arm64) images via buildx and require a logged-in registry. The provisioner image compiles the shim inside the build for each target arch, so the installed shim always matches the node.


The charts/brewlet chart installs the operator, the provisioner RBAC, and the admission webhook. The operator then creates and reconciles the provisioner DaemonSet and the brewlet RuntimeClass from the chart's values — so there is a single runtime source of truth for the JDK/launcher inventory.

helm upgrade --install brewlet oci://ghcr.io/brewlet/charts/brewlet \
  --version 0.1.0 \
  --namespace brewlet \
  --create-namespace \
  --set provisioner.jdks="temurin-21,microsoft-25" \
  --set provisioner.launchers="jaz"

# The chart renders a default NodeProfile that provisions EVERY node (§5.6) —
# there is no per-node opt-in step. The operator provisions each node and the
# provisioner marks it ready once the shim + JDK + runtime are installed. Watch:
kubectl get nodes -L brewlet.sh/runtime -w

To limit provisioning to platform-owned pools instead of every node, disable the chart's default profile (--set defaultProfile.enabled=false) and define named NodeProfiles scoped to those pools — see Configuration (profiles / defaultProfile) and SPECIFICATION §5.6.

Point the chart at your own registry or image digests if required:

helm upgrade --install brewlet oci://ghcr.io/brewlet/charts/brewlet \
  --version 0.1.0 \
  --namespace brewlet \
  --create-namespace \
  --set images.operator=<registry>/operator:<tag> \
  --set images.provisioner=<registry>/node-provisioner:<tag> \
  --set images.admission=<registry>/admission:<tag> \
  --set provisioner.jdks="temurin-21"

Every value is documented in Configuration. Lint / preview the rendered manifests before installing:

make -C kubernetes helm-lint
make -C kubernetes helm-template

Upgrading

Helm does not upgrade CRDs placed under a chart's crds/ directory. Before upgrading an existing Brewlet installation to a release that adds custom JDK or jlink runtime sources, apply that release's NodeProfile CRD explicitly:

kubectl apply -f https://raw.githubusercontent.com/brewlet/brewlet/v0.1.0/kubernetes/deploy/nodeprofile-crd.yaml
helm upgrade brewlet oci://ghcr.io/brewlet/charts/brewlet \
  --version 0.1.0 \
  -f values.yaml

What the chart deploys vs. what the operator creates

Deployed by the chart Created/reconciled by the operator at runtime
brewlet-operator Deployment + RBAC brewlet-node-provisioner DaemonSet
Node-provisioner ServiceAccount + ClusterRole The brewlet RuntimeClass
brewlet-admission webhook + serving cert (tracks node readiness, emits events)

Manual (without Helm)

If you'd rather not use Helm, apply the raw manifests and run the operator directly.

# 1. Namespace + provisioner ServiceAccount/RBAC (and, if you want to hand-wire it,
#    the provisioner DaemonSet):
kubectl apply -f kubernetes/deploy/node-provisioner.yaml

# 2. The operator ServiceAccount + RBAC + Deployment:
kubectl apply -f kubernetes/config/operator.yaml

# 3. Opt nodes in. The standalone provisioner DaemonSet schedules onto nodes
#    carrying this LABEL — it drives nodeAffinity, so it must be a label, not an
#    annotation:
kubectl label node --all brewlet.sh/provision=true

You can also run the operator locally against your current kubeconfig (useful for debugging), passing the same inventory the chart would set:

make -C kubernetes operator-build
./kubernetes/bin/operator \
  --namespace=brewlet \
  --provisioner-image=<registry>/node-provisioner:<tag> \
  --jdks=temurin-21,microsoft-25 \
  --launchers=jaz

The RuntimeClass and provisioner DaemonSet the operator generates mirror deploy/runtimeclass.yaml and deploy/node-provisioner.yaml. All operator and admission flags are in Configuration.

The operator itself does not need to be privileged — it only talks to the API server. The privileged, host-mutating work is done by the DaemonSet it manages.


Verify the installation

# Install the CLI version that matches the chart, then run the readiness check:
export BREWLET_VERSION="0.1.0"
curl -fsSL https://brewlet.sh/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
brewlet doctor --namespace default

# 1. Components are running:
kubectl get pods -n brewlet

# 2. Nodes are being provisioned → ready:
kubectl get nodes -L brewlet.sh/runtime
#   NAME     STATUS   RUNTIME
#   node-1   Ready    ready        ← provisioned

# 3. Inspect what a node advertises:
kubectl get node node-1 -o jsonpath='{.metadata.annotations.brewlet\.sh/jdks}{"\n"}'
#   temurin-21,microsoft-25
kubectl get node node-1 -o jsonpath='{.metadata.annotations.brewlet\.sh/launchers}{"\n"}'
#   java,jaz

# 4. The RuntimeClass exists:
kubectl get runtimeclass brewlet

# 5. The operator's view of each node:
kubectl get node node-1 -o jsonpath='{.metadata.annotations.brewlet\.sh/provision-state}{"\n"}'
#   Ready

Watch provisioning events if a node isn't going ready:

kubectl get events --field-selector reason=NodeReady
kubectl get events --field-selector reason=ProvisionFailed

See Troubleshooting if a node stays Provisioning/Failed.


Smoke test with a workload

kubectl apply -f kubernetes/deploy/raw-deployment.yaml
kubectl get pods -l app=hello -w
kubectl logs -l app=hello

For the full deploy story (raw Deployment, JavaApplication CRD, requesting a specific JDK/launcher), see Deploying workloads.


Uninstall

helm uninstall brewlet

Uninstalling deletes the control-plane components and the chart's NodeProfile objects. Each profile carries a node.brewlet.sh/cleanup finalizer, so the operator holds the object while a short-lived brewlet-cleanup-<profile> DaemonSet (BREWLET_MODE=cleanup) restores the config.toml backup, removes the shim + JDK roots, and drops the runtime + capability labels on every assigned node — reversing host state automatically before the object is garbage-collected (§5.6). Watch it with kubectl get daemonset -n brewlet -w. If a cluster was provisioned the older way (a bare brewlet.sh/provision=true node label with no profile), drain and clean those nodes (or replace them) to fully reverse provisioning.

Next steps