This guide covers three deployment methods: local development, Docker Hub images, and Kubernetes. Each bundles CaSS with Elasticsearch 9.x.
See also CONFIGURATION.md for all configuration options and FILE.md for project structure.
docker run -d --name elasticsearch \
-p 9200:9200 \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
-e "xpack.security.enrollment.enabled=false" \
-e "xpack.security.http.ssl.enabled=false" \
-e "xpack.security.transport.ssl.enabled=false" \
-e "ES_JAVA_OPTS=-Xms2g -Xmx2g" \
docker.elastic.co/elasticsearch/elasticsearch:9.4.2
git clone https://github.com/cassproject/CASS.git
cd CASS
npm install
npm run dev
CaSS is available at http://localhost/api/.
Set environment variables inline or via .env:
ELASTICSEARCH_ENDPOINT=http://localhost:9200 \
PORT=80 \
npm run dev
Pre-built images are available at cassproject/cass.
docker compose up -d
This uses the root docker-compose.yml which starts:
| Compose File | Base | Use Case |
|---|---|---|
docker-compose.yml |
Debian node:24-slim |
Default — FIPS-enabled |
docker-compose-alpine.yml |
Alpine node:24-alpine |
Smaller image (~40% less) |
docker-compose-distroless.yml |
gcr.io/distroless/nodejs24 |
Hardened — no shell, no package manager |
docker-compose-opensearch.yml |
Debian node:24-slim |
OpenSearch instead of Elasticsearch |
# Alpine variant
docker compose -f docker-compose-alpine.yml up -d
# Distroless variant
docker compose -f docker-compose-distroless.yml up -d
Override environment variables in the compose file or via a .env file:
# .env
CASS_OIDC_ENABLED=true
CASS_OIDC_ISSUER_BASE_URL=https://keycloak.example.com/auth/realms/master/
CASS_OIDC_CLIENT_ID=cass
CASS_OIDC_SECRET=your-secret
The compose files define two volumes:
| Volume | Purpose |
|---|---|
etc |
CaSS server keys and adapter config (mounted at /app/etc) |
esdata1 |
Elasticsearch indices and data |
[!CAUTION] Back up both volumes before destroying containers. Losing
etcmeans losing all server keys. Losingesdata1means losing all stored data.
Mount your own certificates:
services:
cassl:
volumes:
- ./certs/server.key:/app/cass.key:ro
- ./certs/server.crt:/app/cass.crt:ro
- ./certs/ca.crt:/app/ca.crt:ro
kubectl configuredetc/)kubectl create namespace cass
# elasticsearch.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: cass
spec:
serviceName: elasticsearch
replicas: 1
selector:
matchLabels:
app: elasticsearch
template:
metadata:
labels:
app: elasticsearch
spec:
containers:
- name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:9.4.2
ports:
- containerPort: 9200
env:
- name: discovery.type
value: single-node
- name: xpack.security.enabled
value: "false"
- name: xpack.security.enrollment.enabled
value: "false"
- name: xpack.security.http.ssl.enabled
value: "false"
- name: xpack.security.transport.ssl.enabled
value: "false"
- name: ES_JAVA_OPTS
value: "-Xms2g -Xmx2g"
- name: bootstrap.memory_lock
value: "true"
resources:
requests:
memory: "3Gi"
cpu: "1"
limits:
memory: "4Gi"
cpu: "2"
volumeMounts:
- name: es-data
mountPath: /usr/share/elasticsearch/data
volumeClaimTemplates:
- metadata:
name: es-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 50Gi
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: cass
spec:
selector:
app: elasticsearch
ports:
- port: 9200
targetPort: 9200
clusterIP: None
# cass.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: cass-config
namespace: cass
data:
CASS_LOOPBACK: "http://cass:80/api/"
ELASTICSEARCH_ENDPOINT: "http://elasticsearch:9200"
PORT: "80"
# Add OIDC, banner, or other config here.
# See CONFIGURATION.md for all options.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cass
namespace: cass
spec:
replicas: 1
selector:
matchLabels:
app: cass
template:
metadata:
labels:
app: cass
spec:
containers:
- name: cass
image: cassproject/cass:latest
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: cass-config
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "2Gi"
cpu: "2"
volumeMounts:
- name: cass-etc
mountPath: /app/etc
readinessProbe:
httpGet:
path: /api/ping
port: 80
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /api/ping
port: 80
initialDelaySeconds: 30
periodSeconds: 30
volumes:
- name: cass-etc
persistentVolumeClaim:
claimName: cass-etc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cass-etc
namespace: cass
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
name: cass
namespace: cass
spec:
selector:
app: cass
ports:
- port: 80
targetPort: 80
type: ClusterIP
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cass
namespace: cass
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
rules:
- host: cass.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: cass
port:
number: 80
tls:
- hosts:
- cass.example.com
secretName: cass-tls
kubectl apply -f elasticsearch.yaml
kubectl apply -f cass.yaml
kubectl apply -f ingress.yaml # optional
# Verify
kubectl -n cass get pods
kubectl -n cass logs deployment/cass
replicas: N), but each replica must share the same etc/ volume (use ReadWriteMany storage or a shared PVC)./api/ping endpoint is used for both readiness and liveness probes.For sensitive values (OIDC secrets, SMTP passwords), use Kubernetes Secrets instead of ConfigMaps:
apiVersion: v1
kind: Secret
metadata:
name: cass-secrets
namespace: cass
type: Opaque
stringData:
CASS_OIDC_SECRET: "your-client-secret"
SMTP_PASS: "your-smtp-password"
Reference in the deployment:
envFrom:
- configMapRef:
name: cass-config
- secretRef:
name: cass-secrets
All deployment methods can use the ping endpoint for health checking:
GET /api/ping
Returns 200 with server info including ping: "pong" and the current time. See CONFIGURATION.md for the full response schema.