Kubernetes

Azure (AKS)

From an empty subscription to an enrolled Enterprise Server on Azure Kubernetes Service: the cluster and its registry, Azure Database for PostgreSQL, Blob storage, and HTTPS through the application routing add-on's Gateway API, with certificates from Let's Encrypt.

Before you begin

  • Azure CLI 2.86 or later (az upgrade), signed in with az login to a subscription where you can create resources, plus kubectl (az aks install-cli) and helm.
  • A hostname and access to its DNS (Azure DNS or anywhere else).
  • A project in the Knwlge app whose Enterprise Server URL is https:// and your host, and an enrollment key for it. Create the project from My Projects → New project, in your organization: you become its admin, and its first key is shown once.
  • A folder with the release and the Dockerfile: the Linux file from the Downloads page and the Dockerfile saved next to it. The commands below run in that folder.

Choose names

Every command below uses these. Registry, database and storage names are global across Azure, so make them your own:

shell
RG=knwlge
LOCATION=eastus
AKS=knwlge-aks
ACR=acmeknwlge            # 5–50 letters and digits
PG=acme-knwlge-pg
STORAGE=acmeknwlgestore   # 3–24 lowercase letters and digits
HOST=knwlge.acme.example
VERSION=1.2.0

Cluster and registry

shell
az group create --name $RG --location $LOCATION
az acr create --resource-group $RG --name $ACR --sku Basic

az aks create --resource-group $RG --name $AKS --location $LOCATION \
  --node-count 2 --node-vm-size Standard_D4s_v5 \
  --attach-acr $ACR \
  --enable-gateway-api --enable-app-routing-istio \
  --generate-ssh-keys

az aks get-credentials --resource-group $RG --name $AKS

On a cluster you already have:

shell
az aks update --resource-group $RG --name $AKS --attach-acr $ACR
az aks update --resource-group $RG --name $AKS --enable-gateway-api --enable-app-routing-istio

--enable-app-routing-istio is the application routing add-on's Gateway API implementation, which Microsoft recommends now that its managed NGINX is supported only until November 2026. Check that it is there:

shell
kubectl get gatewayclass approuting-istio

Build the image

In the folder with the tarball and the Dockerfile, the registry builds the image itself — no Docker needed on your machine:

shell
az acr build --registry $ACR --platform linux/amd64 \
  --build-arg VERSION=$VERSION --image knwlge-enterprise:$VERSION .

The image is $ACR.azurecr.io/knwlge-enterprise:$VERSION; the cluster can pull it because the registry is attached.

PostgreSQL

A Flexible Server on PostgreSQL 18 with pgvector allowed, and a firewall rule that lets the cluster's outbound address in. Azure wants three kinds of character in the password; this one is also safe in a URL:

shell
PG_PASSWORD="Kw$(openssl rand -hex 20)"

az postgres flexible-server create --resource-group $RG --name $PG --location $LOCATION \
  --version 18 --tier GeneralPurpose --sku-name Standard_D2ds_v5 --storage-size 64 \
  --admin-user knwlgeadmin --admin-password "$PG_PASSWORD" \
  --public-access None --backup-retention 14

# pgvector must be allowed before the migrations can create it
az postgres flexible-server parameter set --resource-group $RG --server-name $PG \
  --name azure.extensions --value VECTOR
az postgres flexible-server db create --resource-group $RG --server-name $PG --database-name knwlge

# Let the cluster in: its outbound public address
EGRESS_IP=$(az network public-ip show --query ipAddress -o tsv --ids \
  "$(az aks show --resource-group $RG --name $AKS \
     --query 'networkProfile.loadBalancerProfile.effectiveOutboundIPs[0].id' -o tsv)")
az postgres flexible-server firewall-rule create --resource-group $RG --name $PG \
  --rule-name aks-egress --start-ip-address $EGRESS_IP --end-ip-address $EGRESS_IP

echo "postgres://knwlgeadmin:$PG_PASSWORD@$PG.postgres.database.azure.com:5432/knwlge?sslmode=verify-full"

Keep the last line: it is the wizard's database answer. Azure's certificates are ones Node.js already trusts, so verify-full works as it is.

Blob storage

shell
az storage account create --resource-group $RG --name $STORAGE --location $LOCATION \
  --sku Standard_ZRS --kind StorageV2 --min-tls-version TLS1_2 --allow-blob-public-access false
