Skip to main content

Thunders Hybrid Deployment Guide

This guide covers deploying the Thunders Browserless image on your own Azure infrastructure, enabling secure browser automation through Azure Relay.

Written by Ines

This guide covers deploying Thunders Hybrid Mode on your own Azure infrastructure, enabling secure browser automation through Azure Relay.

Overview

The Thunders Hybrid Deployment image is a unified container that packages:

  • Thunders Browser Runtime: Headless browser execution layer supporting Chromium, Firefox, and WebKit

  • Nginx Reverse Proxy: Request routing, WebSocket proxying, health checks, and file serving

  • Azure Relay Bridge: Outbound-only connection to Azure Relay for secure communication with Thunders's platform

All three components run inside a single container managed by supervisord, making deployment straightforward.

Note: Throughout this guide, <CUSTOMER_NAME> refers to your organization-specific image repository name provided by Thunders. Replace it with the actual name you received (e.g., tunisietelecom, acme-corp, etc.).

How It Works

Architecture of a hybrid deployment: in your infrastructure, one container runs Nginx, the browser engine and the relay bridge. The relay bridge connects outbound only to Azure Relay, which connects to the Thunders platform.

Key points:

  • No inbound firewall rules required: The relay bridge connects outbound to Azure Relay over HTTPS (port 443)

  • No VPN needed: Azure Relay handles secure tunneling

  • Single container: One image to pull, one container to run

Prerequisites

  • An Azure subscription (or any environment that can run Docker containers)

  • ACR credentials provided by Thunders (to pull the image)

  • Azure Relay configuration (see Azure Relay section below)

  • For Azure deployments: Azure CLI installed and authenticated

Credentials You Will Receive from Thunders

Thunders will provide the following credentials for your deployment:

Credential

Description

ACR Server

Container registry URL (e.g., thundersacr-xxx.azurecr.io)

ACR Username

Token name for pulling the image

ACR Password

Token password for pulling the image

Relay Namespace

Azure Relay namespace (if Thunders-managed)

Relay Hybrid Connection Name

Hybrid connection name (if Thunders-managed)

Relay Listen Key

SAS key for the relay listener (if Thunders-managed)

Store these securely. Never commit them to source control.

Azure Relay Configuration

Azure Relay enables secure communication between Thunders's platform and your self-hosted execution environment without opening inbound firewall ports. There are two setup options:

Option A: Thunders-Managed Relay (Recommended)

Thunders creates and manages the Azure Relay namespace and hybrid connection in Thunders's Azure subscription. You will receive:

  • RELAY_NAMESPACE: The relay namespace URL

  • RELAY_HYCO: The hybrid connection name

  • RELAY_LISTEN_KEY: The SAS key with listen permission

Your container connects outbound to this relay. Thunders's platform connects from the other side using send permissions.

No Azure Relay setup required on your end.

Option B: Customer-Managed Relay

If you prefer to manage the relay in your own Azure subscription:

1. Create an Azure Relay namespace:

az relay namespace create \
--resource-group <your-rg> \
--name <relay-namespace-name> \
--location <azure-region>

2. Create a hybrid connection:

az relay hyco create \
--resource-group <your-rg> \
--namespace-name <relay-namespace-name> \
--name <hybrid-connection-name>

3. Create authorization rules:

Create a listen rule (for your self-hosted container):

az relay hyco authorization-rule create \
--resource-group <your-rg> \
--namespace-name <relay-namespace-name> \
--hybrid-connection-name <hybrid-connection-name> \
--name listen \
--rights Listen

