Nais Platform Skill
Patterns and procedures for deploying, configuring, and troubleshooting applications on Nais (Kubernetes on GCP).
When to Use
- Creating or modifying Nais manifests
- Adding GCP resources (PostgreSQL, Kafka, buckets)
- Configuring access policies and ingress
- Troubleshooting pod startup failures or crashes
- Understanding pod lifecycle and graceful shutdown
Commands
bash# Check pod status kubectl get pods -n <namespace> -l app=<app-name> # View pod logs (add -f to follow) kubectl logs -n <namespace> -l app=<app-name> --tail=100 -f # Describe pod (events, errors) kubectl describe pod -n <namespace> <pod-name> # View Nais app status kubectl get app -n <namespace> <app-name> -o yaml # Restart deployment (rolling) kubectl rollout restart deployment/<app-name> -n <namespace> # Port-forward for local debugging kubectl port-forward -n <namespace> svc/<app-name> 8080:80
Nais Manifest Structure
Every Nais application requires:
yamlapiVersion: nais.io/v1alpha1 kind: Application metadata: name: app-name namespace: team-namespace labels: team: team-namespace spec: image: {{ image }} # Replaced by CI/CD port: 8080 # Observability (required) prometheus: enabled: true path: /metrics # Health checks (required) liveness: path: /isalive initialDelay: 5 readiness: path: /isready initialDelay: 5 # Resources (required) resources: requests: cpu: 50m memory: 256Mi limits: memory: 512Mi
Pod Lifecycle and Graceful Shutdown
When Kubernetes terminates a pod on NAIS:
- K8s notifies load balancer and pod simultaneously — LB starts draining connections
- NAIS preStop hook runs
sleep 5— gives LB time to stop routing new traffic - App receives SIGTERM — no new requests arrive from LB
- App drains in-flight requests and exits
- After
terminationGracePeriodSeconds(default 30s): SIGKILL
Key insight: Readiness probes are NOT involved in shutdown. Your app just needs to handle SIGTERM, finish in-flight requests, and exit cleanly.
Common anti-patterns:
- ❌ Setting readiness to
falseon SIGTERM — unnecessary - ❌
terminationGracePeriodSecondstoo low — must be > 5s (preStop) + drain time - ❌ Adding a
preStophook with extra sleep — NAIS already injectssleep 5
Common Tasks
Adding PostgreSQL Database
yamlgcp: sqlInstances: - type: POSTGRES_18 databases: - name: myapp-db envVarPrefix: DB
Application receives: DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD
Configuring Kafka
yamlkafka: pool: nav-dev # or nav-prod
Entra ID Authentication
yamlazure: application: enabled: true tenant: nav.no
TokenX for Service-to-Service
yamltokenx: enabled: true accessPolicy: inbound: rules: - application: calling-app namespace: calling-namespace outbound: rules: - application: downstream-app namespace: downstream-namespace external: - host: api.example.com - host: db.example.com ports: - port: 5432
Egress to hosts outside the cluster is blocked unless listed in outbound.external or on the Nais default allow-list (Google APIs, Microsoft login/Graph, Aiven). Use host (no https://) or ipv4, not both. Add ports for protocols other than HTTPS.
Ingress Configuration
yamlingresses: - https://myapp.intern.dev.nav.no # Internal dev - https://myapp.dev.nav.no # External dev
Scaling
yamlreplicas: min: 2 max: 4 cpuThresholdPercentage: 80
Resource Recommendations
| Size | CPU request | Memory request | Memory limit |
|---|---|---|---|
| Small | 50m | 256Mi | 512Mi |
| Medium | 100m | 512Mi | 1Gi |
| Large | 200m | 1Gi | 2Gi |
Never set CPU limits — causes throttling. Use requests only.
Troubleshooting
Pod Not Starting
- Check logs:
kubectl logs -n namespace pod-name - Check events:
kubectl describe pod -n namespace pod-name - Verify health endpoints return 200 OK
- Check resource limits (memory/CPU)
Database Connection Issues
- Verify database exists in GCP Console
- Check environment variables are injected
- Verify Cloud SQL Proxy is running
- Check network policies allow connection
Kafka Connection Issues
- Verify
kafka.poolis correct (nav-dev/nav-prod) - Check Kafka credentials are injected
- Verify SSL configuration
- Check topic exists and permissions are correct
Deployment Workflow
- Create
.nais/app.yamlmanifest - Implement health endpoints (
/isalive,/isready,/metrics) - Test locally with Docker
- Deploy to dev environment
- Verify metrics in Grafana
- Check logs in Loki
- Create prod manifest (
.nais/app-prod.yaml) - Deploy to production
Gotchas
accessPolicydefaults to deny-all — you must explicitly allow traffic- Don't set CPU limits — only requests (limits cause throttling)
- Memory limits are mandatory — missing limits cause OOM cluster issues
- NAIS injects
preStop: sleep 5— don't add your own {{ image }}in manifest is replaced by CI/CD — don't hardcode images- Environment-specific manifests (
app-dev.yaml,app-prod.yaml) are the norm
Boundaries
✅ Always
- Include liveness, readiness, and metrics endpoints
- Set memory limits
- Define explicit
accessPolicyfor network traffic - Use environment-specific manifests
- Run
kubectl get app <name> -o yamlto verify deployment
⚠️ Ask First
- Changing production resource limits or replicas
- Adding new GCP resources (cost implications)
- Modifying network policies (
accessPolicy) - Changing Kafka topic configurations
- Adding new ingress domains
🚫 Never
- Store secrets in Git (use Azure Key Vault or Kubernetes secrets)
- Deploy directly without CI/CD pipeline
- Skip health endpoints
- Set CPU limits
- Remove memory limits


