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

  1. 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 ns returns an empty list until you make one. Create the walkthrough namespace now, replacing <your-tenant> with your tenant name:

    Terminal window
    kubectl config current-context
    kubectl create ns <your-tenant>-demo
    kubectl get ns
    kubectl auth can-i create deployments --namespace=<your-tenant>-demo
    • current-context should print kestrel (or your locally-configured context name).
    • create ns should print namespace/<your-tenant>-demo created.
    • kubectl get ns should now list <your-tenant>-demo.
    • can-i should print yes. Note that can-i answers no for any namespace that does not exist yet — so if it prints no, first confirm the namespace name matches the one you just created, then revisit Install kubelogin and confirm your first OIDC login succeeded (kubectl auth whoami shows your oidc:<your-tenant> group).
  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. 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. 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, and fill in:

    FieldValue
    Application Name<your-tenant>-demo-nginx
    Project<your-tenant> — your tenant’s AppProject, the only one you can select
    Repository URL (source)your repo’s HTTPS URL
    Path (source)manifests/demo-nginx
    Cluster URL (destination)https://kubernetes.default.svc — ArgoCD’s in-cluster address for Kestrel itself; it is not a kestrel.arbutus.cloud hostname
    Namespace (destination)<your-tenant>-demo

    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.
  • Certificate stuck at READY: False for more than a few minutes, or the hostname does not resolve. The DNS record for demo.<your-tenant>.kestrel.arbutus.cloud probably does not exist yet — DNS records are currently created manually (see the caution in Step 5). Check with kubectl 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 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 auth whoami is missing your oidc:<your-tenant> group. Your kubelogin session is missing the tenant group claim. See Install kubelogin troubleshooting. (An empty kubectl get ns on 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).

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.