STORAGE_CONNECTION=$(az storage account show-connection-string --resource-group $RG --name $STORAGE \
  --query connectionString -o tsv)
az storage container create --name knwlge-artifacts --connection-string "$STORAGE_CONNECTION"
echo "$STORAGE_CONNECTION"

The connection string and the container name are the wizard's storage answer. Where your region has no zone-redundant storage, use Standard_LRS.

Run the setup wizard

The server keeps its configuration on its volume, so the wizard runs once in a pod that mounts that volume. Save the two manifests below as knwlge-home.yaml and knwlge-setup.yaml, put your image in the second, and apply them:

knwlge-home.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: knwlge
---
# The server's home: config.json (written by the setup wizard), state.json and logs.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: knwlge-home
  namespace: knwlge
spec:
  accessModes: ["ReadWriteOnce"]
  storageClassName: managed-csi
  resources:
    requests:
      storage: 10Gi
knwlge-setup.yaml
apiVersion: v1
kind: Pod
metadata:
  name: knwlge-setup
  namespace: knwlge
spec:
  nodeSelector:
    kubernetes.io/arch: amd64
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    runAsGroup: 10001
    fsGroup: 10001
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: setup
      image: <acr-name>.azurecr.io/knwlge-enterprise:1.2.0
      command: ["sleep", "infinity"]
      stdin: true
      tty: true
      securityContext:
        allowPrivilegeEscalation: false
        capabilities:
          drop: ["ALL"]
      volumeMounts:
        - name: home
          mountPath: /var/lib/knwlge-enterprise
  volumes:
    - name: home
      persistentVolumeClaim:
        claimName: knwlge-home
shell
kubectl apply -f knwlge-home.yaml -f knwlge-setup.yaml
shell
kubectl -n knwlge wait --for=condition=Ready pod/knwlge-setup --timeout=5m
kubectl -n knwlge exec -it knwlge-setup -- knwlge-enterprise setup
The wizard asksAnswer
Enrollment keyThe key from the project page. Check the server URL it shows is https:// and your host.
DatabaseThe postgres://knwlgeadmin:…?sslmode=verify-full URL from PostgreSQL
StorageAzure Blob: the connection string, container knwlge-artifacts
Clients and sign-inAs for any server; a server admin can change both later in the console.
Start the server now?No: the Deployment starts it.

