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 withaz loginto a subscription where you can create resources, pluskubectl(az aks install-cli) andhelm. - 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:
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
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:
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:
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:
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:
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
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:
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
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
kubectl apply -f knwlge-home.yaml -f knwlge-setup.yaml
kubectl -n knwlge wait --for=condition=Ready pod/knwlge-setup --timeout=5m
kubectl -n knwlge exec -it knwlge-setup -- knwlge-enterprise setup
| The wizard asks | Answer |
|---|---|
| Enrollment key | The key from the project page. Check the server URL it shows is https:// and your host. |
| Database | The postgres://knwlgeadmin:…?sslmode=verify-full URL from PostgreSQL |
| Storage | Azure Blob: the connection string, container knwlge-artifacts |
| Clients and sign-in | As 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:
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.
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
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:
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:
# 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
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:
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
curl -fsS https://$HOST/healthz
kubectl -n knwlge exec deploy/knwlge-enterprise -- knwlge-enterprise status
$ 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
- Open
https://$HOSTand 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.
