Skip to content

Your first deployment

This page walks the whole way once: container image in, working HTTPS address out. It assumes kubectl is set up and your default namespace is set, see Access and kubectl.

The conventions for routes, listener names and certificates are documented under Gateway API. Here they are applied, not explained a second time.

The manifest

Three objects, one file. Save it as app.yaml and replace two things: put your own namespace in the hostname, and your own image in the container.

Your namespace already owns every name under <your-namespace>.itsh.dev, so myapp.<your-namespace>.itsh.dev works immediately with no DNS setup. Free-tier namespaces use <your-namespace>.itsh-apps.dev instead. Serving a domain of your own is a separate step, see Gateway API.

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: registry.example.com/myapp:1.0.0
          ports:
            - name: http
              containerPort: 8080
          securityContext:
            allowPrivilegeEscalation: false
            runAsNonRoot: true
            seccompProfile:
              type: RuntimeDefault
            capabilities:
              drop: ["ALL"]
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
          readinessProbe:
            httpGet:
              path: /readyz
              port: http
            initialDelaySeconds: 3
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /healthz
              port: http
            initialDelaySeconds: 10
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
  ports:
    - name: http
      port: 80
      targetPort: http
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: myapp
  annotations:
    cert-manager.io/cluster-issuer: "auto"
spec:
  parentRefs:
    - name: default
      namespace: nginx-gateway
      sectionName: https-myapp-your-namespace-itsh-dev
  hostnames:
    - myapp.<your-namespace>.itsh.dev
  rules:
    - backendRefs:
        - name: myapp
          port: 80

Four details in there are not optional extras:

  • Set your requests deliberately. They decide how much capacity is reserved for your pod, and they are what you are billed on, see Billing and quotas. Leaving them out does not fail: your namespace fills in a small default instead, currently 50m CPU and 64Mi of memory, with limits of 200m and 256Mi. That is rarely what your application wants, it is billed all the same, and a later HPA has no useful baseline. Set the numbers rather than inheriting them.
  • The securityContext keeps your first apply quiet. Without it Kubernetes prints four warnings about privilege escalation, capabilities, root and seccomp. They are warnings rather than rejections, so the pod still starts, but the settings are worth having and the noise is not. One caveat: runAsNonRoot: true needs an image that actually runs as a non-root user. If yours does not, drop that one line and keep the other three, which still apply and still remove three of the four warnings.
  • Probes separate "running" from "able to answer requests". Without a readiness probe the service sends traffic to pods that are still starting. Adjust the paths to what your application actually serves.
  • sectionName has to match the hostname. Take the hostname and replace every dot with a dash, so myapp.tenant-7.itsh.dev gives https-myapp-tenant-7-itsh-dev. Get that wrong and your route attaches to the wrong listener, or to none at all.

Your workloads run in a sandboxed runtime, and manifests that break the platform's rules are rejected before they ever reach the cluster. Which rules those are is under Permissions and policies.

Apply it

bash
kubectl apply -f app.yaml
bash
kubectl get pods
kubectl rollout status deployment/myapp

rollout status only returns once every replica is ready. If it just sits there, that is already half the diagnosis: no pod is becoming ready. Cancel with Ctrl+C and work through the troubleshooting below.

DNS

Nothing to do. Your namespace subdomain already points at the gateway, so the hostname in the manifest above resolves as soon as the route exists.

This only becomes a step when you bring your own domain, which is covered under Gateway API.

When something does not come up

kubectl describe pod <name> is the first move in all three cases: the events at the end of the output almost always name the reason.

  • ImagePullBackOff: the image could not be pulled. For a private registry the credentials are missing. Create a secret of type kubernetes.io/dockerconfigjson and reference it under imagePullSecrets in the pod template. For a public registry, check the spelling and the tag.
  • CrashLoopBackOff: the container starts and exits immediately. kubectl logs deployment/myapp shows why. Once the pod has already restarted you need the previous attempt: kubectl logs deployment/myapp --previous.
  • Pending: the pod cannot be placed. Almost always your namespace quota is exhausted. kubectl get resourcequota shows what is left, and Billing and quotas explains what feeds into that quota in the first place.

What's next