Skip to content

Kubernetes Installation

Deploy Mantis on Kubernetes for production-grade scalability and high availability.

RequirementVersion
Kubernetes1.28+
kubectl1.28+
Helm3.12+ (optional)

The following services must be available (managed or self-hosted):

ServicePurpose
PostgreSQLPrimary database
RabbitMQMessage queue
RedisSession and cache
Terminal window
# Create namespace
kubectl create namespace mantis
# Set as default context
kubectl config set-context --current --namespace=mantis
secrets/database-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mantis-database
namespace: mantis
type: Opaque
stringData:
url: 'postgres://mantis:secret@postgres.internal:5432/mantis?sslmode=verify-full'
secrets/jwt-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mantis-jwt
namespace: mantis
type: Opaque
stringData:
secret: 'your-256-bit-secret-key-at-least-32-characters'
secrets/encryption-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mantis-encryption
namespace: mantis
type: Opaque
stringData:
master-key: 'K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols='
secrets/tls-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mantis-tls
namespace: mantis
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded-certificate>
tls.key: <base64-encoded-private-key>
ca.crt: <base64-encoded-ca-certificate>

Create secrets:

Terminal window
kubectl apply -f secrets/
configmaps/mandible-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mandible-config
namespace: mantis
data:
MANDIBLE__SERVER__PORT: '3000'
MANDIBLE__JWT__ALGORITHM: 'HS256'
MANDIBLE__JWT__ACCESS_TOKEN_EXPIRY_SECS: '86400'
MANDIBLE__CORS__ALLOWED_ORIGINS: 'https://lens.example.com'
# Log filtering uses the standard tracing EnvFilter
RUST_LOG: 'info'
MANDIBLE__GRPC__ORCHESTRATION_URL: 'https://thorax-headless.mantis.svc.cluster.local:50051'
configmaps/thorax-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: thorax-config
namespace: mantis
data:
THORAX__GRPC__PORT: '50051'
MANTIS_TLS_AUTH_MODE: 'ca_signed'
THORAX__CLUSTER__REDIS_URL: 'redis://:password@redis.internal:6379'
MANTIS_LOG_LEVEL: 'info'
# Session timeouts (heartbeat_timeout_secs=90, stale_timeout_secs=300,
# cleanup_interval_secs=60) are FILE-ONLY — there are no THORAX__SESSION__* env
# vars. Mount a thorax.toml with a [session] block to override them.
# Queue (RabbitMQ) settings can be configured via env vars — the most
# commonly needed keys are shown below; omit any that are set in thorax.toml.
THORAX__QUEUE__HOST: 'rabbitmq.internal'
THORAX__QUEUE__PORT: '5671'
THORAX__QUEUE__VHOST: '/'
deployments/mandible.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mandible
namespace: mantis
labels:
app: mantis
component: mandible
spec:
replicas: 2
selector:
matchLabels:
app: mantis
component: mandible
template:
metadata:
labels:
app: mantis
component: mandible
spec:
containers:
- name: mandible
image: ghcr.io/mantisplatform/mantis-mandible:latest
ports:
- containerPort: 3000
name: http
envFrom:
- configMapRef:
name: mandible-config
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: mantis-database
key: url
- name: MANDIBLE__JWT__SECRET
valueFrom:
secretKeyRef:
name: mantis-jwt
key: secret
- name: MANTIS_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: mantis-encryption
key: master-key
volumeMounts:
- name: tls-certs
mountPath: /etc/mantis/certs
readOnly: true
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
livenessProbe:
httpGet:
path: /api/v1/health/live
port: http
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/v1/health/ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: tls-certs
secret:
secretName: mantis-tls
deployments/thorax.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: thorax
namespace: mantis
labels:
app: mantis
component: thorax
spec:
replicas: 2
selector:
matchLabels:
app: mantis
component: thorax
template:
metadata:
labels:
app: mantis
component: thorax
spec:
containers:
- name: thorax
image: ghcr.io/mantisplatform/mantis-thorax:latest
ports:
- containerPort: 50051
name: grpc
envFrom:
- configMapRef:
name: thorax-config
env:
# Thorax has NO database; it reaches Mandible over gRPC for all
# persistence. Point it at Mandible's internal gRPC service.
- name: THORAX__MANDIBLE__ENDPOINT
value: 'https://mandible:50052'
- name: MANTIS_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: mantis-encryption
key: master-key
- name: THORAX__CLUSTER__INSTANCE_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- name: tls-certs
mountPath: /etc/mantis/certs
readOnly: true
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 1Gi
livenessProbe:
grpc:
port: 50051
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
grpc:
port: 50051
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: tls-certs
secret:
secretName: mantis-tls
deployments/lens.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: lens
namespace: mantis
labels:
app: mantis
component: lens
spec:
replicas: 2
selector:
matchLabels:
app: mantis
component: lens
template:
metadata:
labels:
app: mantis
component: lens
spec:
containers:
- name: lens
image: ghcr.io/mantisplatform/mantis-lens:latest
ports:
- containerPort: 4173
name: http
env:
- name: VITE_API_URL
value: 'https://api.mantis.example.com'
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 3
periodSeconds: 5
services/mandible-service.yaml
apiVersion: v1
kind: Service
metadata:
name: mandible
namespace: mantis
spec:
selector:
app: mantis
component: mandible
ports:
- port: 3000
targetPort: http
name: http
type: ClusterIP
services/thorax-service.yaml
apiVersion: v1
kind: Service
metadata:
name: thorax
namespace: mantis
spec:
selector:
app: mantis
component: thorax
ports:
- port: 50051
targetPort: grpc
name: grpc
type: ClusterIP
---
# Headless service for gRPC load balancing
apiVersion: v1
kind: Service
metadata:
name: thorax-headless
namespace: mantis
spec:
selector:
app: mantis
component: thorax
ports:
- port: 50051
targetPort: grpc
name: grpc
clusterIP: None

