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), withkubectland 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
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
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
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
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:
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.
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
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
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 | postgres://postgres:…@127.0.0.1:5432/knwlge?sslmode=disable, from Cloud SQL |
| Storage | Local filesystem: /var/lib/knwlge-enterprise/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
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.
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
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:
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:
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
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
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.
