Get up and running with Kubeflow on Kubernetes

Deploy Kubeflow on a Kubernetes cluster, secure the Istio gateway with HTTPS, and launch a Jupyter notebook: all in a concise, step‑by‑step workflow that gets you from a fresh cluster to a fully functional machine‑learning environment ready for pipelines and experiments.

9 minutes reading time

Written by

Jubril Oyetunji
Jubril Oyetunji

Technical Writer at Civo

One of the most important projects when it comes to MLOps on Kubernetes is Kubeflow. In 2026, AI-powered experiences are fast becoming the default for many teams; as such, Kubeflow is a natural choice for teams either already on Kubernetes or looking to train or serve models using the planet's most popular container orchestrator.

This guide takes you from an empty Civo account to a working Kubeflow install with the Central Dashboard reachable over real HTTPS, signed in, and running a Jupyter notebook. It is written to be a clean, self-contained starting point, so a later tutorial can pick up this same cluster and serve a model on it.

What is Kubeflow?

If you're unfamiliar, Kubeflow is an open-source platform for machine learning and MLOps on Kubernetes introduced by Google. The different stages in a typical machine learning lifecycle are represented with different software components in Kubeflow, including model development, model training, model serving, and automated machine learning.

Those components are separate projects that install together as a platform. You will use each of them from the same dashboard:

  • Kubeflow Notebooks give you managed JupyterLab, VS Code, or RStudio servers running inside the cluster for model development. Each notebook gets its own CPU, memory, and storage.
  • The Training Operator runs distributed training jobs for PyTorch, TensorFlow, and others as Kubernetes-native custom resources, so a training run is just another object the cluster schedules.
  • KServe handles model serving. It turns a trained model into an autoscaling inference endpoint, including scale-to-zero when the endpoint is idle.
  • Katib is the AutoML component. It runs hyperparameter tuning and neural architecture search by launching many trials and comparing their results.
  • Kubeflow Pipelines orchestrates the whole thing. You describe an ML workflow as a graph of steps, and Pipelines runs, tracks, and versions each execution.

Underneath, Kubeflow uses Istio for in-cluster routing and Dex for authentication. You do not manage those directly, but it helps to know they are there, because the dashboard sits behind the Istio ingress gateway and every request passes through the Dex login.

A note on install methods

Older versions of this guide, and Civo's own marketplace, install Kubeflow through a one-click app. At the time of writing that app is pinned to Kubeflow 1.6.0-rc.1, which is a release candidate from 2022. Civo also briefly offered a managed "Kubeflow as a Service" product, which is no longer available.

The current, maintained way to install Kubeflow is the upstream kubeflow/manifests repository, which is what this guide uses. The latest release is 1.10.0, and this guide pins to it so your install matches what you see here. Pinning matters: Kubeflow's manifests change between releases, and an unpinned git clone will drift.

Prerequisites

You will need:

  • A Civo account.
  • The Civo CLI installed and authenticated.
  • kubectl installed on your machine.
  • kustomize version 5.4.3 or newer, installed as a standalone binary. Kubeflow's manifests use features that the kubectl kustomize built-in does not always handle the same way, so install the real thing:
brew install kustomize
kustomize version

Kubeflow is not a light install. The full platform includes Istio, Dex, Notebooks, Pipelines, Katib, KServe, the Training Operator, the Central Dashboard, and Profiles. Upstream recommends at least 8 CPU cores and 16 GB of RAM. The cluster below gives you that with headroom to spare for a notebook and, later, a served model.

Step 1: Create a Civo Kubernetes cluster

First, check which Kubernetes versions Civo currently offers:

civo kubernetes versions

Kubeflow 1.10 targets Kubernetes 1.32. Civo retires older patch versions quickly, so at the time of writing the closest available stable release is 1.35.0-k3s1, which this guide uses. Kubeflow installs cleanly on it with the two small manifest fixes described in Step 3.

Create a single-node cluster sized for the full platform. A single g4p.kube.medium node gives you 8 vCPU and 32 GB of RAM, which is enough for Kubeflow plus a notebook. Name everything kubeflow-demo so it is easy to find and clean up later:

civo kubernetes create kubeflow-demo \
--region nyc1 \
--size g4p.kube.medium \
--nodes 1 \
--version 1.35.0-k3s1 \
--cni-plugin flannel \
--wait \
--save

The --wait flag holds the terminal until the cluster is ACTIVE, and --save merges the cluster's credentials into your kubeconfig. When it returns, confirm the node is Ready:

kubectl get nodes -o wide
Create a Civo Kubernetes cluster

You can see the cluster's details, including its public IP, with:

civo kubernetes show kubeflow-demo
Create a Civo Kubernetes cluster 2

Note the External IP in that output. You will need it in Step 4 for the HTTPS hostname. In this guide it is 212.2.245.220.

