Software pack tutorial
A Software Pack connects an existing app to Nebari rather than rewriting it. This tutorial walks you through deploying one onto a local Nebari cluster from scratch.
By the end, you'll have:
- A local Nebari cluster running on your laptop
- A real application deployed as a pack, reachable at its own hostname over HTTPS
Prerequisites
You'll need:
Clone Nebari Infrastructure Core (NIC) and run the cluster steps from its repository root:
git clone https://github.com/nebari-dev/nebari-infrastructure-core.git
cd nebari-infrastructure-core
1. Bring up a local Nebari cluster
Run one command to build the nic binary and create a Kind cluster named nebari-local:
make localkind-up
When this finishes, your local cluster is running the same foundational software a real Nebari cluster has, including:
- ArgoCD (deploys packs via GitOps)
- cert-manager (issues HTTPS certificates)
- Envoy Gateway (routes traffic by hostname)
- MetalLB (hands out load-balancer IP addresses)
- Keycloak (handles user sign-in)
- Nebari Operator (watches for
NebariAppresources and wires them into routing and TLS)
Confirm the operator is running:
kubectl get pods -n nebari-operator-system
NAME READY STATUS RESTARTS AGE
nebari-operator-controller-manager-66d9f7fcdf-rlbqc 1/1 Running 0 90s
2. Explore the pack
The example you'll deploy, wrap-existing-chart, wraps podinfo, a small open-source web app. To build your own pack, you'd wrap your app instead of podinfo.
Clone it outside the nebari-infrastructure-core repo:
git clone https://github.com/nebari-dev/nebari-software-pack-template.git
cd nebari-software-pack-template/examples/wrap-existing-chart
The pack is a small Helm chart in the chart/ directory:
wrap-existing-chart/
└── chart/
├── Chart.yaml
├── values.yaml
└── templates/
└── nebariapp.yaml
Three files show how it wraps podinfo:
-
chart/Chart.yamllists podinfo as a dependency:dependencies:
- name: podinfo
version: 6.10.1
repository: oci://ghcr.io/stefanprodan/chartsFor your own app, swap podinfo for your chart's name, version, and repository.
-
chart/values.yamlholds theNebariAppconfiguration plus any overrides for the upstream chart:nebariapp:
enabled: false
# hostname: my-pack.nebari.example.com # required when enabled
service:
port: 9898
podinfo: # values passed to the upstream chart
ui:
message: "Hello from Nebari!"For your own app, set the service port to match it, and rename the
podinfooverride block to your chart's name. -
chart/templates/nebariapp.yamlpoints the operator at podinfo's Service:apiVersion: reconcilers.nebari.dev/v1
kind: NebariApp
metadata:
name: {{ include "my-pack.fullname" . }}
spec:
hostname: {{ .Values.nebariapp.hostname }}
service:
name: {{ .Values.nebariapp.service.name | default (include "my-pack.podinfo-service-name" .) }}
port: {{ .Values.nebariapp.service.port | default 9898 }}For your own app, set
nebariapp.service.nameto its Service name. Names vary by chart, so confirm it withhelm template myrelease ./chart/ | grep -A2 "kind: Service".
Notice that the NebariApp is the one thing the pack adds for Nebari. Everything else comes from podinfo's chart.
3. Deploy the pack
You can deploy the pack two ways: an ArgoCD Application (the recommended, production-like path) or a direct helm install (quickest locally).
Start with creating the demo namespace and labeling it so the operator will manage resources in it:
kubectl create namespace demo
kubectl label namespace demo nebari.dev/managed=true
ArgoCD Application (recommended)
ArgoCD is Nebari's GitOps engine: it pulls manifests from a git repo and keeps the cluster in sync with them.
To deploy the pack, register an Application that tells ArgoCD where the pack lives and what values to use. Save this as application.yaml:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: mypack # your pack's name
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/nebari-dev/nebari-software-pack-template # your pack's repo
targetRevision: main
path: examples/wrap-existing-chart/chart # path to the chart in that repo
helm:
valuesObject:
nebariapp:
enabled: true
hostname: mypack.nebari.local # hostname to expose the app at
routing:
tls:
enabled: true
destination:
server: https://kubernetes.default.svc
namespace: demo # namespace to deploy into
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Apply it with kubectl apply -f application.yaml. ArgoCD pulls the chart, including the upstream podinfo dependency, and the operator wires up the NebariApp.
Helm install
helm install deploys the chart to the cluster in one command, straight from your local copy. Run it from your clone:
helm dependency update ./chart/
helm install mypack ./chart/ \
--namespace demo \
--set nebariapp.enabled=true \
--set nebariapp.hostname=mypack.nebari.local \
--set nebariapp.routing.tls.enabled=true
helm dependency updatefetches the upstream podinfo chart the pack wraps.helm installdeploys the pack as releasemypackintodemo, with theNebariAppenabled, a hostname set, and TLS turned on.
4. Check what the operator created
The operator picks up the NebariApp and creates the routing and certificate for it. Verify the NebariApp is Ready:
kubectl describe nebariapp -n demo
In the Conditions block at the bottom, find the Ready condition. As long as its Status is True, the operator finished setting up the app, creating:
- An
HTTPRoutethat sends traffic formypack.nebari.localto podinfo's Service - A
Certificateformypack.nebari.localso users connect over HTTPS
The app is only reachable inside the cluster, so port-forward the gateway to a local port to reach it from your machine.
First, look up the gateway service name:
GATEWAY_SVC=$(kubectl get svc -n envoy-gateway-system \
-l gateway.envoyproxy.io/owning-gateway-name=nebari-gateway \
-o jsonpath='{.items[0].metadata.name}')
Then forward its HTTPS port to local port 8443:
kubectl port-forward -n envoy-gateway-system "svc/$GATEWAY_SVC" 8443:443
port-forward keeps running, so send the request from another terminal:
curl -k --resolve mypack.nebari.local:8443:127.0.0.1 https://mypack.nebari.local:8443
--resolvekeeps themypack.nebari.localhostname (which the route and certificate require) while connecting to the forwarded port.-kaccepts the cluster's self-signed certificate without verifying it.
You should get podinfo's JSON greeting back, including the message the chart set in values.yaml:
{
"hostname": "mypack-podinfo-657dbffc8d-bh7mc",
"version": "6.10.1",
"message": "Hello from Nebari!",
"goos": "linux",
"goarch": "arm64",
"runtime": "go1.25.6"
}
5. Clean up
When you're finished, remove the pack, then tear down the cluster.
Remove the pack
If you used ArgoCD, delete the Application first, or ArgoCD will re-sync and recreate the pack:
kubectl delete application mypack -n argocd
If you used Helm, uninstall the release:
helm uninstall mypack -n demo
Tear down the cluster
Delete the namespace, then destroy the local cluster:
kubectl delete namespace demo
# from the nebari-infrastructure-core repo
make localkind-down
Where to go next
- Pack architecture: how packs, the
NebariApp, and the operator fit together. - NKP architecture: the platform and foundational software a pack runs on.
- Software Packs catalog: browse the packs OpenTeams and the community have already built.
- Build your own pack: wrap your own chart as a reusable pack.