Skip to content

This guide walks through deploying Frona on Kubernetes. All three containers (Frona, Browserless, SearXNG) run in a single pod and communicate via localhost.

Example manifests are available in the repository at examples/kubernetes/.

Prerequisites

  • A Kubernetes cluster (1.25+)
  • kubectl configured to access your cluster

Quick start

bash
# 1. Clone the repository and navigate to the examples
git clone https://github.com/fronalabs/frona.git
cd frona/examples/kubernetes

# 2. Deploy all resources
kubectl apply -k .

# 3. Wait for the pod to be ready
kubectl -n frona get pods -w

# 4. Access Frona (port-forward for local testing)
kubectl -n frona port-forward svc/frona 3001:3001
open http://localhost:3001

Architecture

All three containers run in a single pod:

ContainerDescription
fronaFrona server (port 3001)
browserlessHeadless Chromium for browser automation
searxngMeta search engine for web search

Because they share a pod, Frona connects to Browserless and SearXNG via localhost.

Manifests

Persistent storage

Frona needs persistent storage for its database, workspaces, files, and browser profiles. A single volume mounted at /app/data covers everything.

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

Deployment

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frona
  labels:
    app: frona
spec:
  replicas: 1
  selector:
    matchLabels:
      app: frona
  template:
    metadata:
      labels:
        app: frona
    spec:
      containers:
        - name: frona
          image: ghcr.io/fronalabs/frona:latest
          ports:
            - containerPort: 3001
          env:
            - name: FRONA_BROWSER_WS_URL
              value: "ws://localhost:3333"
            - name: FRONA_SEARCH_SEARXNG_BASE_URL
              value: "http://localhost:8080"
          volumeMounts:
            - name: data
              mountPath: /app/data
          readinessProbe:
            httpGet:
              path: /healthz
              port: 3001
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /healthz
              port: 3001
            initialDelaySeconds: 10
            periodSeconds: 30

        - name: browserless
          image: ghcr.io/browserless/chromium:latest
          env:
            - name: MAX_CONCURRENT_SESSIONS
              value: "10"
            - name: PREBOOT_CHROME
              value: "true"
          volumeMounts:
            - name: data
              mountPath: /profiles
              subPath: browser-profiles

        - name: searxng
          image: searxng/searxng:latest
          env:
            - name: SEARXNG_BASE_URL
              value: "http://localhost:8080"
          volumeMounts:
            - name: searxng-config
              mountPath: /etc/searxng/settings.yml
              subPath: settings.yml
              readOnly: true

      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: frona-data
        - name: searxng-config
          configMap:
            name: searxng-config

Frona uses an embedded database, so only one replica is supported. Do not scale beyond 1.

Service

yaml
apiVersion: v1
kind: Service
metadata:
  name: frona
spec:
  selector:
    app: frona
  ports:
    - name: http
      port: 3001
      targetPort: 3001
  type: ClusterIP

SearXNG config

The JSON format is required for the web_search tool to work.

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: searxng-config
data:
  settings.yml: |
    use_default_settings: true

    server:
      secret_key: "change-me-to-something-random"

    search:
      formats:
        - html
        - json

ServiceMonitor (optional)

If you use Prometheus Operator, add a ServiceMonitor to scrape metrics:

yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: frona
  labels:
    app: frona
spec:
  selector:
    matchLabels:
      app: frona
  endpoints:
    - port: http
      path: /metrics
      interval: 30s

Ingress

The Service is ClusterIP by default. To expose Frona externally, add an Ingress:

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

Updating

bash
kubectl -n frona rollout restart deployment frona

Notes

  • Only one replica is supported. Frona uses an embedded database that doesn't support concurrent access.
  • Browserless and SearXNG are internal to the pod. Don't expose them outside the cluster.
  • The Frona container runs as a rootless, non-root user (UID 1000).
  • For Twilio voice callbacks, set FRONA_VOICE_CALLBACK_BASE_URL to your public Ingress URL.