Helm Chart Deployment
EasyLab is available as a Helm chart published on Docker Hub as an OCI artifact.
Prerequisites
- Kubernetes cluster (v1.24+)
- Helm 3.8+ (OCI support required)
- To reach the EasyLab UI, an Ingress controller matching
ingress.classNamemust already be running in the cluster if you setingress.enabled=true(the recommended way to expose it). The chart defaultsingress.classNametotraefik, which is only preinstalled on some distributions (for example k3s) — most managed Kubernetes offerings, including OVHcloud Managed Kubernetes, ship with no ingress controller out of the box. Either install Traefik yourself beforehand, or settraefik.enabled=trueto have this chart install it for you — see Optional infrastructure components. If you use a different ingress controller (for example NGINX), install it separately and setingress.classNameto match. - If you terminate TLS with cert-manager annotations (rather than a pre-existing secret), cert-manager and a configured
ClusterIssuermust already be installed, or setcert-manager.enabled=trueto have the chart install cert-manager for you. This chart can also create theClusterIssueritself (HTTP-01, Azure DNS-01, or OVH DNS-01) — see Certificates for the EasyLab ingress.
App image platforms (multi-arch)
Tags of the application image published from this project’s CI (for example docker.io/yodamad/easylab) are multi-platform: each tag is a manifest list for linux/amd64 and linux/arm64. Kubernetes (and docker pull) selects the variant that matches the node or host. You do not need separate Helm values per architecture—image.repository and image.tag stay the same.
Image CPU architecture (exec format error)
If the pod exits immediately with exec /app/main: exec format error, the image’s architecture does not match your nodes (for example an arm64 image on amd64 workers). That often happens when you build a custom image on Apple Silicon with plain docker build and no platform flag.
Fix for custom builds: build and push with an explicit platform that matches your cluster (most cloud clusters are linux/amd64):
docker buildx build --platform linux/amd64 -t your-registry/easylab:your-tag --push .
For arm64 nodes (for example AWS Graviton), use --platform linux/arm64 instead. To publish both architectures in one tag (like CI does), use:
docker buildx build --platform linux/amd64,linux/arm64 -t your-registry/easylab:your-tag --push .
(--load only supports a single platform; multi-arch builds must be pushed to a registry.)
Install
helm install easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--set secrets.adminPassword="your-secure-password"
Versions follow SemVer without the v prefix.
Available versions
Check available versions on Docker Hub or with:
helm show chart oci://registry-1.docker.io/yodamad/easylab-helm --version 1.0.0
Configuration
All configuration is done through values.yaml overrides. You can either pass --set flags or provide a custom values file:
helm install easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
-f my-values.yaml
Key values
| Parameter | Description | Default |
|---|---|---|
namespace.create |
Create a dedicated namespace | true |
namespace.name |
Namespace name | easylab |
image.repository |
Docker image repository | docker.io/yodamad/easylab |
image.tag |
Docker image tag (defaults to v + chart appVersion when appVersion has no leading v, to match Git-tag Docker images) |
"" |
image.pullPolicy |
Image pull policy | Always |
replicaCount |
Number of replicas | 1 |
runtime.openFilesLimit |
Process ulimit -n before starting the app (helps Pulumi/fsnotify in pods) |
65536 |
config.port |
Application port | "8080" |
config.workDir |
Job workspace directory | "/app/jobs" |
config.dataDir |
Data persistence directory | "/app/data" |
config.mode |
Areas served: all or admin — see In-lab student portals |
"" (all) |
config.publicUrl |
This release's address as students reach it, used by in-lab student portals for Microsoft / GitHub / GitLab sign-in | "" (the ingress host when ingress.enabled) |
config.portalImage |
Image the in-lab student portals run | "" (this release's own image) |
secrets.create |
Create a Kubernetes secret | true |
secrets.adminPassword |
Admin login password | "" |
secrets.studentPassword |
Student login password | "" |
secrets.ovh.applicationKey |
OVH application key | "" |
secrets.ovh.applicationSecret |
OVH application secret | "" |
secrets.ovh.consumerKey |
OVH consumer key | "" |
secrets.ovh.serviceName |
OVH service name | "" |
secrets.ovh.endpoint |
OVH API endpoint | "ovh-eu" |
persistence.jobs.size |
PVC size for jobs storage | 1Gi |
persistence.jobs.storageClass |
Storage class for jobs PVC | "" |
persistence.data.size |
PVC size for data storage | 200Mi |
persistence.data.storageClass |
Storage class for data PVC | "" |
service.type |
Kubernetes service type | ClusterIP |
service.port |
Service port | 80 |
service.annotations |
Service annotations | {} |
ingress.enabled |
Enable ingress | false |
ingress.className |
Ingress class name | traefik |
ingress.annotations |
Ingress annotations | {} |
ingress.host |
Ingress hostname | easylab.example.com |
ingress.tls.enabled |
Enable TLS | false |
ingress.tls.secretName |
TLS secret name | easylab-tls |
ingress.httpsRedirect.enabled |
Redirect HTTP to HTTPS with a Traefik redirectScheme Middleware — created only when ingress.className is traefik and ingress.tls.enabled=true, see Redirecting HTTP to HTTPS |
true |
ingress.httpsRedirect.permanent |
true issues a 301 (remembered by browsers), false a 302 |
true |
ingress.httpsRedirect.apiVersion |
Middleware CRD API group — Traefik v3 uses traefik.io/v1alpha1, Traefik v2 traefik.containo.us/v1alpha1 |
traefik.io/v1alpha1 |
traefik.enabled |
Install Traefik as part of this chart (IngressClass name pinned to traefik, matching ingress.className's default) |
false |
cert-manager.enabled |
Install cert-manager as part of this chart | false |
cert-manager.crds.enabled |
Install cert-manager CRDs (required on first install) | true |
certManager.namespace |
Namespace cert-manager's controller runs in — must match wherever it's actually installed | cert-manager |
certManager.clusterIssuer.create |
Create a Let's Encrypt ClusterIssuer for the EasyLab ingress (HTTP-01, or Azure DNS-01 if dns.azure.enabled=true) |
false |
certManager.clusterIssuer.name |
ClusterIssuer name | letsencrypt |
certManager.clusterIssuer.email |
ACME account email | "" |
certManager.clusterIssuer.server |
ACME server URL | https://acme-v02.api.letsencrypt.org/directory |
dns.azure.enabled |
Use Azure DNS-01 (cert-manager's built-in azureDNS solver) instead of HTTP-01 |
false |
dns.azure.zone |
Azure DNS hosted zone name | "" |
dns.azure.tenantId / .subscriptionId / .resourceGroup / .clientId / .clientSecret |
Azure service principal credentials | "" |
dns.ovh.enabled |
Install cert-manager-webhook-ovh and let it create its own OVH DNS-01 ClusterIssuer — see Certificates for the EasyLab ingress |
false |
dns.ovh.zone |
OVH DNS zone ingress.host is a subdomain of, e.g. example.com — only needed when dns.externalDns.enabled=true, see Automatic DNS record for the EasyLab ingress |
"" |
cert-manager-webhook-ovh.* |
Raw values for the vendored OVH webhook chart (issuer name, ACME email, OVH credentials, etc.) — deeply nested, see upstream chart values | see values.yaml |
dns.externalDns.enabled |
Run a persistent ExternalDNS controller that keeps ingress.host's A record pointed at the ingress controller's LoadBalancer IP — see Automatic DNS record for the EasyLab ingress |
false |
dns.externalDns.image.repository / .tag |
ExternalDNS container image | registry.k8s.io/external-dns/external-dns / v0.22.0 |
dns.externalDns.policy |
sync also deletes the record if it's no longer needed; upsert-only never deletes |
sync |
dns.externalDns.txtOwnerId |
Scopes ExternalDNS's TXT ownership registry so this install never touches records owned by another ExternalDNS instance sharing the same zone; empty defaults to the release fullname | "" |
dns.externalDns.dryRun |
Log planned DNS changes without calling the provider API — recommended true for a first rollout |
false |
dns.externalDns.resources |
Resource requests/limits for the ExternalDNS pod | see values.yaml |
resources.requests.memory |
Memory request | 1024Mi |
resources.requests.cpu |
CPU request | 500m |
resources.limits.memory |
Memory limit | 4096Mi |
resources.limits.cpu |
CPU limit | 3000m |
nodeSelector |
Pin the EasyLab server pod to nodes matching these labels (e.g. a dedicated node pool) | {} |
tolerations |
Tolerations for the EasyLab server pod, needed if its target node pool is tainted | [] |
Set an explicit data encryption key for persistent deployments
Because config.dataDir is set by default, persisted job files hold cluster kubeconfigs and DNS credentials. Provide a LAB_DATA_ENCRYPTION_KEY environment variable (a base64-encoded 32-byte key, e.g. from openssl rand -base64 32) through your secret / pod environment and keep it stable across upgrades, or previously-encrypted kubeconfigs become unreadable and the affected labs must be recreated. If LAB_DATA_ENCRYPTION_KEY is not set, the server auto-generates one and saves it to <dataDir>/.encryption_key so it still starts — but on Kubernetes this file only survives pod restarts if dataDir is backed by a persistent volume; without one, a new pod means a new key and unreadable existing job data. Explicitly setting the key via a Secret is strongly recommended for any real deployment. Setting a strong PULUMI_CONFIG_PASSPHRASE is likewise recommended (see Docker — Environment Variables for details on both). Provider API credentials are never written to lab state.
Keeping credentials across pod restarts
With LAB_DATA_ENCRYPTION_KEY set from a Secret, cloud provider credentials and DNS profiles saved in the admin UI are stored encrypted under it and are available again automatically after a pod restart (see Credential storage). The Secret can be fed by whatever already manages secrets in your cluster — a plain Kubernetes Secret, External Secrets Operator, or an OpenBao / Vault agent injector — with no change to EasyLab: it only reads the environment variable. Without the variable, an admin can set a passphrase in the UI instead and enter it once after each restart.
X-Forwarded-Proto trust
The server trusts an incoming X-Forwarded-Proto: https header to mark the session cookie Secure (env var TRUST_FORWARDED_PROTO, default true — see Docker — Environment Variables). This is safe with the standard ingress-terminated-TLS setup above, since the ingress controller sets this header itself and any client-supplied value is overwritten. Only set TRUST_FORWARDED_PROTO=false if you expose the pod in a way where a client-supplied header could reach it unmodified.
In-lab student portals
A lab can have its own student portal, deployed by EasyLab inside the lab's cluster. Three values control how this release takes part:
config:
# "admin": this release serves the admin space only; students use each lab's portal.
# Leave empty to also keep the central student space.
mode: admin
# Where portals send students for Microsoft / GitHub / GitLab sign-in.
# Defaults to the ingress host when ingress.enabled is true.
publicUrl: https://easylab.example.com
# Defaults to this release's own image. The lab's nodes must be able to pull it.
portalImage: ""
secrets.studentPassword is still what students type on a portal's password form: each
portal checks it locally, against a hash EasyLab copies to it.
Optional infrastructure components
By default (traefik.enabled=false, cert-manager.enabled=false), this chart does not install an ingress controller or cert-manager for you — it assumes a controller matching ingress.className (default traefik) and, if you rely on cert-manager annotations for TLS, cert-manager is already installed in your cluster. If they are not, you can let the chart install Traefik (matches the default ingress.className out of the box) and cert-manager instead:
# Install Traefik alongside EasyLab (IngressClass name pinned to "traefik")
traefik:
enabled: true
# Install cert-manager alongside EasyLab (CRDs included)
cert-manager:
enabled: true
crds:
enabled: true
Or via --set flags:
helm install easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--set traefik.enabled=true \
--set cert-manager.enabled=true \
--set secrets.adminPassword="SuperAdmin"
If your cluster already has an ingress controller and/or cert-manager installed — including a different controller such as NGINX — leave the relevant flag at false (default) and configure ingress.className to match your existing controller.
All traefik and cert-manager values can be passed under their respective keys — see the Traefik chart values and cert-manager chart values for the full list. This also covers scheduling: traefik.nodeSelector/.tolerations and cert-manager.nodeSelector/.tolerations pin those components to specific nodes the same way nodeSelector/tolerations pin the EasyLab pod itself (see Pinning to a dedicated node pool), for example --set traefik.nodeSelector.pool=control-plane.
cert-manager CRDs are not updated by helm upgrade
cert-manager.crds.enabled=true installs the cert-manager CRDs on first install, but Helm does not update CRDs on helm upgrade — this is a general Helm/cert-manager limitation, not specific to this chart. Bumping the vendored cert-manager version may require manually applying the new CRDs (see cert-manager's CRD upgrade docs) before helm upgrade picks up the new version.
Certificates for the EasyLab ingress
Beyond installing cert-manager itself, this chart can also create the ClusterIssuer that actually requests a certificate for the EasyLab ingress — closing the gap left by cert-manager.enabled=true alone (that only installs cert-manager; nothing issues a certificate until an issuer exists).
Namespace alignment (read this first)
certManager.namespace (and, for OVH, cert-manager-webhook-ovh.certManager.namespace) must match the namespace cert-manager's own pods actually run in. cert-manager.enabled=true installs cert-manager as a normal Helm dependency of this chart, which lands in whatever --namespace you pass to helm install/helm upgrade — not this chart's own namespace.name (EasyLab's own resources are explicitly namespaced independently of that flag; cert-manager's are not). Always pass --namespace cert-manager --create-namespace explicitly when using any of the options below, and verify with:
kubectl get pods -n cert-manager
If cert-manager is pre-installed separately, use its actual namespace instead (commonly cert-manager).
Enable only one ClusterIssuer mechanism at a time: certManager.clusterIssuer.create (HTTP-01 or Azure DNS-01) or dns.ovh.enabled (OVH DNS-01) — not both.
HTTP-01 (default solver, no DNS provider)
Needs no DNS credentials, but the EasyLab ingress host must be reachable on port 80 for the ACME challenge:
helm install easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--namespace cert-manager --create-namespace \
--set secrets.adminPassword="SuperAdmin" \
--set traefik.enabled=true \
--set cert-manager.enabled=true \
--set certManager.clusterIssuer.create=true \
--set certManager.clusterIssuer.email="you@example.com" \
--set ingress.enabled=true \
--set ingress.host="easylab.example.com" \
--set ingress.tls.enabled=true \
--wait --timeout 5m
--wait --timeout 5m makes Helm wait for cert-manager's Deployments (webhook included) to be ready before the post-install hook that creates the ClusterIssuer fires — without it, the ClusterIssuer can fail to create on a fresh combined install because cert-manager's admission webhook isn't up yet. The cert-manager.io/cluster-issuer Ingress annotation is added automatically; no manual ingress.annotations --set is needed here.
Azure DNS-01
No port-80 requirement; uses cert-manager's built-in azureDNS solver:
helm install easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--namespace cert-manager --create-namespace \
--set secrets.adminPassword="SuperAdmin" \
--set traefik.enabled=true \
--set cert-manager.enabled=true \
--set certManager.clusterIssuer.create=true \
--set certManager.clusterIssuer.email="you@example.com" \
--set dns.azure.enabled=true \
--set dns.azure.zone="example.com" \
--set dns.azure.tenantId="..." \
--set dns.azure.subscriptionId="..." \
--set dns.azure.resourceGroup="..." \
--set dns.azure.clientId="..." \
--set dns.azure.clientSecret="..." \
--set ingress.enabled=true \
--set ingress.host="easylab.example.com" \
--set ingress.tls.enabled=true \
--wait --timeout 5m
A ready-to-edit values file for this scenario (AKS + Azure DNS-01 + ExternalDNS, with every value you must fill in marked xxx) ships with the chart as helm/easylab/values-azure-example.yaml.
OVH DNS-01
Vendors the cert-manager-webhook-ovh chart, which manages its own ClusterIssuer, credential Secret, and RBAC end-to-end — the same webhook EasyLab's own Pulumi-provisioned student clusters use for DNS-01. Its values are deeply nested, so a values file is easier than a long --set chain:
# ovh-dns01-values.yaml
secrets:
adminPassword: "SuperAdmin"
traefik:
enabled: true
cert-manager:
enabled: true
dns:
ovh:
enabled: true
cert-manager-webhook-ovh:
issuers:
- name: letsencrypt-ovh
create: true
kind: ClusterIssuer
acmeServerUrl: https://acme-v02.api.letsencrypt.org/directory
email: "you@example.com"
ovhEndpointName: ovh-eu
ovhAuthenticationMethod: application
ovhAuthentication:
applicationKey: "your-app-key"
applicationSecret: "your-app-secret"
applicationConsumerKey: "your-consumer-key"
ingress:
enabled: true
host: easylab.example.com
tls:
enabled: true
# The auto-annotation only covers certManager.clusterIssuer.create (HTTP-01/
# Azure) above — for OVH, match this to cert-manager-webhook-ovh.issuers[0].name:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-ovh
helm install easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--namespace cert-manager --create-namespace \
-f ovh-dns01-values.yaml \
--wait --timeout 5m
Unlike the HTTP-01/Azure path above, the OVH issuer is created directly by the vendored webhook chart's own templates, not deferred to a post-install hook — --wait --timeout 5m reduces but does not fully eliminate a possible webhook-readiness race on a fresh combined install. If the first install fails on this, a helm upgrade retry with the same values succeeds once cert-manager is up. See the cert-manager-webhook-ovh values reference for the full set of issuers[] fields (OAuth2 authentication, EAB, alternate OVH endpoints, etc.).
Fresh cluster with dns.ovh.enabled=true: install in two steps
Combining cert-manager.enabled=true and dns.ovh.enabled=true in a single helm install on a cluster that has no cert-manager CRDs yet fails with errors like:
Error: INSTALLATION FAILED: unable to build kubernetes objects from release manifest:
[resource mapping not found for name: "letsencrypt-ovh" namespace: "" from "":
no matches for kind "ClusterIssuer" in version "cert-manager.io/v1"
ensure CRDs are installed first, ...]
cert-manager-webhook-ovh subchart — against the cluster's API discovery cache before applying anything. Since the cert-manager-webhook-ovh subchart's Certificate/Issuer/ClusterIssuer resources are validated in that same pass, and the cert-manager CRDs those kinds depend on don't exist on the API server yet, the whole install aborts before creating anything — including cert-manager itself.
Fix by splitting into two steps. First, install cert-manager and its CRDs only, with the OVH webhook disabled (dns.ovh.enabled is the condition: gate for that entire dependency in Chart.yaml, so setting it false drops its templates from the render):
helm install easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--namespace cert-manager --create-namespace \
--set secrets.adminPassword="SuperAdmin" \
--set traefik.enabled=true \
--set cert-manager.enabled=true \
--set cert-manager.crds.enabled=true \
--set dns.ovh.enabled=false \
--wait --timeout 5m
dns.ovh.enabled: true included), now that the CRDs are registered:
helm upgrade easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--namespace cert-manager \
-f ovh-dns01-values.yaml \
--wait --timeout 5m
kubectl get crd | grep cert-manager.io.
Once any of the above is applied, check certificate status with:
kubectl describe clusterissuer <issuer-name>
kubectl describe certificate -n easylab easylab-tls
Automatic DNS record for the EasyLab ingress (ExternalDNS)
Everything above (certManager.clusterIssuer.create, dns.azure.enabled, dns.ovh.enabled) gets you a certificate for ingress.host — it does not make the hostname resolve. DNS-01 challenges only prove domain ownership via an ephemeral ACME TXT record that cert-manager deletes again once validated; the actual A record pointing ingress.host at the ingress controller's LoadBalancer IP has always been a manual step, done once by hand in your DNS provider's console.
Setting dns.externalDns.enabled=true automates that: it runs a persistent ExternalDNS controller that watches the EasyLab Ingress and keeps its A record in sync with the ingress controller's current LoadBalancer IP — including if that IP ever changes. It requires one of the OVH or Azure DNS-01 sections above to already be configured, since it reuses those same credentials (nothing new to enter, beyond the OVH zone below).
OVH, layered onto the ovh-dns01-values.yaml example above — add dns.ovh.zone (the zone ingress.host is a subdomain of, e.g. labdevrel.ovh) and dns.externalDns.enabled:
dns:
ovh:
enabled: true
zone: labdevrel.ovh # NEW — ExternalDNS needs the zone itself, not the ingress host
externalDns:
enabled: true
dryRun: true # recommended for a first rollout — see below
Azure needs no new value — dns.azure.zone is already required for the DNS-01 solver and is reused as-is:
dns:
azure:
enabled: true
zone: example.com
externalDns:
enabled: true
dryRun: true
helm upgrade easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--namespace easylab \
-f your-values.yaml \
--wait --timeout 5m
Recommended: dry-run first. With dns.externalDns.dryRun=true, ExternalDNS logs the DNS changes it would make without calling the OVH/Azure API. Confirm it identified the right record, then set dryRun: false and helm upgrade again to go live:
kubectl get deploy,pods -n easylab -l app.kubernetes.io/component=externaldns
kubectl logs -n easylab -l app.kubernetes.io/component=externaldns
Once live, confirm the hostname resolves without any manual step: dig +short <ingress.host>.
Wildcard ingress.host
A wildcard host (e.g. *.easylab.example.com) works with ExternalDNS — both OVH and Azure DNS accept a * subdomain prefix for an A record. The one constraint is TLS: Let's Encrypt only issues wildcard certificates via DNS-01, never HTTP-01, so a wildcard ingress.host needs dns.ovh.enabled or dns.azure.enabled already set (which dns.externalDns.enabled requires anyway) — it will not work with the default HTTP-01 certManager.clusterIssuer.create path.
Exposing with Traefik
The chart creates a standard Kubernetes Ingress and defaults ingress.className to traefik, which matches Traefik’s default IngressClass on many clusters (for example k3s and typical Traefik Helm installs). If your cluster has no ingress controller yet, set traefik.enabled=true to have this chart install Traefik for you — see Optional infrastructure components.
Prerequisites
- Traefik running with the Kubernetes Ingress provider enabled (either pre-existing, or installed via
traefik.enabled=true). - An IngressClass whose name matches
ingress.className(defaulttraefik). Check with:
kubectl get ingressclass
If your class is named differently (for example traefik-internal), set --set ingress.className=traefik-internal or the same field in your values file.
Expose EasyLab
- Keep the app service internal: leave
service.typeasClusterIP(default). - Enable ingress and set your hostname:
ingress:
enabled: true
host: easylab.example.com
className: traefik
- Optional: add Traefik-specific annotations under
ingress.annotationsif your install uses non-default entrypoint names, for example:
ingress:
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
Adjust names to match your Traefik static configuration (entryPoints).
TLS can be enabled with ingress.tls and a TLS secret in the same namespace, or with cert-manager annotations on the Ingress (same pattern as other ingress controllers).
Redirecting HTTP to HTTPS
When ingress.className is traefik and ingress.tls.enabled=true, the chart creates a Traefik redirectScheme Middleware and references it from the Ingress automatically. Nothing extra to set:
ingress:
enabled: true
host: easylab.example.com
className: traefik
tls:
enabled: true
renders a <release>-https-redirect Middleware plus this annotation on the Ingress:
traefik.ingress.kubernetes.io/router.middlewares: easylab-easylab-https-redirect@kubernetescrd
Serving EasyLab over plain HTTP breaks login
This is not cosmetic. The login page hashes the password with crypto.subtle (Web Crypto), which browsers expose only in a secure context. Over http://, crypto.subtle is undefined and the login form fails with TypeError: Cannot read properties of undefined (reading 'digest') in the browser console, showing only a generic "An error occurred. Please try again." to the user. If port 80 answers without redirecting, any client that reaches the http:// URL — from a bookmark, a history entry, or a typed hostname — cannot log in at all. The default permanent: true (301) means the browser remembers the redirect, which is what repairs a client already stuck on such a URL.
The Middleware is deliberately not created when ingress.tls.enabled=false, since redirecting to HTTPS with no certificate configured only breaks the site a different way. Turn the redirect off with ingress.httpsRedirect.enabled=false.
If you set traefik.ingress.kubernetes.io/router.middlewares yourself under ingress.annotations, your value replaces the chart's rather than adding to it — include <namespace>-<release>-https-redirect@kubernetescrd in your own comma-separated chain to keep the redirect.
Other ingress controllers express this as their own annotation instead — the chart does not manage it for them. For NGINX:
ingress:
className: nginx
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
Examples
Minimal install
helm install easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--set secrets.adminPassword="SuperAdmin"
With Traefik ingress and TLS (cert-manager)
ingress.className defaults to traefik; set it explicitly here only if your IngressClass name differs. HTTP is redirected to HTTPS automatically — see Redirecting HTTP to HTTPS.
helm install easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--set secrets.adminPassword="SuperAdmin" \
--set ingress.enabled=true \
--set ingress.host="easylab.example.com" \
--set ingress.tls.enabled=true \
--set ingress.className=traefik \
--set ingress.annotations."cert-manager\.io/cluster-issuer"=letsencrypt
With nginx ingress and TLS
If you use the NGINX Ingress Controller instead, set ingress.className to your NGINX IngressClass (often nginx).
helm install easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--set secrets.adminPassword="SuperAdmin" \
--set ingress.enabled=true \
--set ingress.host="easylab.example.com" \
--set ingress.tls.enabled=true \
--set ingress.className=nginx \
--set ingress.annotations."cert-manager\.io/cluster-issuer"=letsencrypt
Fresh cluster (with Traefik and cert-manager)
For a cluster that does not have an ingress controller or cert-manager yet, traefik.enabled=true installs Traefik with the default ingress.className=traefik — no need to override it. certManager.clusterIssuer.create=true has this chart create the ClusterIssuer too (HTTP-01 here — see Certificates for the EasyLab ingress for Azure/OVH DNS-01 alternatives), so the certificate actually gets issued instead of the Ingress annotation pointing at a ClusterIssuer that doesn't exist:
helm install easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--namespace cert-manager --create-namespace \
--set secrets.adminPassword="SuperAdmin" \
--set traefik.enabled=true \
--set cert-manager.enabled=true \
--set certManager.clusterIssuer.create=true \
--set certManager.clusterIssuer.email="you@example.com" \
--set ingress.enabled=true \
--set ingress.host="easylab.example.com" \
--set ingress.tls.enabled=true \
--wait --timeout 5m
--namespace cert-manager --create-namespace makes certManager.namespace's default line up with where cert-manager actually lands (see the namespace alignment note above). --wait --timeout 5m lets cert-manager's webhook come up before the ClusterIssuer is created. The cert-manager.io/cluster-issuer annotation is now added to the Ingress automatically — no manual ingress.annotations --set needed.
With OVH credentials
helm install easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--set secrets.adminPassword="SuperAdmin" \
--set secrets.ovh.applicationKey="your-key" \
--set secrets.ovh.applicationSecret="your-secret" \
--set secrets.ovh.consumerKey="your-consumer-key" \
--set secrets.ovh.serviceName="your-service-name"
Pinning to a dedicated node pool
To keep the EasyLab server off the node pool student workspaces run on (see Splitting EasyLab and workspaces across node pools), label a "control-plane" pool and target it:
helm install easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--set secrets.adminPassword="SuperAdmin" \
--set nodeSelector.pool="control-plane"
If that pool is also tainted, add a matching toleration:
--set tolerations[0].key="dedicated" \
--set tolerations[0].operator="Equal" \
--set tolerations[0].value="easylab" \
--set tolerations[0].effect="NoSchedule"
Using a custom values file
Create a my-values.yaml:
namespace:
name: my-lab
secrets:
adminPassword: "SuperAdmin"
studentPassword: "StudentPass"
ovh:
applicationKey: "your-key"
applicationSecret: "your-secret"
consumerKey: "your-consumer-key"
serviceName: "your-service-name"
ingress:
enabled: true
host: easylab.mycompany.com
className: traefik
tls:
enabled: true
persistence:
jobs:
size: 5Gi
storageClass: longhorn
data:
size: 1Gi
storageClass: longhorn
Then install:
helm install easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
-f my-values.yaml
Upgrade
helm upgrade easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
-f my-values.yaml
Uninstall
helm uninstall easylab
PersistentVolumeClaims are not deleted by helm uninstall
To fully clean up, delete the PVCs manually:
kubectl delete pvc -n easylab -l app.kubernetes.io/name=easylab
Generate raw Kubernetes manifests
If you prefer deploying with plain kubectl instead of Helm, you can use helm template to render the chart into standard Kubernetes YAML manifests.
Render to stdout
helm template easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
--set secrets.adminPassword="SuperAdmin" \
> easylab-manifests.yaml
Render with custom values
helm template easylab oci://registry-1.docker.io/yodamad/easylab-helm \
--version 1.0.0 \
-f my-values.yaml \
> easylab-manifests.yaml
Apply with kubectl
kubectl apply -f easylab-manifests.yaml
All Helm values work with helm template
The same --set flags and -f values.yaml files used with helm install work identically with helm template. The only difference is that the output goes to a file instead of being applied to the cluster.