Thorax External Service (for Tarsus agents)

Section titled “Thorax External Service (for Tarsus agents)”
services/thorax-external.yaml
apiVersion: v1
kind: Service
metadata:
name: thorax-external
namespace: mantis
annotations:
# For cloud provider load balancers
service.beta.kubernetes.io/aws-load-balancer-type: nlb
service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp
spec:
selector:
app: mantis
component: thorax
ports:
- port: 50051
targetPort: grpc
name: grpc
type: LoadBalancer
services/lens-service.yaml
apiVersion: v1
kind: Service
metadata:
name: lens
namespace: mantis
spec:
selector:
app: mantis
component: lens
ports:
- port: 4173
targetPort: http
name: http
type: ClusterIP
ingress/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mantis-ingress
namespace: mantis
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: 'true'
nginx.ingress.kubernetes.io/proxy-body-size: '50m'
cert-manager.io/cluster-issuer: 'letsencrypt-prod'
spec:
ingressClassName: nginx
tls:
- hosts:
- mantis.example.com
- api.mantis.example.com
secretName: mantis-ingress-tls
rules:
- host: mantis.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: lens
port:
number: 4173
- host: api.mantis.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mandible
port:
number: 3000
jobs/migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: mantis-migration
namespace: mantis
spec:
template:
spec:
containers:
- name: migration
image: ghcr.io/mantisplatform/mantis-mandible:latest
command: ['mandible', '--migrate-only']
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: mantis-database
key: url
restartPolicy: OnFailure
backoffLimit: 3

Run migrations:

Terminal window
kubectl apply -f jobs/migration-job.yaml
kubectl wait --for=condition=complete job/mantis-migration --timeout=120s
hpa/mandible-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: mandible-hpa
namespace: mantis
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: mandible
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
hpa/thorax-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: thorax-hpa
namespace: mantis
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: thorax
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
pdb/mandible-pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: mandible-pdb
namespace: mantis
spec:
minAvailable: 1
selector:
matchLabels:
app: mantis
component: mandible
---
# pdb/thorax-pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: thorax-pdb
namespace: mantis
spec:
minAvailable: 1
selector:
matchLabels:
app: mantis
component: thorax
network-policies/default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: mantis
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# network-policies/allow-mantis-internal.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-mantis-internal
namespace: mantis
spec:
podSelector:
matchLabels:
app: mantis
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: mantis
egress:
- to:
- podSelector:
matchLabels:
app: mantis
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
app: postgres
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
app: rabbitmq
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
app: redis

Deploy resources in this order:

Terminal window
# 1. Create namespace and secrets
kubectl apply -f namespace.yaml
kubectl apply -f secrets/
# 2. Create configmaps
kubectl apply -f configmaps/
# 3. Run migrations
kubectl apply -f jobs/migration-job.yaml
kubectl wait --for=condition=complete job/mantis-migration
# 4. Deploy services
kubectl apply -f services/
# 5. Deploy applications
kubectl apply -f deployments/
# 6. Configure ingress
kubectl apply -f ingress/
# 7. Set up autoscaling and policies
kubectl apply -f hpa/
kubectl apply -f pdb/
kubectl apply -f network-policies/
Terminal window
kubectl get deployments -n mantis
kubectl get pods -n mantis
kubectl get services -n mantis
Terminal window
# Port forward to check health endpoints
kubectl port-forward svc/mandible 3000:3000 -n mantis
curl http://localhost:3000/api/v1/health
# Check gRPC health
kubectl port-forward svc/thorax 50051:50051 -n mantis
grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check
Terminal window
kubectl logs -l app=mantis,component=mandible -n mantis
kubectl logs -l app=mantis,component=thorax -n mantis
Terminal window
# Check pod events
kubectl describe pod <pod-name> -n mantis
# Check container logs
kubectl logs <pod-name> -n mantis --previous
  1. Verify secret contains correct URL
  2. Check network policies allow egress to database
  3. Ensure database is accessible from cluster
  1. Verify secrets contain valid certificates
  2. Check certificate expiration dates
  3. Ensure CA certificate is correct