Civo's k3s ships with Traefik as its ingress controller, listed under Installed Applications as traefik2-nodeport. Leave it in place. You will use it to terminate TLS in Step 4.

Step 2: Fetch the Kubeflow manifests

Clone the manifests repository at the 1.10.0 tag so your install is reproducible:

git clone --depth 1 --branch v1.10.0 https://github.com/kubeflow/manifests.git
cd manifests

The full platform is assembled by a single kustomization at example/kustomization.yaml. It pulls in every component: cert-manager, Istio, Dex, oauth2-proxy, Pipelines, Katib, the Central Dashboard, Notebooks, Profiles, the Training Operator, and KServe.

Step 3: Apply two small fixes, then install

Kubeflow 1.10.0 was released against an older kustomize and against image locations that have since changed. Two edits keep the install reproducible on a current cluster. Make them before you install.

Fix 1: the user Profile name

The manifests use kustomize's older variable syntax to fill in a default user Profile. kustomize 5.x removed that feature, so the Profile ships with a literal, invalid name and the install never finishes. Replace the template with the values it was meant to produce:

cat > common/user-namespace/base/profile-instance.yaml <<'EOF'
apiVersion: kubeflow.org/v1beta1
kind: Profile
metadata:
name: kubeflow-user-example-com
spec:
owner:
kind: User
name: user@example.com
EOF

Fix 2: the MinIO image

Kubeflow Pipelines ships an object store using an old MinIO image hosted on Google Container Registry. That image path was removed and now returns a 404, which leaves the pipeline API server crash-looping. Point MinIO at the official image, which still accepts the same configuration:

sed -i.bak \
's#gcr.io/ml-pipeline/minio:RELEASE.2019-08-14T20-37-41Z-license-compliance#minio/minio:RELEASE.2022-11-17T23-20-09Z#' \
apps/pipeline/upstream/third-party/minio/base/minio-deployment.yaml

Raise the node's inotify limits

Several Kubeflow controllers watch a large number of files. On a fresh node the default inotify limits are too low, and at least one controller (Katib) crash-loops with a "too many open files" error. Raise the limits on the node with a one-off privileged pod:

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: node-sysctl-fix
namespace: kube-system
spec:
restartPolicy: Never
hostPID: true
containers:
- name: sysctl
image: busybox:1.36
securityContext:
privileged: true
command:
- /bin/sh
- -c
- |
sysctl -w fs.inotify.max_user_instances=1280
sysctl -w fs.inotify.max_user_watches=655360
EOF

Install

Now apply the platform. Kubeflow's own installer runs the apply in a loop, because custom resource definitions and the resources that depend on them are applied together, and the dependent resources fail until their CRDs are registered. Re-running resolves the ordering:

while ! kustomize build example | kubectl apply --server-side --force-conflicts -f -; do
echo "Retrying to apply resources"
sleep 20
done

The first pass applies the CRDs and namespaces, and later passes fill in the rest. Give it a few minutes.

When the loop settles, watch the pods come up. The install is spread across several namespaces, so watch them all:

kubectl get pods -A

It is normal for pods to sit in Init or ContainerCreating for a while, and for a few to restart once or twice as their dependencies start. Wait until everything in the kubeflow namespace is Running and Ready:

kubectl get pods -n kubeflow
Install Kubeflow

Applying the fixes above also creates the default user Profile and its namespace, kubeflow-user-example-com. That is the namespace you will work in from the dashboard.

Step 4: Put the dashboard on HTTPS

The dashboard has to be served over HTTPS. Kubeflow's web apps set secure cookies for the login session, and browsers will not send those cookies over plain HTTP, so an HTTP-only dashboard cannot keep you logged in.

The dashboard itself sits behind Istio's ingress gateway, which inside the cluster only listens on plain HTTP. Rather than hand-editing Istio to terminate TLS, using Traefik is an easy way out.

If you don’t own a domain yet. sslip.io is a free wildcard DNS service that resolves any <ip>.sslip.io hostname to the IP embedded in it. Using your cluster's public IP, 212.2.245.220, the hostname becomes 212-2-245-220.sslip.io.

 Substitute your own node's IP throughout this step.

Create a Let's Encrypt issuer

cert-manager needs an issuer that knows how to talk to Let's Encrypt. This one solves the HTTP-01 challenge through Traefik:

kubectl apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: you@example.com
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
class: traefik
EOF

Change the email to your own. It is only used by Let's Encrypt for expiry notices.

Request the certificate

Ask cert-manager for a certificate for your hostname. It will run the challenge and write the signed certificate into a secret named kubeflow-dashboard-tls:

kubectl apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: kubeflow-dashboard
namespace: istio-system
spec:
secretName: kubeflow-dashboard-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
commonName: 212-2-245-220.sslip.io
dnsNames:
- 212-2-245-220.sslip.io
EOF

