Skip to content

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.

  1. Verify your tenant is provisioned.

    Run these three read-only commands and confirm each returns the expected output:

    Terminal window
    kubectl config current-context
    kubectl get ns
    kubectl auth can-i create deployments --namespace=<your-tenant>-demo
    • current-context should print kestrel (or your locally-configured context name).
    • kubectl get ns should list at least one namespace starting with your tenant name.
    • can-i should print yes. If it prints no or errors, revisit Install kubelogin and confirm your first OIDC login succeeded.
  2. 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>.git
    cd <your-repo>
    mkdir -p manifests/demo-nginx
  3. Create your tenant-prefixed namespace.

    The walkthrough deploys into <your-tenant>-demo. Create manifests/demo-nginx/namespace.yaml in 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 name
    apiVersion: v1
    kind: Namespace
    metadata:
    name: <your-tenant>-demo
    labels:
    capsule.clastix.io/tenant: <your-tenant>

    The capsule.clastix.io/tenant label 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.

  4. Create the Deployment manifest.

    Create manifests/demo-nginx/deployment.yaml with 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 tune
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: demo-nginx
    namespace: <your-tenant>-demo
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: demo-nginx
    template:
    metadata:
    labels:
    app: demo-nginx
    spec:
    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: true
    runAsUser: 101
    seccompProfile:
    type: RuntimeDefault
    containers:
    - name: nginx
    image: nginxinc/nginx-unprivileged:1.27-alpine
    ports:
    - containerPort: 8080
    securityContext:
    allowPrivilegeEscalation: false
    readOnlyRootFilesystem: true
    capabilities:
    drop: [ALL]
    resources:
    requests:
    cpu: 100m
    memory: 64Mi
    limits:
    cpu: 200m
    memory: 128Mi
    volumeMounts:
    - name: tmp
    mountPath: /tmp
    - name: cache
    mountPath: /var/cache/nginx
    - name: run
    mountPath: /var/run
    volumes:
    - name: tmp
    emptyDir: {}
    - name: cache
    emptyDir: {}
    - name: run
    emptyDir: {}
  5. Create the Service and Ingress.

    Create manifests/demo-nginx/service.yaml and manifests/demo-nginx/ingress.yaml with the content below, replacing <your-tenant> in both. The Service is a ClusterIP type — LoadBalancer and NodePort are blocked on Kestrel, see Known limitations. The Ingress uses ingressClassName: traefik and a tenant-scoped hostname demo.<your-tenant>.kestrel.arbutus.cloud:

    # You own this — replace <your-tenant> with your real tenant name
    apiVersion: v1
    kind: Service
    metadata:
    name: demo-nginx
    namespace: <your-tenant>-demo
    spec:
    type: ClusterIP
    selector:
    app: demo-nginx
    ports:
    - port: 80
    targetPort: 8080
    ---
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
    name: demo-nginx
    namespace: <your-tenant>-demo
    annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
    spec:
    ingressClassName: traefik
    tls:
    - hosts:
    - demo.<your-tenant>.kestrel.arbutus.cloud
    secretName: demo-nginx-tls
    rules:
    - host: demo.<your-tenant>.kestrel.arbutus.cloud
    http:
    paths:
    - path: /
    pathType: Prefix
    backend:
    service:
    name: demo-nginx
    port:
    number: 80
  6. 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 main

    Then log in to the ArgoCD UI at https://kestrel.arbutus.cloud/argocd via Keycloak (the same Alliance LDAP-backed login as kubelogin), click New App, paste your repo’s HTTPS URL as the source, set the path to manifests/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.

  7. Watch the sync and verify with read-only kubectl.

    In the ArgoCD UI, your new Application should move to Synced / Healthy within about 30 seconds. If it shows OutOfSync or Degraded, 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>-demo
    kubectl get ingress -n <your-tenant>-demo
    kubectl logs -n <your-tenant>-demo deployment/demo-nginx

    Expected: a demo-nginx-<hash>-<hash> Pod is Running, the Ingress has the hostname demo.<your-tenant>.kestrel.arbutus.cloud assigned, and the logs show nginx startup messages.

  8. 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.

  • Pod Running but Ingress 404. The TLS cert has not been issued yet (cert-manager takes ~30 seconds on the first request). Wait and retry.
  • Pod CrashLoopBackOff or Error with pods "demo-nginx-..." is forbidden: violates PodSecurity in the ArgoCD sync message. You edited the securityContext block — revert to the manifest in Step 4 verbatim.
  • Namespace rejected with violates PodSecurity or forceTenantPrefix. The namespace name or the capsule.clastix.io/tenant label does not match your real tenant. See Known limitations.
  • kubectl get ns returns nothing from your tenant. Your kubelogin session 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).

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.