The wizard writes /var/lib/knwlge-enterprise/config.json on the volume (readable by the server's user only) and runs the migrations. Then remove the setup pod; the volume and what is on it stay:

shell
kubectl -n knwlge delete pod knwlge-setup

Start the server

Save this as knwlge-server.yaml, put your image in it, and apply it. One replica, replaced rather than rolled: the server runs its own scheduled jobs and its volume can be mounted by one pod at a time.

knwlge-server.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: knwlge-enterprise
  namespace: knwlge
  labels:
    app: knwlge-enterprise
spec:
  # One server per project: it runs its own scheduled jobs, so never more than one replica.
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: knwlge-enterprise
  template:
    metadata:
      labels:
        app: knwlge-enterprise
    spec:
      nodeSelector:
        kubernetes.io/arch: amd64
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        fsGroup: 10001
        seccompProfile:
          type: RuntimeDefault
      terminationGracePeriodSeconds: 60
      containers:
        - name: server
          image: <acr-name>.azurecr.io/knwlge-enterprise:1.2.0
          args: ["start"]
          ports:
            - name: http
              containerPort: 3000
          # Migrations run before the server listens: give a first start a few minutes.
          startupProbe:
            httpGet:
              path: /healthz
              port: http
            periodSeconds: 5
            failureThreshold: 60
          readinessProbe:
            httpGet:
              path: /readyz
              port: http
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /healthz
              port: http
            periodSeconds: 20
          resources:
            requests:
              cpu: 500m
              memory: 1Gi
            limits:
              memory: 2Gi
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: home
              mountPath: /var/lib/knwlge-enterprise
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: home
          persistentVolumeClaim:
            claimName: knwlge-home
        - name: tmp
          emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: knwlge-enterprise
  namespace: knwlge
spec:
  selector:
    app: knwlge-enterprise
  ports:
    - name: http
      port: 80
      targetPort: http
shell
kubectl apply -f knwlge-server.yaml
kubectl -n knwlge rollout status deploy/knwlge-enterprise --timeout=10m
kubectl -n knwlge logs deploy/knwlge-enterprise --tail=20

The first start enrolls the server with Knwlge Global; the log says global.enrolled and then that api-mcp and the worker are listening. Later starts run any new migrations and carry on.

HTTPS and DNS

cert-manager gets a certificate from Let's Encrypt for the Gateway's HTTPS listener and renews it. Install it with its Gateway API support on:

shell
helm repo add jetstack https://charts.jetstack.io --force-update
helm upgrade --install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --set crds.enabled=true \
  --set config.apiVersion=controller.config.cert-manager.io/v1alpha1 \
  --set config.kind=ControllerConfiguration \
  --set config.enableGatewayAPI=true

Then the issuer, the Gateway and the routes — with your host, and your email for Let's Encrypt's notices — saved as knwlge-gateway.yaml:

knwlge-gateway.yaml
# Let's Encrypt, answering its HTTP challenge through the Gateway below.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: platform@acme.example
    privateKeySecretRef:
      name: letsencrypt-account
    solvers:
      - http01:
          gatewayHTTPRoute:
            parentRefs:
              - kind: Gateway
                name: knwlge
                namespace: knwlge
                sectionName: http
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: knwlge
  namespace: knwlge
  annotations:
    # cert-manager requests the certificate for the HTTPS listener and keeps it renewed.
    cert-manager.io/cluster-issuer: letsencrypt
spec:
  gatewayClassName: approuting-istio
  listeners:
    - name: http
      port: 80
      protocol: HTTP
      hostname: knwlge.acme.example
      allowedRoutes:
        namespaces:
          from: Same
    - name: https
      port: 443
      protocol: HTTPS
      hostname: knwlge.acme.example
      tls:
        mode: Terminate
        certificateRefs:
          - name: knwlge-tls
      allowedRoutes:
        namespaces:
          from: Same
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: knwlge
  namespace: knwlge
spec:
  parentRefs:
    - name: knwlge
      sectionName: https
  hostnames:
    - knwlge.acme.example
  rules:
    - backendRefs:
        - name: knwlge-enterprise
          port: 80
---
# Plain HTTP is sent to HTTPS (the certificate challenge, a more specific route, still answers).
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: knwlge-https-redirect
  namespace: knwlge
spec:
  parentRefs:
    - name: knwlge
      sectionName: http
  hostnames:
    - knwlge.acme.example
  rules:
    - filters:
        - type: RequestRedirect
          requestRedirect:
            scheme: https
            statusCode: 301
shell
kubectl apply -f knwlge-gateway.yaml
kubectl -n knwlge wait --for=condition=Programmed gateway/knwlge --timeout=10m
kubectl -n knwlge get gateway knwlge -o jsonpath='{.status.addresses[0].value}'

Point an A record for your host at that address. With Azure DNS:

shell
az network dns record-set a add-record --resource-group <dns-zone-rg> \
  --zone-name acme.example --record-set-name knwlge --ipv4-address <gateway-address>

Once the name resolves, the certificate follows within a few minutes; kubectl -n knwlge get certificate shows READY True. With your organization's own certificate instead, create the secret it names (kubectl -n knwlge create secret tls knwlge-tls --cert=fullchain.pem --key=privkey.pem) and leave out the issuer and the Gateway's annotation.

Verify

shell
curl -fsS https://$HOST/healthz
kubectl -n knwlge exec deploy/knwlge-enterprise -- knwlge-enterprise status
kubectl — knwlge
$ kubectl -n knwlge exec deploy/knwlge-enterprise -- knwlge-enterprise status
Knwlge Enterprise Server 1.2.0
  Config       /var/lib/knwlge-enterprise/config.json
  Server URL   https://knwlge.acme.example  (api :3000, worker :3002)
  Global       https://api.knwlge.com  ·  project Acme Platform  ·  Acme
  Process      running (pid 11, since 2026-10-01T16:26:15.878Z)
  Enrollment   enrolled
  Database     ok
  Storage chk  ok
  Readiness    ready
Sample values. The command exits with 1 when the server is not ready, so it also works as a check.
  • Open https://$HOST and sign in with Knwlge: that is the server's web console.
  • The project's page in the Knwlge app shows the server as enrolled, its version and its health within a couple of minutes.
  • Connect repositories in the console (Repositories), then send developers to CLI installation with the server's URL.
  • Upgrades, logs, backups and what to do when something is off are in the Kubernetes overview.