Skip to main content

Customer Portal — Deployment Topology

Status: populated Owner: Platform Engineering / DevOps Last updated: 2026-04-18

1. Overview​

The customer-portal is built as a Next.js standalone application and packaged as a Docker container. It is deployed as a stateless workload — no Kubernetes service mesh, no sidecar proxies, no persistent volumes.

2. Build Pipeline​

Source (Git)
└─► GitHub Actions CI
├─ pnpm install --frozen-lockfile
├─ pnpm build # next build → .next/standalone/
├─ pnpm lint + typecheck
├─ Docker build (multi-stage)
└─ Push to Container Registry (GCP Artifact Registry / ECR)

Dockerfile (multi-stage)​

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

The output: 'standalone' option in next.config.ts produces a self-contained server bundle — no node_modules copy needed in the runner stage.

3. Kubernetes Deployment​

# customer-portal Deployment (simplified)
replicas: 2
image: ghcr.io/ghasitech/customer-portal:${TAG}
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 512Mi }
env:
- name: API_BASE_URL # Kong internal ClusterIP
value: "http://kong-proxy.kong.svc.cluster.local"
- name: NEXT_PUBLIC_API_BASE_URL
value: "https://api.ghasi.io"
- name: FIREBASE_PROJECT_ID
valueFrom: { secretKeyRef: { name: firebase-config, key: project_id } }
- name: SESSION_SECRET
valueFrom: { secretKeyRef: { name: customer-portal-secrets, key: session_secret } }
livenessProbe: { httpGet: { path: /api/health, port: 3000 }, initialDelaySeconds: 10 }
readinessProbe: { httpGet: { path: /api/health, port: 3000 }, initialDelaySeconds: 5 }

4. Service & Ingress​

# Kubernetes Service
type: ClusterIP
port: 80 → targetPort: 3000

# Ingress (Nginx or Cloudflare Tunnel)
host: portal.ghasi.io
path: /
TLS: Cloudflare origin certificate

No Istio / Linkerd service mesh — the portal is a pure HTTP service behind Cloudflare and the cluster ingress.

5. Environment Variables​

VariableRequiredDescription
API_BASE_URLYesServer-side Kong base URL (internal cluster)
NEXT_PUBLIC_API_BASE_URLYesPublic Kong base URL (browser fetch fallback)
FIREBASE_PROJECT_IDYesFirebase project for client SDK init
NEXT_PUBLIC_FIREBASE_API_KEYYesFirebase web API key (public)
NEXT_PUBLIC_FIREBASE_AUTH_DOMAINYesFirebase auth domain
SESSION_SECRETYes32-byte hex string for iron-session encryption
SESSION_COOKIE_SECUREYestrue in prod
SENTRY_DSNNoSentry error reporting DSN
NEXT_PUBLIC_SENTRY_DSNNoSentry DSN for client-side errors

6. Scaling​

  • Horizontal: Stateless; scale replicas freely. Target: 2–6 pods.
  • Autoscaling: HPA on CPU > 60% or memory > 70%.
  • No sticky sessions: Cookies are self-contained (encrypted); any replica can handle any request.

7. CDN​

Static assets (/_next/static/) are served via Cloudflare CDN with immutable cache headers set by Next.js. The portal origin only handles dynamic SSR requests and API route handlers.

8. No Service Mesh​

The portal makes outbound HTTP calls only to Kong (internal ClusterIP). No east-west mTLS needed. Kong handles authentication and routing to backend services.