Watch it until it reports Ready:

kubectl -n istio-system get certificate kubeflow-dashboard -w
Request the certificate

Tell Traefik to serve the certificate and route to Istio

Civo runs Traefik v2.9, which needs two Traefik resources here. The version that ships with Civo does not have these custom resource definitions installed, so add them first:

kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v2.9/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml

The first resource is a TLSStore named default. Without it, Traefik keeps presenting its own self-signed certificate instead of the Let's Encrypt one. Pointing the default store at your certificate secret fixes that:d

kubectl apply -f - <<'EOF'
apiVersion: traefik.containo.us/v1alpha1
kind: TLSStore
metadata:
name: default
namespace: istio-system
spec:
defaultCertificate:
secretName: kubeflow-dashboard-tls
EOF

The second resource is an IngressRoute that matches your hostname and forwards to the Istio ingress gateway on port 80. Add one route for HTTPS and one for plain HTTP:

kubectl apply -f - <<'EOF'
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: kubeflow-dashboard
namespace: istio-system
spec:
entryPoints:
- websecure
routes:
- match: Host(`212-2-245-220.sslip.io`)
kind: Rule
services:
- name: istio-ingressgateway
port: 80
tls:
secretName: kubeflow-dashboard-tls
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: kubeflow-dashboard-http
namespace: istio-system
spec:
entryPoints:
- web
routes:
- match: Host(`212-2-245-220.sslip.io`)
kind: Rule
services:
- name: istio-ingressgateway
port: 80
EOF

You can confirm the routing objects are in place:

kubectl get ingressroute,tlsstore -n istio-system
Tell Traefik to serve the certificate and route to Istio

Confirm HTTPS works

Check the served certificate and the endpoint. The certificate should be issued by Let's Encrypt, and a plain curl should verify it without the -k flag:

echo | openssl s_client -connect 212-2-245-220.sslip.io:443 \
-servername 212-2-245-220.sslip.io 2>/dev/null \
| openssl x509 -noout -issuer -subject -dates
curl -sSI https://212-2-245-220.sslip.io/
Confirm HTTPS works

The 403 from istio-envoy is expected and is a good sign. It means the request reached Kubeflow's authentication layer, which refuses anything that is not signed in. A browser will be redirected to the login page instead.

Step 5: Log in and create a notebook

Open https://<yourip>.sslip.io/ in your browser, using your own hostname. You will land on the sign-in page.

Log in and create a notebook

Continue through to the Dex login form. The default credentials that ship with Kubeflow are:

Log in and create a notebook account details

These defaults are public knowledge, so change them before you put anything real on this cluster. The password lives as a bcrypt hash in the Dex config map (kubectl -n auth edit configmap dex), and the user list is documented in the Kubeflow manifests README. For a throwaway cluster the defaults are fine to explore with.

After logging in, the Central Dashboard loads, scoped to the kubeflow-user-example-com namespace.

Log in and create a notebook central dashboard

To prove the workflow end to end, create a notebook. In the sidebar, choose Notebooks, then New Notebook. Give it a name, leave JupyterLab selected as the image, and set a small CPU and memory request. The defaults of 0.5 CPU and 1 GB are enough to start.

Log in and create a notebook central dashboard: New notebook

Scroll down and click Launch. The notebook takes a minute or two to pull its image and start. When it is ready, it shows a green status in the list with a Connect button.

Log in and create a notebook central dashboard: New notebook connection

A note on storage

Civo Kubernetes provisions persistent volumes automatically through its civo-volume storage class, which is the default on your cluster. When Kubeflow Pipelines, Katib, and your notebook ask for storage, Civo creates real block volumes and attaches them to the node with no extra setup. You can see them with kubectl get pvc -A.

Where to go next

You now have a working Kubeflow install with the Central Dashboard on HTTPS, a signed-in session, and a running notebook. The cluster is a clean base for the rest of an ML workflow: training jobs through the Training Operator, hyperparameter sweeps with Katib, pipelines to tie steps together, and model serving with KServe.

When you are done, tear the whole thing down in one command, since every resource carries the kubeflow-demo name:

civo kubernetes delete kubeflow-demo

That removes the cluster and the volumes Kubeflow created along with it.

Jubril Oyetunji
Jubril Oyetunji

Technical Writer at Civo

Jubril Oyetunji is a DevOps engineer and technical writer with a strong focus on cloud-native technologies and open-source tools. His work centers on creating practical tutorials that help developers better understand platforms such as Kubernetes, NGINX, Rust, and Go.

As a contract technical writer, Jubril authored an extensive library of technical guides covering cloud-native infrastructure and modern development workflows. Many of his tutorials achieved strong search rankings, helping developers around the world learn and adopt emerging technologies.

View author profile