Kubernetes
AWS (EKS)
From an AWS account to an enrolled Enterprise Server on Amazon EKS: an Auto Mode cluster, ECR, RDS for PostgreSQL, S3, and an Application Load Balancer with an ACM certificate in front.
Before you begin
- The AWS CLI (version 2) signed in to the account,
eksctl,kubectland Docker. - A hostname and access to its DNS (Route 53 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
AWS_REGION=us-east-1
CLUSTER=knwlge
HOST=knwlge.acme.example
VERSION=1.2.0
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGISTRY=$ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
BUCKET=knwlge-artifacts-$ACCOUNT_ID
Cluster
EKS Auto Mode brings compute, EBS volumes and load balancing with it; nothing else to install:
eksctl create cluster --name $CLUSTER --region $AWS_REGION --enable-auto-mode
Auto Mode ships no storage class or ingress class of its own. Save these as knwlge-cluster.yaml and apply them:
# EKS Auto Mode provisions EBS volumes through its own driver, but ships no StorageClass.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: knwlge-gp3
provisioner: ebs.csi.eks.amazonaws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
type: gp3
encrypted: "true"
---
# ...and asks for an internet-facing Application Load Balancer through this class.
apiVersion: eks.amazonaws.com/v1
kind: IngressClassParams
metadata:
name: knwlge-alb
spec:
scheme: internet-facing
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: knwlge-alb
spec:
controller: eks.amazonaws.com/alb
parameters:
apiGroup: eks.amazonaws.com
kind: IngressClassParams
name: knwlge-alb
kubectl apply -f knwlge-cluster.yaml
Push the image to ECR
aws ecr create-repository --repository-name knwlge-enterprise --region $AWS_REGION \
--image-scanning-configuration scanOnPush=true
aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $REGISTRY
docker build --platform linux/amd64 --build-arg VERSION=$VERSION -t $REGISTRY/knwlge-enterprise:$VERSION .
docker push $REGISTRY/knwlge-enterprise:$VERSION
The cluster's nodes pull from ECR in their own account without anything more.
RDS for PostgreSQL
The database goes in the cluster's VPC, open only to the cluster's security group, on the newest PostgreSQL 18:
VPC_ID=$(aws eks describe-cluster --name $CLUSTER --query cluster.resourcesVpcConfig.vpcId --output text)
SUBNETS=$(aws eks describe-cluster --name $CLUSTER --query 'cluster.resourcesVpcConfig.subnetIds' --output text)
CLUSTER_SG=$(aws eks describe-cluster --name $CLUSTER \
--query cluster.resourcesVpcConfig.clusterSecurityGroupId --output text)
aws rds create-db-subnet-group --db-subnet-group-name knwlge \
--db-subnet-group-description "Knwlge Enterprise Server" --subnet-ids $SUBNETS
DB_SG=$(aws ec2 create-security-group --group-name knwlge-db --description "Knwlge database" \
--vpc-id $VPC_ID --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id $DB_SG --protocol tcp --port 5432 --source-group $CLUSTER_SG
PG_VERSION=$(aws rds describe-db-engine-versions --engine postgres \
--query "DBEngineVersions[?starts_with(EngineVersion, '18.')].EngineVersion | [-1]" --output text)
PG_PASSWORD=$(openssl rand -hex 24)
aws rds create-db-instance --db-instance-identifier knwlge-pg \
--engine postgres --engine-version $PG_VERSION \
--db-instance-class db.t4g.medium --allocated-storage 50 --storage-type gp3 --storage-encrypted \
--master-username knwlgeadmin --master-user-password "$PG_PASSWORD" --db-name knwlge \
--db-subnet-group-name knwlge --vpc-security-group-ids $DB_SG --no-publicly-accessible \
--backup-retention-period 7
aws rds wait db-instance-available --db-instance-identifier knwlge-pg
DB_HOST=$(aws rds describe-db-instances --db-instance-identifier knwlge-pg \
--query 'DBInstances[0].Endpoint.Address' --output text)
echo "postgres://knwlgeadmin:$PG_PASSWORD@$DB_HOST:5432/knwlge?sslmode=verify-full"
Keep the last line: it is the wizard's database answer. RDS requires TLS, and its certificates come from Amazon's own authority, which Node.js does not trust by default: the pods mount Amazon's bundle and point NODE_EXTRA_CA_CERTS at it (in the manifests below). Size the instance for production with db.m7g.large and --multi-az.
S3
A private bucket, and an access key that can use only that bucket — the server's S3 storage takes a key, not an IAM role:
if [ "$AWS_REGION" = us-east-1 ]; then
aws s3api create-bucket --bucket $BUCKET
else
aws s3api create-bucket --bucket $BUCKET --create-bucket-configuration LocationConstraint=$AWS_REGION
fi
cat > knwlge-s3.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::$BUCKET" },
{ "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::$BUCKET/*" }
]
}
EOF
aws iam create-user --user-name knwlge-enterprise-storage
aws iam put-user-policy --user-name knwlge-enterprise-storage --policy-name knwlge-artifacts \
--policy-document file://knwlge-s3.json
aws iam create-access-key --user-name knwlge-enterprise-storage
If your organization allows no IAM users, keep artifacts on the server's volume instead: answer Local filesystem in the wizard and make the volume 50Gi.
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:
Between the two, put Amazon's certificate bundle in the namespace, where both pods mount it.
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: knwlge-gp3
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: <account-id>.dkr.ecr.<region>.amazonaws.com/knwlge-enterprise:1.2.0
command: ["sleep", "infinity"]
stdin: true
tty: true
env:
- name: NODE_EXTRA_CA_CERTS
value: "/etc/knwlge/rds/global-bundle.pem"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: home
mountPath: /var/lib/knwlge-enterprise
- name: rds-ca
mountPath: /etc/knwlge/rds
readOnly: true
volumes:
- name: home
persistentVolumeClaim:
claimName: knwlge-home
- name: rds-ca
configMap:
name: rds-ca
kubectl apply -f knwlge-home.yaml
curl -fsSLO https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
kubectl -n knwlge create configmap rds-ca --from-file=global-bundle.pem
kubectl apply -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 RDS for PostgreSQL |
| Storage | S3-compatible: endpoint https://s3.$AWS_REGION.amazonaws.com, the bucket, the region, and the access key and secret from S3 |
| 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: <account-id>.dkr.ecr.<region>.amazonaws.com/knwlge-enterprise:1.2.0
args: ["start"]
env:
- name: NODE_EXTRA_CA_CERTS
value: "/etc/knwlge/rds/global-bundle.pem"
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
- name: rds-ca
mountPath: /etc/knwlge/rds
readOnly: true
volumes:
- name: home
persistentVolumeClaim:
claimName: knwlge-home
- name: tmp
emptyDir: {}
- name: rds-ca
configMap:
name: rds-ca
---
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 certificate from ACM, validated through DNS:
CERT_ARN=$(aws acm request-certificate --domain-name $HOST --validation-method DNS \
--region $AWS_REGION --query CertificateArn --output text)
aws acm describe-certificate --certificate-arn $CERT_ARN --region $AWS_REGION \
--query 'Certificate.DomainValidationOptions[0].ResourceRecord'
Add that CNAME record at your DNS provider (a few seconds after the request, if the query comes back empty), then wait for ACM:
aws acm wait certificate-validated --certificate-arn $CERT_ARN --region $AWS_REGION
The Ingress asks Auto Mode for the load balancer. Put your host and the certificate's ARN in it, save it as knwlge-ingress.yaml, and apply it:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: knwlge
namespace: knwlge
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:<region>:<account-id>:certificate/<id>
alb.ingress.kubernetes.io/healthcheck-path: /healthz
spec:
ingressClassName: knwlge-alb
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 get ingress knwlge -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
The load balancer takes a few minutes to appear. Point your host at its name: a CNAME, or in Route 53 an alias A record.
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.
