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. The one direct write is kubectl create ns in Step 1, which creates your first namespace (Kestrel does not pre-create any); everything after that is read-only kubectl (get, describe, logs, auth can-i) for verification.
The walkthrough
Section titled “The walkthrough”-
Verify your tenant is provisioned and create your first namespace.
A freshly provisioned tenant has no namespaces — Kestrel does not pre-create any for you, so
kubectl get nsreturns an empty list until you make one. Create the walkthrough namespace now, replacing<your-tenant>with your tenant name:Terminal window kubectl config current-contextkubectl create ns <your-tenant>-demokubectl get nskubectl auth can-i create deployments --namespace=<your-tenant>-democurrent-contextshould printkestrel(or your locally-configured context name).create nsshould printnamespace/<your-tenant>-demo created.kubectl get nsshould now list<your-tenant>-demo.can-ishould printyes. Note thatcan-ianswersnofor any namespace that does not exist yet — so if it printsno, first confirm the namespace name matches the one you just created, then revisit Install kubelogin and confirm your first OIDC login succeeded (kubectl auth whoamishows youroidc:<your-tenant>group).
-
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 -
Add the namespace manifest to your repo.
The walkthrough deploys into
<your-tenant>-demo— the namespace you already created in Step 1. ArgoCD adopts that existing namespace on its first sync (it does not conflict with it), so the namespace is tracked in Git from then on. 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, and fill in:Field Value Application Name <your-tenant>-demo-nginxProject <your-tenant>— your tenant’s AppProject, the only one you can selectRepository URL (source) your repo’s HTTPS URL Path (source) manifests/demo-nginxCluster URL (destination) https://kubernetes.default.svc— ArgoCD’s in-cluster address for Kestrel itself; it is not akestrel.arbutus.cloudhostnameNamespace (destination) <your-tenant>-demoSave. 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. - Certificate stuck at
READY: Falsefor more than a few minutes, or the hostname does not resolve. The DNS record fordemo.<your-tenant>.kestrel.arbutus.cloudprobably does not exist yet — DNS records are currently created manually (see the caution in Step 5). Check withkubectl get certificate -n <your-tenant>-demo, and open a ticket with RCS to have the hostname created. Once the record exists, cert-manager retries and issues the certificate without any action on your side. - 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 auth whoamiis missing youroidc:<your-tenant>group. Yourkubeloginsession is missing the tenant group claim. See Install kubelogin troubleshooting. (An emptykubectl get nson its own is not a failure — namespaces only exist once you create them in Step 1.)- 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.