Deploying to Kubernetes
A single manifest that runs OrbisID on any Kubernetes cluster — PostgreSQL, the backend, and the frontend, fronted by the same nginx reverse proxy config the Docker Compose release packages use, pulling orbisid/orbisid-backend and orbisid/orbisid-frontend directly from Docker Hub.
Architecture
The nginx ConfigMap below is the exact nginx.conf used by the Docker Compose release packages, unmodified — its upstream hostnames (backend, frontend) resolve via Kubernetes' own cluster DNS, since that's exactly what the backend and frontend Services are named. Same /api/ + /actuator/ vs. everything-else routing split as every other OrbisID deployment method.
Prerequisites
kubectlinstalled and pointed at a cluster (local — kind/minikube/Docker Desktop — or managed — EKS/GKE/AKS)- A
StorageClassavailable for dynamicPersistentVolumeClaimprovisioning (the default on every managed cluster and on kind/minikube/Docker Desktop out of the box)
Deploy
Step 1: Create the namespace and secrets
Generate a database password and an encryption key, then create the namespace and a Secret holding both — never paste real secret values into the manifest itself:
- Linux
- macOS
- Windows
export ENCRYPTION_KEY=$(openssl rand -base64 32)
export DB_PASSWORD=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9')
echo "Save this — it cannot be recovered if lost: $ENCRYPTION_KEY"
kubectl create namespace orbisid
kubectl create secret generic orbisid-secrets -n orbisid \
--from-literal=encryption-key="$ENCRYPTION_KEY" \
--from-literal=postgres-password="$DB_PASSWORD"
export ENCRYPTION_KEY=$(openssl rand -base64 32)
export DB_PASSWORD=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9')
echo "Save this — it cannot be recovered if lost: $ENCRYPTION_KEY"
kubectl create namespace orbisid
kubectl create secret generic orbisid-secrets -n orbisid \
--from-literal=encryption-key="$ENCRYPTION_KEY" \
--from-literal=postgres-password="$DB_PASSWORD"
$bytes = New-Object byte[] 32
[System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
$ENCRYPTION_KEY = [Convert]::ToBase64String($bytes)
$DB_PASSWORD = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 24 | ForEach-Object { [char]$_ })
Write-Host "Save this — it cannot be recovered if lost: $ENCRYPTION_KEY"
kubectl create namespace orbisid
kubectl create secret generic orbisid-secrets -n orbisid `
--from-literal=encryption-key="$ENCRYPTION_KEY" `
--from-literal=postgres-password="$DB_PASSWORD"
Step 2: Apply the manifest
Copy the manifest below and save it locally as kubernetes.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
namespace: orbisid
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: orbisid
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: orbisid
- name: POSTGRES_USER
value: orbisid
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: orbisid-secrets
key: postgres-password
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
readinessProbe:
exec:
command: ["pg_isready", "-U", "orbisid", "-d", "orbisid"]
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
exec:
command: ["pg_isready", "-U", "orbisid", "-d", "orbisid"]
initialDelaySeconds: 30
periodSeconds: 30
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-data
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: orbisid
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: orbisid
spec:
replicas: 1
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
initContainers:
- name: wait-for-postgres
image: postgres:16-alpine
command:
- sh
- -c
- until pg_isready -h postgres -p 5432 -U orbisid; do echo waiting for postgres; sleep 2; done
containers:
- name: backend
# Pin to a specific released version (e.g. orbisid/orbisid-backend:2.12.2)
# for production — :latest will change under you on the next image push.
image: orbisid/orbisid-backend:latest
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: docker
- name: SPRING_DATASOURCE_URL
value: jdbc:postgresql://postgres:5432/orbisid
- name: SPRING_DATASOURCE_USERNAME
value: orbisid
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: orbisid-secrets
key: postgres-password
- name: ORBISID_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: orbisid-secrets
key: encryption-key
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 45
periodSeconds: 15
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: backend
namespace: orbisid
spec:
selector:
app: backend
ports:
- port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: orbisid
spec:
replicas: 1
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
# Pin to a specific released version (e.g. orbisid/orbisid-frontend:2.12.2)
# for production — :latest will change under you on the next image push.
image: orbisid/orbisid-frontend:latest
ports:
- containerPort: 3000
env:
# Frontend calls the API via a relative path — it and the backend are
# served from the same nginx origin (see the ConfigMap below), so no
# absolute URL is needed here.
- name: NEXT_PUBLIC_API_URL
value: ""
---
apiVersion: v1
kind: Service
metadata:
name: frontend
namespace: orbisid
spec:
selector:
app: frontend
ports:
- port: 3000
targetPort: 3000
---
# Same nginx.conf used by the Docker Compose release packages, unmodified — its
# upstream hostnames ("backend", "frontend") resolve via Kubernetes' own DNS
# since that's exactly what the two Services above are named.
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
namespace: orbisid
data:
nginx.conf: |
# =============================================================================
# OrbisID — Nginx Reverse Proxy Configuration
# =============================================================================
# HTTP only on port 80. TLS termination is handled externally (load balancer,
# Cloudflare, or a separate Nginx instance with Certbot).
#
# To add TLS directly on this server:
# 1. Obtain certificates (e.g. via Certbot: certbot --nginx)
# 2. Uncomment the HTTPS server block below and update the cert paths.
# 3. Add a redirect from the HTTP block: return 301 https://$host$request_uri;
# =============================================================================
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
# ── Gzip compression ───────────────────────────────────────────────────
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
application/xml+rss
application/atom+xml
image/svg+xml;
# ── Upstream services ─────────────────────────────────────────────────
upstream backend {
server backend:8080;
keepalive 32;
}
upstream frontend {
server frontend:3000;
keepalive 32;
}
# ── HTTP server block ─────────────────────────────────────────────────
server {
listen 80;
server_name _;
# ── Security headers ───────────────────────────────────────────────
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Strict-Transport-Security is only meaningful over HTTPS.
# Uncomment if you are terminating TLS here or have HTTPS everywhere.
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# ── API proxy ──────────────────────────────────────────────────────
location /api/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
# Increase timeouts for long-running scan operations
proxy_read_timeout 300s;
proxy_connect_timeout 10s;
proxy_send_timeout 300s;
# Allow large file uploads (CSV imports, script uploads)
client_max_body_size 50m;
}
# ── Actuator (internal health checks only — restrict in production) ─
location /actuator/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
# Uncomment to restrict actuator to internal networks only:
# allow 10.0.0.0/8;
# allow 172.16.0.0/12;
# allow 192.168.0.0/16;
# deny all;
}
# ── Frontend (Next.js) ─────────────────────────────────────────────
location / {
proxy_pass http://frontend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
# ── Swagger / API-docs server block (port 8443) ───────────────────────
# Exposes Swagger UI and OpenAPI docs on a dedicated port so they can be
# kept separate from the main application (e.g. blocked at the firewall
# in production, or only accessible inside a VPN).
server {
listen 8443;
server_name _;
# Swagger UI assets
location /swagger-ui/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
}
location = /swagger-ui.html {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
}
# OpenAPI JSON / YAML
location /api-docs {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
}
# Block everything else on this port
location / {
return 404;
}
}
# ── HTTPS server block (commented out — enable after obtaining certs) ──
# server {
# listen 443 ssl http2;
# server_name your.domain.com;
#
# ssl_certificate /etc/nginx/ssl/fullchain.pem;
# ssl_certificate_key /etc/nginx/ssl/privkey.pem;
#
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
# ssl_prefer_server_ciphers off;
# ssl_session_cache shared:SSL:10m;
# ssl_session_timeout 1d;
#
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
#
# # ... same location blocks as the HTTP server above ...
# }
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: orbisid
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
- containerPort: 8443
volumeMounts:
- name: config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
volumes:
- name: config
configMap:
name: nginx-config
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: orbisid
spec:
type: LoadBalancer
selector:
app: nginx
ports:
- name: http
port: 80
targetPort: 80
- Linux
- macOS
- Windows
kubectl apply -f kubernetes.yaml
kubectl apply -f kubernetes.yaml
kubectl apply -f kubernetes.yaml
Step 3: Wait for everything to come up
- Linux
- macOS
- Windows
kubectl get pods -n orbisid -w
kubectl get pods -n orbisid -w
kubectl get pods -n orbisid -w
backend won't reach Running until postgres is ready — its wait-for-postgres init container blocks on pg_isready in a loop, so a CrashLoopBackOff on postgres itself resolves before backend ever starts. Press Ctrl-C once all pods show Running/1/1.
Post-deployment
- Get to the application:
-
Managed cluster (EKS/GKE/AKS):
kubectl get svc nginx -n orbisid— thenginxService istype: LoadBalancer, so wait forEXTERNAL-IPto populate, then browse tohttp://<that-ip>. -
Local cluster (kind/minikube/Docker Desktop) without LoadBalancer support: port-forward instead:
- Linux
- macOS
- Windows
kubectl port-forward -n orbisid svc/nginx 8080:80kubectl port-forward -n orbisid svc/nginx 8080:80kubectl port-forward -n orbisid svc/nginx 8080:80Then browse to
http://localhost:8080.
-
- Log in with the default credentials (
admin/ChangeMe123!) and change the password immediately — Administration > Users. - Add target systems and configure scanning as usual — see Systems and Scanning.
Updating
Bump the image tag and reapply — this is the same pattern as any Kubernetes rolling update:
- Linux
- macOS
- Windows
kubectl set image deployment/backend backend=orbisid/orbisid-backend:2.13.0 -n orbisid
kubectl set image deployment/frontend frontend=orbisid/orbisid-frontend:2.13.0 -n orbisid
kubectl rollout status deployment/backend -n orbisid
kubectl rollout status deployment/frontend -n orbisid
kubectl set image deployment/backend backend=orbisid/orbisid-backend:2.13.0 -n orbisid
kubectl set image deployment/frontend frontend=orbisid/orbisid-frontend:2.13.0 -n orbisid
kubectl rollout status deployment/backend -n orbisid
kubectl rollout status deployment/frontend -n orbisid
kubectl set image deployment/backend backend=orbisid/orbisid-backend:2.13.0 -n orbisid
kubectl set image deployment/frontend frontend=orbisid/orbisid-frontend:2.13.0 -n orbisid
kubectl rollout status deployment/backend -n orbisid
kubectl rollout status deployment/frontend -n orbisid
The database and its PersistentVolumeClaim are untouched by an image version bump. Database migrations run automatically on backend startup (Flyway), same as every other OrbisID deployment method.
Known limitations
- No Kafka. Endpoint Sensor / Threat Detection event ingestion (
KAFKA_BOOTSTRAP_SERVERS) needs a reachable Kafka broker, which this manifest doesn't provision — same as the released Docker Compose packages, which also ship without one. If you need that feature, deploy a Kafka cluster into the same namespace (e.g. Strimzi) and setKAFKA_BOOTSTRAP_SERVERSon thebackendDeployment. - No bundled AI runtime. This manifest doesn't run Ollama — AI features stay off. To enable them, either point Administration > Settings > OrbisAI's base URL at a separately-hosted Ollama instance reachable from the cluster, or add another Deployment running
ollama/ollamawith aPersistentVolumeClaimfor model storage (CPU inference is memory-hungry — see Requirements). - Single replica everywhere.
postgres,backend,frontend, andnginxare allreplicas: 1.backend/frontend/nginxcan be scaled horizontally (kubectl scale deployment/backend --replicas=3 -n orbisid) since they're stateless;postgrescan't without moving to a proper HA Postgres setup (e.g. an operator like CloudNativePG) — itsPersistentVolumeClaimisReadWriteOnce, so more than one Postgres pod can't safely share it. - HTTP only, no TLS. Session cookies are
Secure-only (seeapplication.yml), which browsers still honor for plain HTTP onlocalhost/port-forwarded access, but not once you put a real hostname in front of thenginxService's external IP — add a TLS-terminating Ingress or LoadBalancer in front of it for anything beyond local testing, the same way the AWS, Azure, and GCP guides do.
Tearing down
- Linux
- macOS
- Windows
kubectl delete namespace orbisid
kubectl delete namespace orbisid
kubectl delete namespace orbisid
This deletes everything in the namespace, including the postgres-data PersistentVolumeClaim and (depending on your cluster's StorageClass reclaim policy) the underlying data. Take a manual backup first if you need one:
- Linux
- macOS
- Windows
kubectl exec -n orbisid deploy/postgres -- pg_dump -U orbisid orbisid > backup.sql
kubectl exec -n orbisid deploy/postgres -- pg_dump -U orbisid orbisid > backup.sql
kubectl exec -n orbisid deploy/postgres -- pg_dump -U orbisid orbisid > backup.sql
Troubleshooting
backendstuck inInit:0/1: its init container is waiting onpostgres— checkkubectl logs -n orbisid deploy/postgresfor why Postgres itself isn't becoming ready (often aPersistentVolumeClaimstuckPendingbecause noStorageClassis available).nginxServiceEXTERNAL-IPstuck<pending>: your cluster has noLoadBalancerimplementation (common on kind/minikube withoutmetallb, or Docker Desktop without an add-on) — use thekubectl port-forwardfallback from Post-deployment instead.- 502 from nginx: the backend takes time to run Flyway migrations on first boot; its readiness probe should hold nginx's upstream back until it's ready, but check
kubectl logs -n orbisid deploy/backendif it persists. - Frontend loads but API calls fail: confirm the
backendandfrontendService names weren't changed — the embeddednginx.confproxies to them by hostname (backend,frontend), which only resolves via cluster DNS if the Service names match exactly.