Create a send rule (for Thunders's platform):

az relay hyco authorization-rule create \
--resource-group <your-rg> \
--namespace-name <relay-namespace-name> \
--hybrid-connection-name <hybrid-connection-name> \
--name send \
--rights Send

4. Retrieve the keys:

Get the listen key (for your container):

az relay hyco authorization-rule keys list \
--resource-group <your-rg> \
--namespace-name <relay-namespace-name> \
--hybrid-connection-name <hybrid-connection-name> \
--name listen \
--query primaryKey -o tsv

Get the send key (share this with Thunders):

az relay hyco authorization-rule keys list \
--resource-group <your-rg> \
--namespace-name <relay-namespace-name> \
--hybrid-connection-name <hybrid-connection-name> \
--name send \
--query primaryKey -o tsv

5. Share with Thunders:

  • Relay namespace: <relay-namespace-name>.servicebus.windows.net

  • Hybrid connection name: <hybrid-connection-name>

  • Send key name: send

  • Send key secret: (the send key from step 4)

Thunders will configure these in their platform to connect to your relay.

Environment Variables Reference

Required Variables

Variable

Description

Example

RELAY_NAMESPACE

Azure Relay namespace

myrelay.servicebus.windows.net

RELAY_HYCO

Hybrid connection name

thunders-hybrid

RELAY_LISTEN_KEY

SAS key with listen permission

base64-encoded-key

Optional Variables

Variable

Default

Description

CONCURRENT

15

Maximum concurrent browser sessions

TIMEOUT

1800000

Session timeout in milliseconds (default: 30 minutes)

EXTERNAL

http://localhost:3000

External URL of this instance (used in health responses)

Ports

Port

Service

Description

3000

Nginx (exposed)

Main entry point, health checks, WebSocket proxy

3001

Thunders Browser Runtime (internal)

Browser engine (proxied through Nginx)

3002

Health check (internal)

Health score calculation service

8080

Relay bridge (internal)

Azure Relay bridge health endpoint

Only port 3000 needs to be exposed.

Deployment Options

Option 1: Azure Container Apps (Recommended)

Azure Container Apps provides a managed container hosting platform with built-in scaling, health probes, and HTTPS.

Quick Deploy with Azure CLI

# Login to Azure
az login

# Set variables
RESOURCE_GROUP="rg-thunder-browserless"
LOCATION="westeurope"
ENV_NAME="env-thunder-browserless"
APP_NAME="thunder-browserless"

# Create resource group
az group create --name $RESOURCE_GROUP --location $LOCATION

# Create Container Apps environment
az containerapp env create \
--name $ENV_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATION

# Deploy the container app
az containerapp create \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--environment $ENV_NAME \
--image <THUNDER_ACR_SERVER>/<CUSTOMER_NAME>:latest \
--registry-server <THUNDER_ACR_SERVER> \
--registry-username <THUNDER_ACR_USERNAME> \
--registry-password <THUNDER_ACR_PASSWORD> \
--target-port 3000 \
--ingress internal \
--cpu 4.0 \
--memory 8.0Gi \
--min-replicas 1 \
--max-replicas 1 \
--env-vars \
CONCURRENT=15 \
TIMEOUT=1800000 \
RELAY_NAMESPACE=<YOUR_RELAY_NAMESPACE> \
RELAY_HYCO=<YOUR_RELAY_HYCO> \
RELAY_LISTEN_KEY=secretref:relay-key \
--secrets \
relay-key=<YOUR_RELAY_LISTEN_KEY>

Deploy with ARM Template

Use the provided ARM template for a more configurable deployment:

az deployment group create \
--resource-group $RESOURCE_GROUP \
--template-file customer-browserless-template.json \
--parameters \
containerAppName=$APP_NAME \
containerAppEnvironmentId=$(az containerapp env show -g $RESOURCE_GROUP -n $ENV_NAME --query id -o tsv) \
containerImage=<THUNDER_ACR_SERVER>/<CUSTOMER_NAME>:latest \
acrServer=<THUNDER_ACR_SERVER> \
acrUsername=<THUNDER_ACR_USERNAME> \
acrPassword=<THUNDER_ACR_PASSWORD> \
relayNamespace=<YOUR_RELAY_NAMESPACE> \
relayHycoName=<YOUR_RELAY_HYCO> \
relayListenKey=<YOUR_RELAY_LISTEN_KEY>

Option 2: Azure Container Instances (ACI)

Azure Container Instances provides serverless container hosting. Simpler but with fewer features than Container Apps.

# Create resource group
az group create --name rg-thunder-browserless --location westeurope

# Deploy container
az container create \
--resource-group rg-thunder-browserless \
--name thunder-browserless \
--image <THUNDER_ACR_SERVER>/<CUSTOMER_NAME>:latest \
--registry-login-server <THUNDER_ACR_SERVER> \
--registry-username <THUNDER_ACR_USERNAME> \
--registry-password <THUNDER_ACR_PASSWORD> \
--cpu 4 \
--memory 8 \
--ports 3000 \
--ip-address Private \
--environment-variables \
CONCURRENT=15 \
TIMEOUT=1800000 \
RELAY_NAMESPACE=<YOUR_RELAY_NAMESPACE> \
RELAY_HYCO=<YOUR_RELAY_HYCO> \
RELAY_LISTEN_KEY=<YOUR_RELAY_LISTEN_KEY> \
--restart-policy Always

# Check status
az container show \
--resource-group rg-thunder-browserless \
--name thunder-browserless \
--query "{status:instanceView.state, ip:ipAddress.ip}" \
-o table

Option 3: Azure Kubernetes Service (AKS)

For organizations already running AKS clusters.

Create Kubernetes Secret

# Create namespace
kubectl create namespace thunder

# Create image pull secret
kubectl create secret docker-registry thunder-acr \
--namespace thunder \
--docker-server=<THUNDER_ACR_SERVER> \
--docker-username=<THUNDER_ACR_USERNAME> \
--docker-password=<THUNDER_ACR_PASSWORD>

# Create secrets for environment variables
kubectl create secret generic thunder-browserless-secrets \
--namespace thunder \
--from-literal=RELAY_LISTEN_KEY=<YOUR_RELAY_LISTEN_KEY>

Kubernetes Deployment Manifest

Save the following as thunders-hybrid.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
name: thunder-browserless
namespace: thunder
spec:
replicas: 1
selector:
matchLabels:
app: thunder-browserless
template:
metadata:
labels:
app: thunder-browserless
spec:
imagePullSecrets:
- name: thunder-acr
containers:
- name: thunder-browserless
image: <THUNDER_ACR_SERVER>/<CUSTOMER_NAME>:latest
ports:
- containerPort: 3000
env:
- name: CONCURRENT
value: "15"
- name: TIMEOUT
value: "1800000"
- name: RELAY_NAMESPACE
value: "<YOUR_RELAY_NAMESPACE>"
- name: RELAY_HYCO
value: "<YOUR_RELAY_HYCO>"
- name: RELAY_LISTEN_KEY
valueFrom:
secretKeyRef:
name: thunder-browserless-secrets
key: RELAY_LISTEN_KEY
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
cpu: "4"
memory: "8Gi"
startupProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 10
failureThreshold: 30
livenessProbe:
httpGet:
path: /health
port: 3000
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 3000
periodSeconds: 10
timeoutSeconds: 10
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: thunder-browserless
namespace: thunder
spec:
type: ClusterIP
selector:
app: thunder-browserless
ports:
- port: 3000
targetPort: 3000
protocol: TCP

Apply:

kubectl apply -f thunder-browserless.yaml

Option 4: Docker (VM or On-Premises)

For running directly on a VM or on-premises server.

Pull and Run

# Login to Thunders's ACR
docker login <THUNDER_ACR_SERVER> \
-u <THUNDER_ACR_USERNAME> \
-p <THUNDER_ACR_PASSWORD>

# Pull the image
docker pull <THUNDER_ACR_SERVER>/<CUSTOMER_NAME>:latest

# Run the container
docker run -d \
--name thunder-browserless \
--restart unless-stopped \
-p 3000:3000 \
-e CONCURRENT=15 \
-e TIMEOUT=1800000 \
-e RELAY_NAMESPACE=<YOUR_RELAY_NAMESPACE> \
-e RELAY_HYCO=<YOUR_RELAY_HYCO> \
-e RELAY_LISTEN_KEY=<YOUR_RELAY_LISTEN_KEY> \
--shm-size=2g \
<THUNDER_ACR_SERVER>/<CUSTOMER_NAME>:latest

Important: The --shm-size=2g flag is required. Chromium uses /dev/shm for shared memory, and the default 64MB is insufficient for browser automation.

Docker Compose

Save as docker-compose.yml:

services:
thunder-browserless:
image: <THUNDER_ACR_SERVER>/<CUSTOMER_NAME>:latest
container_name: thunder-browserless
restart: unless-stopped
ports:
- "3000:3000"
environment:
- CONCURRENT=${CONCURRENT:-15}
- TIMEOUT=${TIMEOUT:-1800000}
- RELAY_NAMESPACE=${RELAY_NAMESPACE}
- RELAY_HYCO=${RELAY_HYCO}
- RELAY_LISTEN_KEY=${RELAY_LISTEN_KEY}
shm_size: 2g
deploy:
resources:
limits:
cpus: "4.0"
memory: 8G
reservations:
cpus: "2.0"
memory: 4G

Create a .env file alongside docker-compose.yml:

RELAY_NAMESPACE=your-relay.servicebus.windows.net
RELAY_HYCO=your-hybrid-connection
RELAY_LISTEN_KEY=your-listen-key
CONCURRENT=15
TIMEOUT=1800000

Run:

docker compose up -d

Health Checks and Monitoring

Health Endpoint

The container exposes health checks at:

  • GET /health: Returns health status with score

  • GET /healthz: Alias for /health

Healthy response (HTTP 200):

{
"origin": "http://localhost:3000",
"healthy": true,
"score": 85,
"error": null,
"pressure": {
"cpu": 15,
"memory": 45,
"running": 2,
"maxConcurrent": 15,
"queued": 0,
"isAvailable": true
},
"scores": {
"cpu": 84,
"memory": 44,
"concurrent": 87
},
"timestamp": "2026-01-15T10:30:00.000Z"
}

Unhealthy response (HTTP 503):

Returned when CPU exceeds 95%, memory exceeds 80%, or concurrent sessions are at maximum.

Monitoring Recommendations

  • Poll /health every 15-30 seconds

  • Alert when healthy is false for more than 2 consecutive checks

  • Monitor pressure.running vs pressure.maxConcurrent for capacity planning

  • Track scores.cpu and scores.memory trends for scaling decisions

Viewing Logs

Azure Container Apps:

az containerapp logs show -g <resource-group> -n <app-name> --follow

Azure Container Instances:

az container logs --resource-group <resource-group> --name <container-name> --follow

Docker:

docker logs -f thunder-browserless

Kubernetes:

kubectl logs -f deployment/thunder-browserless -n thunder

When everything is ready

When your container is deployed and the relay bridge is connected, please let us know.

You should then see a new location in Thunders called: <organization name> Self Hosted.

Select it and run a quick test to confirm everything works as expected.

If you’re testing a non-HTTPS website, enable Ignore HTTPS certificate errors during testing in the location’s Advanced settings.

Keep your image up to date

Thunders checks the version of the browser service that each of your self-hosted locations runs. When a location runs a version that Thunders no longer supports, a warning shows when you select that location for a run:

  • On the Test Case page, above the browser view

  • In the Execution settings dialog

  • In the Schedule this Test Set and Edit Schedule dialogs

When the location is set to Per Test, the warning shows if any of your self-hosted locations is out of date.

The warning names the location, the version that it runs and the minimum version. It asks the team that manages the location to update it, because an out-of-date image can make tests fail. The warning also shows when the image is too old to report its version. Locations that Thunders manages never show this warning.

To update your image, deploy the <THUNDER_ACR_SERVER>/<CUSTOMER_NAME>:latest image again. Keep the same environment variables.

  • Azure Container Apps: create a new revision with the image. Use a new suffix for each update, for example the date.

az containerapp update -n <app-name> -g <resource-group> --image <THUNDER_ACR_SERVER>/<CUSTOMER_NAME>:latest --revision-suffix <new-suffix>
  • Azure Container Instances: restart the container group. The restart downloads the new image.

az container restart --resource-group <resource-group> --name <container-name>
  • Kubernetes: restart the deployment.

kubectl rollout restart deployment/<deployment-name> -n thunder
  • Docker Compose: pull the new image, then create the container again.

docker compose pull
docker compose up -d
  • Docker: pull the image and remove the container. Then run the docker run command of Option 4 again.

docker pull <THUNDER_ACR_SERVER>/<CUSTOMER_NAME>:latest
docker rm -f <container-name>

The warning disappears within about 10 minutes after the updated container connects to the relay.

Troubleshooting

Container fails to start

  1. Check logs for missing environment variables:

Required environment variable RELAY_NAMESPACE is not set

Ensure all required environment variables are set.

  1. Verify ACR credentials:

echo '<password>' | docker login <THUNDER_ACR_SERVER> -u <username> --password-stdin
docker pull <THUNDER_ACR_SERVER>/<CUSTOMER_NAME>:latest

Relay bridge not connecting

  1. Verify outbound connectivity on port 443 to *.servicebus.windows.net

  2. Check relay bridge logs for connection errors

  3. Verify RELAY_NAMESPACE, RELAY_HYCO, and RELAY_LISTEN_KEY are correct

  4. The relay bridge auto-reconnects every 5 seconds on disconnect

Health check returns 503

  • CPU threshold exceeded: Reduce CONCURRENT or allocate more CPU

  • Memory threshold exceeded: Allocate more memory or reduce CONCURRENT

  • All sessions busy: Wait for sessions to complete or increase CONCURRENT

Thunders Hybrid sessions timing out

  • Increase TIMEOUT (default: 1800000ms = 30 minutes)

  • Check network latency between your infrastructure and Azure Relay

Shared memory errors

If running with Docker, ensure --shm-size=2g is set. Chromium requires more than the default 64MB of shared memory.

Resource Sizing Guide

Concurrent Sessions

CPU

Memory

Shared Memory

5

2 cores

4 GB

1 GB

15 (default)

4 cores

8 GB

2 GB

30

8 cores

16 GB

4 GB

60

16 cores

32 GB

8 GB

These are recommended minimums. Actual requirements depend on the complexity of the web pages being tested.

Network Requirements

Direction

Protocol

Port

Destination

Purpose

Outbound

HTTPS/WSS

443

*.servicebus.windows.net

Azure Relay connection

Outbound

HTTPS

443

Target web applications

Browser automation

Inbound (optional)

HTTP

3000

Local network

Health checks (not required for relay)

No inbound internet access is required for the relay connection to work.

For technical assistance or questions about architecture, please reach out to our support team at [email protected].

Other related reads you might find useful:

Did this answer your question?