Skip to content

Kubernetes

This guide walks through deploying Frona on Kubernetes. The same services from the Docker Compose setup (engine, Browserless, SearXNG) are deployed as separate pods.

  • A running Kubernetes cluster
  • kubectl configured to access your cluster
  • A container registry with the Frona image (or build from the Dockerfile)
  • A StorageClass that supports ReadWriteOnce persistent volumes

Create a dedicated namespace:

apiVersion: v1
kind: Namespace
metadata:
name: frona

Store sensitive values in a Kubernetes Secret:

apiVersion: v1
kind: Secret
metadata:
name: frona-secrets
namespace: frona
type: Opaque
stringData:
FRONA_AUTH_ENCRYPTION_SECRET: "your-secret-key-here"
ANTHROPIC_API_KEY: "sk-ant-..."
# Add other provider keys as needed
# OPENAI_API_KEY: "sk-..."
# FRONA_VOICE_TWILIO_ACCOUNT_SID: "..."
# FRONA_VOICE_TWILIO_AUTH_TOKEN: "..."

Non-sensitive configuration:

apiVersion: v1
kind: ConfigMap
metadata:
name: frona-config
namespace: frona
data:
FRONA_SERVER_PORT: "3001"
FRONA_DATABASE_PATH: "/data/db"
FRONA_STORAGE_WORKSPACES_PATH: "/data/workspaces"
FRONA_STORAGE_FILES_PATH: "/data/files"
FRONA_BROWSER_WS_URL: "ws://browserless:3333"
FRONA_BROWSER_PROFILES_PATH: "/data/browser_profiles"
FRONA_SEARCH_PROVIDER: "searxng"
FRONA_SEARCH_SEARXNG_BASE_URL: "http://searxng:8080"
FRONA_SERVER_BASE_URL: "https://frona.example.com"

Frona needs persistent storage for its embedded database and file storage. Create a PersistentVolumeClaim:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: frona-data
namespace: frona
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi

The /data directory holds the database, workspaces, files, and browser profiles. A single volume mounted at /data covers everything.

apiVersion: apps/v1
kind: Deployment
metadata:
name: frona
namespace: frona
spec:
replicas: 1
selector:
matchLabels:
app: frona
template:
metadata:
labels:
app: frona
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: frona
image: your-registry/frona:latest
ports:
- containerPort: 3001
envFrom:
- configMapRef:
name: frona-config
- secretRef:
name: frona-secrets
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
livenessProbe:
httpGet:
path: /api/health
port: 3001
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /api/health
port: 3001
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: data
persistentVolumeClaim:
claimName: frona-data

Frona embeds its database in the same process, so you only need one replica. Running multiple replicas against the same volume is not supported.

apiVersion: v1
kind: Service
metadata:
name: frona
namespace: frona
spec:
selector:
app: frona
ports:
- port: 3001
targetPort: 3001
apiVersion: apps/v1
kind: Deployment
metadata:
name: browserless
namespace: frona
spec:
replicas: 1
selector:
matchLabels:
app: browserless
template:
metadata:
labels:
app: browserless
spec:
containers:
- name: browserless
image: ghcr.io/browserless/chromium:latest
ports:
- containerPort: 3333
env:
- name: MAX_CONCURRENT_SESSIONS
value: "10"
- name: PREBOOT_CHROME
value: "true"
- name: ENABLE_DEBUGGER
value: "true"
- name: PORT
value: "3333"
- name: TIMEOUT
value: "1800000"
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 4Gi
---
apiVersion: v1
kind: Service
metadata:
name: browserless
namespace: frona
spec:
selector:
app: browserless
ports:
- port: 3333
targetPort: 3333

Browserless needs more memory than the other services because it runs headless Chrome instances. Adjust limits based on MAX_CONCURRENT_SESSIONS.

apiVersion: apps/v1
kind: Deployment
metadata:
name: searxng
namespace: frona
spec:
replicas: 1
selector:
matchLabels:
app: searxng
template:
metadata:
labels:
app: searxng
spec:
containers:
- name: searxng
image: searxng/searxng:2026.2.16
ports:
- containerPort: 8080
env:
- name: SEARXNG_BASE_URL
value: "http://searxng:8080"
volumeMounts:
- name: searxng-config
mountPath: /etc/searxng/settings.yml
subPath: settings.yml
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
volumes:
- name: searxng-config
configMap:
name: searxng-config
---
apiVersion: v1
kind: Service
metadata:
name: searxng
namespace: frona
spec:
selector:
app: searxng
ports:
- port: 8080
targetPort: 8080
---
apiVersion: v1
kind: ConfigMap
metadata:
name: searxng-config
namespace: frona
data:
settings.yml: |
search:
formats:
- html
- json

The JSON format is required. Without it, the web_search tool won’t work.

Expose Frona externally with an Ingress. This example uses nginx-ingress with TLS:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frona
namespace: frona
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-buffering: "off"
spec:
ingressClassName: nginx
tls:
- hosts:
- frona.example.com
secretName: frona-tls
rules:
- host: frona.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frona
port:
number: 3001

The timeout and buffering annotations are important. Frona uses Server-Sent Events for real-time streaming, which requires long-lived connections and no response buffering.

  • Frona uses an embedded database, so only one replica is supported. Do not scale the Frona deployment beyond 1.
  • Browserless and SearXNG are internal services. Don’t expose them outside the cluster.
  • The Frona container runs as non-root user frona (UID 1000). Make sure the persistent volume is writable by this user (the fsGroup: 1000 in the security context handles this).
  • For Twilio voice callbacks, set FRONA_VOICE_CALLBACK_BASE_URL to your public Ingress URL.