Your first deployment
This walkthrough takes a provisioned Kestrel tenant and runs a minimal nginx Deployment end-to-end: create your own Git repo, add one manifest per resource, push, register the repo as a single ArgoCD Application, watch ArgoCD reconcile, and hit the app in your browser. You will not run kubectl apply at any point — ArgoCD owns every write to the cluster. Read-only kubectl (get, describe, logs, auth can-i) shows up in Step 1 and Step 7 for verification only.
The walkthrough
Section titled “The walkthrough”-
Verify your tenant is provisioned.
Run these three read-only commands and confirm each returns the expected output:
Terminal window kubectl config current-contextkubectl get nskubectl auth can-i create deployments --namespace=<your-tenant>-democurrent-contextshould printkestrel(or your locally-configured context name).kubectl get nsshould list at least one namespace starting with your tenant name.can-ishould printyes. If it printsnoor errors, revisit Install kubelogin and confirm your first OIDC login succeeded.
-
Create your workload repository.
Create a new Git repository on your preferred hosting service (GitHub, GitLab, etc.) for your Kestrel manifests. Clone it locally:
Terminal window git clone https://github.com/<your-org>/<your-repo>.gitcd <your-repo>mkdir -p manifests/demo-nginx -
Create your tenant-prefixed namespace.
The walkthrough deploys into
<your-tenant>-demo. Createmanifests/demo-nginx/namespace.yamlin your repo with the content below, replacing the<your-tenant>placeholder with your real tenant name:# You own this — replace <your-tenant> with your real tenant nameapiVersion: v1kind: Namespacemetadata:name: <your-tenant>-demolabels:capsule.clastix.io/tenant: <your-tenant>The
capsule.clastix.io/tenantlabel is what ties the namespace into your ResourcePool and NetworkPolicy rules. If you omit it or mis-spell your tenant, Capsule rejects the namespace at ArgoCD sync time. See Known limitations for the full rule. -
Create the Deployment manifest.
Create
manifests/demo-nginx/deployment.yamlwith the content below — edit<your-tenant>to your tenant name and leave everything else alone on the first walk:# You own this — edit <your-tenant> to your tenant name; replicas/image/resources are yours to tuneapiVersion: apps/v1kind: Deploymentmetadata:name: demo-nginxnamespace: <your-tenant>-demospec:replicas: 1selector:matchLabels:app: demo-nginxtemplate:metadata:labels:app: demo-nginxspec:priorityClassName: uber-user-significant # fine for a first walk; production long-running services should use uber-user-preempt-high — see /running/priority-classes/securityContext:runAsNonRoot: truerunAsUser: 101seccompProfile:type: RuntimeDefaultcontainers:- name: nginximage: nginxinc/nginx-unprivileged:1.27-alpineports:- containerPort: 8080securityContext:allowPrivilegeEscalation: falsereadOnlyRootFilesystem: truecapabilities:drop: [ALL]resources:requests:cpu: 100mmemory: 64Milimits:cpu: 200mmemory: 128MivolumeMounts:- name: tmpmountPath: /tmp- name: cachemountPath: /var/cache/nginx- name: runmountPath: /var/runvolumes:- name: tmpemptyDir: {}- name: cacheemptyDir: {}- name: runemptyDir: {} -
Create the Service and Ingress.
Create
manifests/demo-nginx/service.yamlandmanifests/demo-nginx/ingress.yamlwith the content below, replacing<your-tenant>in both. The Service is aClusterIPtype —LoadBalancerandNodePortare blocked on Kestrel, see Known limitations. The Ingress usesingressClassName: traefikand a tenant-scoped hostnamedemo.<your-tenant>.kestrel.arbutus.cloud:# You own this — replace <your-tenant> with your real tenant nameapiVersion: v1kind: Servicemetadata:name: demo-nginxnamespace: <your-tenant>-demospec:type: ClusterIPselector:app: demo-nginxports:- port: 80targetPort: 8080---apiVersion: networking.k8s.io/v1kind: Ingressmetadata:name: demo-nginxnamespace: <your-tenant>-demoannotations:cert-manager.io/cluster-issuer: letsencrypt-prodtraefik.ingress.kubernetes.io/router.entrypoints: websecuretraefik.ingress.kubernetes.io/router.tls: "true"spec:ingressClassName: traefiktls:- hosts:- demo.<your-tenant>.kestrel.arbutus.cloudsecretName: demo-nginx-tlsrules:- host: demo.<your-tenant>.kestrel.arbutus.cloudhttp:paths:- path: /pathType: Prefixbackend:service:name: demo-nginxport:number: 80 -
Commit, push, and register the repo with ArgoCD.
Terminal window git add manifests/demo-nginx/git commit -m "Register demo-nginx for <your-tenant>"git push origin mainThen log in to the ArgoCD UI at
https://kestrel.arbutus.cloud/argocdvia Keycloak (the same Alliance LDAP-backed login askubelogin), click New App, paste your repo’s HTTPS URL as the source, set the path tomanifests/demo-nginx, and save. ArgoCD creates a single Application that reconciles the manifests in that directory. This is a one-time registration per repo — the full ArgoCD UI tour lives in ArgoCD. For multi-workload repos and the app-of-apps layout, see User brings a repo. -
Watch the sync and verify with read-only kubectl.
In the ArgoCD UI, your new Application should move to
Synced/Healthywithin about 30 seconds. If it showsOutOfSyncorDegraded, click through to the sync message — usually a YAML validation error or a Kyverno admission rejection, both of which show up in the UI diff.Verify from the command line with read-only kubectl:
Terminal window kubectl get pods -n <your-tenant>-demokubectl get ingress -n <your-tenant>-demokubectl logs -n <your-tenant>-demo deployment/demo-nginxExpected: a
demo-nginx-<hash>-<hash>Pod isRunning, the Ingress has the hostnamedemo.<your-tenant>.kestrel.arbutus.cloudassigned, and the logs show nginx startup messages. -
Hit the app.
Open
https://demo.<your-tenant>.kestrel.arbutus.cloud/in a browser. You should see the default nginx welcome page. TLS is handled by cert-manager and Let’s Encrypt production — if the page loads without a browser warning, the full chain worked.
If something goes wrong
Section titled “If something goes wrong”- Pod
Runningbut Ingress 404. The TLS cert has not been issued yet (cert-manager takes ~30 seconds on the first request). Wait and retry. - Pod
CrashLoopBackOfforErrorwithpods "demo-nginx-..." is forbidden: violates PodSecurityin the ArgoCD sync message. You edited thesecurityContextblock — revert to the manifest in Step 4 verbatim. - Namespace rejected with
violates PodSecurityorforceTenantPrefix. The namespace name or thecapsule.clastix.io/tenantlabel does not match your real tenant. See Known limitations. kubectl get nsreturns nothing from your tenant. Yourkubeloginsession is missing the tenant group claim. See Install kubelogin troubleshooting.- Reactive triage for other first-time breakers. See Triage for the first places to check across the four common stuck-states (kubelogin, ArgoCD, Ingress, quotas).
Follow-ups
Section titled “Follow-ups”This walkthrough is the narrowest possible path. Once it works for you end-to-end, see ArgoCD for the full UI tour, Recipes for long-running service / batch / dev-pod recipes, and Priority classes for picking the right priorityClassName.