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.
Written by
Technical Writer at Civo
Written by
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 kustomizekustomize 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
You can see the cluster's details, including its public IP, with:
civo kubernetes show kubeflow-demo
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.gitcd 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/v1beta1kind: Profilemetadata:name: kubeflow-user-example-comspec:owner:kind: Username: user@example.comEOF
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: v1kind: Podmetadata:name: node-sysctl-fixnamespace: kube-systemspec:restartPolicy: NeverhostPID: truecontainers:- name: sysctlimage: busybox:1.36securityContext:privileged: truecommand:- /bin/sh- -c- |sysctl -w fs.inotify.max_user_instances=1280sysctl -w fs.inotify.max_user_watches=655360EOF
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 -; doecho "Retrying to apply resources"sleep 20done
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
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/v1kind: ClusterIssuermetadata:name: letsencrypt-prodspec:acme:server: https://acme-v02.api.letsencrypt.org/directoryemail: you@example.comprivateKeySecretRef:name: letsencrypt-prod-account-keysolvers:- http01:ingress:class: traefikEOF
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/v1kind: Certificatemetadata:name: kubeflow-dashboardnamespace: istio-systemspec:secretName: kubeflow-dashboard-tlsissuerRef:name: letsencrypt-prodkind: ClusterIssuercommonName: 212-2-245-220.sslip.iodnsNames:- 212-2-245-220.sslip.ioEOF
Watch it until it reports Ready:
kubectl -n istio-system get certificate kubeflow-dashboard -w
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/v1alpha1kind: TLSStoremetadata:name: defaultnamespace: istio-systemspec:defaultCertificate:secretName: kubeflow-dashboard-tlsEOF
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/v1alpha1kind: IngressRoutemetadata:name: kubeflow-dashboardnamespace: istio-systemspec:entryPoints:- websecureroutes:- match: Host(`212-2-245-220.sslip.io`)kind: Ruleservices:- name: istio-ingressgatewayport: 80tls:secretName: kubeflow-dashboard-tls---apiVersion: traefik.containo.us/v1alpha1kind: IngressRoutemetadata:name: kubeflow-dashboard-httpnamespace: istio-systemspec:entryPoints:- webroutes:- match: Host(`212-2-245-220.sslip.io`)kind: Ruleservices:- name: istio-ingressgatewayport: 80EOF
You can confirm the routing objects are in place:
kubectl get ingressroute,tlsstore -n istio-system
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 -datescurl -sSI https://212-2-245-220.sslip.io/
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.
Continue through to the Dex login form. The default credentials that ship with Kubeflow are:
- Username:
user@example.com - Password:
12341234
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.
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.
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.
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.

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.
Share this article
Further Reading
28 January 2025
Accelerating machine learning: Jupyter Notebook on Civo GPUs
26 November 2023