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, kubectl and 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

shell
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:

shell
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:

knwlge-cluster.yaml
# 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
shell
kubectl apply -f knwlge-cluster.yaml

Push the image to ECR

shell
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:

shell
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:

shell
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.

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: knwlge-gp3
  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: <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
shell
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
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 RDS for PostgreSQL
StorageS3-compatible: endpoint https://s3.$AWS_REGION.amazonaws.com, the bucket, the region, and the access key and secret from S3
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: <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
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 certificate from ACM, validated through DNS:

shell
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:

shell
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:

knwlge-ingress.yaml
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
shell
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

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.