End-to-end cloud deployment skill for Kubernetes (AKS/GKE/DOKS) with CI/CD pipelines. Covers managed services integration (Neon, Upstash), ingress configuration, SSL certificates, GitHub Actions workflows with selective builds, and Next.js build-time vs runtime environment handling. Battle-tested from 9-hour deployment session.
This skill captures the complete knowledge for deploying a multi-service application to cloud Kubernetes, based on battle-tested learnings from deploying TaskFlow (5 microservices) to Azure AKS.
NEXT_PUBLIC_* variables in Docker/K8s INTERNET
│
▼
┌─────────────────┐
│ Load Balancer │ (Single Public IP)
└────────┬────────┘
│
┌────────▼────────┐
│ Ingress (Traefik│ Routes by subdomain
│ or nginx) │
└────────┬────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Web │ │ SSO │ │ MCP │
│ (PUBLIC) │ │ (PUBLIC) │ │ (PUBLIC) │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ ┌────▼─────┐ │
└──────────────► API ◄─────────────┘
│(INTERNAL)│
└────┬─────┘
│
┌────────────┴────────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Neon │ │ Upstash │
│ (Postgres) │ │ (Redis) │
│ EXTERNAL │ │ EXTERNAL │
└─────────────┘ └─────────────┘
Next.js NEXT_PUBLIC_* variables are embedded at build time, not runtime. This means:
# WRONG: Setting NEXT_PUBLIC_* at runtime does NOTHING
ENV NEXT_PUBLIC_API_URL=https://api.example.com
# RIGHT: Must be set as build ARG
ARG NEXT_PUBLIC_API_URL=https://api.example.com
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
| Service | Variable | Purpose |
|---------|----------|---------|
| Web | NEXT_PUBLIC_SSO_URL | SSO endpoint for browser OAuth |
| Web | NEXT_PUBLIC_API_URL | API endpoint for browser fetch |
| Web | NEXT_PUBLIC_APP_URL | App URL for redirects |
| SSO | NEXT_PUBLIC_BETTER_AUTH_URL | Better Auth URL for browser |
| SSO | NEXT_PUBLIC_CONTINUE_URL | Redirect after email verify |
| Service | Variable | Source |
|---------|----------|--------|
| SSO | DATABASE_URL | Secret (Neon) |
| SSO | BETTER_AUTH_SECRET | Secret |
| API | SSO_URL | ConfigMap (internal K8s URL) |
| MCP | TASKFLOW_SSO_URL | ConfigMap (internal K8s URL) |
Services communicate via K8s service names, NOT public URLs:
# CORRECT - Internal communication
SSO_URL: http://sso-platform:3001
API_URL: http://taskflow-api:8000
# WRONG - Don't use public URLs for internal traffic
SSO_URL: https://sso.example.com
jobs:
changes:
runs-on: ubuntu-latest
outputs:
api: ${{ steps.filter.outputs.api }}
web: ${{ steps.filter.outputs.web }}
steps:
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
api:
- 'apps/api/**'
web:
- 'apps/web/**'
build-api:
needs: changes
if: needs.changes.outputs.api == 'true' || github.event_name == 'workflow_dispatch'
- name: Build and push (web)
uses: docker/build-push-action@v5
with:
build-args: |
NEXT_PUBLIC_SSO_URL=https://sso.${{ vars.DOMAIN }}
NEXT_PUBLIC_API_URL=https://api.${{ vars.DOMAIN }}
NEXT_PUBLIC_APP_URL=https://${{ vars.DOMAIN }}
NEON_SSO_DATABASE_URL
NEON_API_DATABASE_URL
NEON_CHATKIT_DATABASE_URL
NEON_NOTIFICATION_DATABASE_URL
UPSTASH_REDIS_HOST
UPSTASH_REDIS_PASSWORD
REDIS_URL
REDIS_TOKEN
BETTER_AUTH_SECRET
OPENAI_API_KEY
SMTP_USER
SMTP_PASSWORD
AZURE_CREDENTIALS (or GCP_CREDENTIALS)
DOMAIN=example.com
CLOUD_PROVIDER=azure
AZURE_RESOURCE_GROUP=myapp-rg
AZURE_CLUSTER_NAME=myapp-cluster
INGRESS_CLASS=traefik
global:
domain: "" # Set via --set
namespace: taskflow
imagePullPolicy: Always
managedServices:
neon:
enabled: true
# Connection strings injected via --set from secrets
upstash:
enabled: true
# Credentials injected via --set from secrets
sso:
enabled: true
name: sso-platform
postgresql:
enabled: false # Using Neon
env:
NODE_ENV: production
BETTER_AUTH_URL: "" # Set via --set
helm upgrade --install taskflow ./infrastructure/helm/taskflow \
--values values-cloud.yaml \
--set global.imageRegistry="ghcr.io/owner/repo" \
--set global.imageTag="${{ github.sha }}" \
--set "managedServices.neon.ssoDatabase=${{ secrets.NEON_SSO_DATABASE_URL }}" \
--set "sso.env.BETTER_AUTH_SECRET=${{ secrets.BETTER_AUTH_SECRET }}"
BEFORE ANY DEPLOYMENT, check your cluster's node architecture:
kubectl get nodes -o jsonpath='{.items[*].status.nodeInfo.architecture}'
amd64 → Use platforms: linux/amd64arm64 → Use platforms: linux/arm64ARM64 is increasingly common (Azure, AWS Graviton, Apple Silicon dev). Don't assume amd64!
- uses: docker/build-push-action@v5
with:
platforms: linux/arm64 # MATCH YOUR CLUSTER!
provenance: false # Avoid manifest issues
no-cache: true # When debugging
Why provenance: false?
Buildx attestation creates complex manifest lists that can cause "no match for platform" errors. Disable for simple, reliable images.
Problem: request.url in K8s returns container bind address
Solution: Use NEXT_PUBLIC_APP_URL env var for redirects
// WRONG
const response = NextResponse.redirect(new URL("/", request.url));
// RIGHT
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
const response = NextResponse.redirect(new URL("/", APP_URL));
Problem: Missing NEXT_PUBLIC_CONTINUE_URL in SSO Dockerfile
Solution: Add to Dockerfile and CD pipeline:
ARG NEXT_PUBLIC_CONTINUE_URL=http://localhost:3000
ENV NEXT_PUBLIC_CONTINUE_URL=$NEXT_PUBLIC_CONTINUE_URL
Problem: NEXT_PUBLIC_* not passed as build arg
Solution: Check ALL NEXT_PUBLIC_* variables systematically:
grep -r "NEXT_PUBLIC_" apps/web/src --include="*.ts" --include="*.tsx" | \
grep -oE "NEXT_PUBLIC_[A-Z_]+" | sort -u
Problem: Email/passwords hardcoded in values files
Solution: Use --set from GitHub Secrets for ALL sensitive data
Problem: Helm templates expect database.host, postgresql.name etc.
Solution: Include empty/default sections even for managed services:
postgresql:
enabled: false
name: sso-platform-postgres
database:
host: ""
port: "5432"
name: taskflow_sso
user: postgres
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your-email@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.tls: "true"
NEXT_PUBLIC_* vars documented and in Dockerfilesrequest.urlNEXT_PUBLIC_* as ARG and ENVaks-deployment-troubleshooter - Debug ImagePullBackOff, CrashLoopBackOff, architecture issuescontainerize-apps - Dockerization patternshelm-charts - Helm chart structurekubernetes-essentials - K8s fundamentalsbetter-auth-sso - SSO integrationimpact-analyzer-agent - Pre-containerization analysisSearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer