Kubernetes

Google Cloud (GKE)

From a Google Cloud project to an enrolled Enterprise Server on GKE: an Autopilot cluster, Artifact Registry, Cloud SQL for PostgreSQL reached through the Cloud SQL Auth Proxy, and HTTPS with a Google-managed certificate.

Before you begin

  • The gcloud CLI signed in to the project (gcloud auth login), with kubectl and its GKE plugin (gcloud components install kubectl gke-gcloud-auth-plugin), and Docker.
  • A hostname and access to its DNS (Cloud 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

shell
PROJECT_ID=$(gcloud config get-value project)
REGION=us-central1
CLUSTER=knwlge
HOST=knwlge.acme.example
VERSION=1.2.0
REGISTRY=$REGION-docker.pkg.dev/$PROJECT_ID/knwlge

Cluster and registry

shell
gcloud services enable container.googleapis.com artifactregistry.googleapis.com \
  sqladmin.googleapis.com compute.googleapis.com

gcloud container clusters create-auto $CLUSTER --region $REGION
gcloud container clusters get-credentials $CLUSTER --region $REGION

gcloud artifacts repositories create knwlge --repository-format=docker --location=$REGION
# The nodes pull as the Compute Engine default service account
gcloud artifacts repositories add-iam-policy-binding knwlge --location=$REGION \
  --member="serviceAccount:$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')-compute@developer.gserviceaccount.com" \
  --role=roles/artifactregistry.reader

Autopilot clusters have Workload Identity on, which the Auth Proxy uses below. Use your own cluster if you have one.

Build and push the image

shell
gcloud auth configure-docker $REGION-docker.pkg.dev
docker build --platform linux/amd64 --build-arg VERSION=$VERSION -t $REGISTRY/knwlge-enterprise:$VERSION .
docker push $REGISTRY/knwlge-enterprise:$VERSION

Cloud SQL for PostgreSQL

shell
gcloud sql instances create knwlge-pg --database-version=POSTGRES_18 --edition=ENTERPRISE \
  --tier=db-custom-2-7680 --region=$REGION --storage-size=50 --storage-auto-increase \
  --backup-start-time=03:00 --enable-point-in-time-recovery
PG_PASSWORD=$(openssl rand -hex 24)
gcloud sql users set-password postgres --instance=knwlge-pg --password="$PG_PASSWORD"
gcloud sql databases create knwlge --instance=knwlge-pg
gcloud sql instances describe knwlge-pg --format='value(connectionName)'   # <project-id>:<region>:knwlge-pg

The server reaches the instance through the Cloud SQL Auth Proxy, which runs beside it in the pod and authenticates as a Google service account through Workload Identity — no database firewall rules, and the connection is encrypted by the proxy:

shell
gcloud iam service-accounts create knwlge-sql --display-name="Knwlge Enterprise Server (Cloud SQL)"
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:knwlge-sql@$PROJECT_ID.iam.gserviceaccount.com" --role=roles/cloudsql.client
gcloud iam service-accounts add-iam-policy-binding knwlge-sql@$PROJECT_ID.iam.gserviceaccount.com \
  --role=roles/iam.workloadIdentityUser \
  --member="serviceAccount:$PROJECT_ID.svc.id.goog[knwlge/knwlge-enterprise]"
echo "postgres://postgres:$PG_PASSWORD@127.0.0.1:5432/knwlge?sslmode=disable"

Keep the last line: it is the wizard's database answer — the proxy listens on the pod's own loopback address.

Storage

Knwlge has no Cloud Storage provider of its own, so artifacts stay on the server's persistent disk: the volume below is 50Gi, and in the wizard the answer is Local filesystem with the directory it offers, /var/lib/knwlge-enterprise/artifacts. Snapshot the disk with the rest of your backups. (Cloud Storage's S3-compatible API with HMAC keys may work through the S3 option — the wizard's write, read and delete probe would say so — but it is not a path Knwlge tests.)

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:

The service account comes first: the Auth Proxy beside the wizard needs it. Put your project in it and in the proxy's instance name.

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: standard-rwo
  resources:
    requests:
      storage: 50Gi
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: knwlge-enterprise
  namespace: knwlge
  annotations:
    iam.gke.io/gcp-service-account: knwlge-sql@<project-id>.iam.gserviceaccount.com
knwlge-setup.yaml
apiVersion: v1
kind: Pod
metadata:
  name: knwlge-setup
  namespace: knwlge
spec:
  serviceAccountName: knwlge-enterprise
  nodeSelector:
    kubernetes.io/arch: amd64
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    runAsGroup: 10001
    fsGroup: 10001
    seccompProfile:
      type: RuntimeDefault
  initContainers:
    - name: cloud-sql-proxy
      image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.26.0
      restartPolicy: Always
      args:
        - "--port=5432"
        - "--health-check"
        - "--http-address=0.0.0.0"
        - "--http-port=9090"
        - "--exit-zero-on-sigterm"
        - "<project-id>:<region>:knwlge-pg"
      startupProbe:
        httpGet:
          path: /startup
          port: 9090
        periodSeconds: 2
        failureThreshold: 30
      resources:
        requests:
          cpu: 100m
          memory: 128Mi
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
  containers:
    - name: setup
      image: <region>-docker.pkg.dev/<project-id>/knwlge/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.
Databasepostgres://postgres:…@127.0.0.1:5432/knwlge?sslmode=disable, from Cloud SQL
StorageLocal filesystem: /var/lib/knwlge-enterprise/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

The proxy is gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.26.0, started before the server and checked ready first, so the server's first connection never races it.

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:
      serviceAccountName: knwlge-enterprise
      nodeSelector:
        kubernetes.io/arch: amd64
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        fsGroup: 10001
        seccompProfile:
          type: RuntimeDefault
      initContainers:
        - name: cloud-sql-proxy
          image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.26.0
          restartPolicy: Always
          args:
            - "--port=5432"
            - "--health-check"
            - "--http-address=0.0.0.0"
            - "--http-port=9090"
            - "--exit-zero-on-sigterm"
            - "<project-id>:<region>:knwlge-pg"
          startupProbe:
            httpGet:
              path: /startup
              port: 9090
            periodSeconds: 2
            failureThreshold: 30
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
      terminationGracePeriodSeconds: 60
      containers:
        - name: server
          image: <region>-docker.pkg.dev/<project-id>/knwlge/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

A global address for the load balancer, then point your host at it with an A record:

shell
gcloud compute addresses create knwlge-ip --global
gcloud compute addresses describe knwlge-ip --global --format='value(address)'

Then the certificate, the redirect to HTTPS and the Ingress, with your host, saved as knwlge-ingress.yaml:

knwlge-ingress.yaml
apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
  name: knwlge
  namespace: knwlge
spec:
  domains:
    - knwlge.acme.example
---
apiVersion: networking.gke.io/v1beta1
kind: FrontendConfig
metadata:
  name: knwlge
  namespace: knwlge
spec:
  redirectToHttps:
    enabled: true
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: knwlge
  namespace: knwlge
  annotations:
    kubernetes.io/ingress.class: gce
    kubernetes.io/ingress.global-static-ip-name: knwlge-ip
    networking.gke.io/managed-certificates: knwlge
    networking.gke.io/v1beta1.FrontendConfig: knwlge
spec:
  rules:
    - host: knwlge.acme.example
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: knwlge-enterprise
                port:
                  name: http
shell
kubectl apply -f knwlge-ingress.yaml
kubectl -n knwlge describe managedcertificate knwlge

The load balancer checks the server through its readiness probe. Google issues the certificate once the name resolves to the address, which can take up to an hour; the certificate's status turns Active.

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.