Skip to content

Conversation

VietND96
Copy link
Member

@VietND96 VietND96 commented Sep 2, 2025

User description

Thanks for contributing to the Docker-Selenium project!
A PR well described will help maintainers to quickly review and merge it

Before submitting your PR, please check our contributing guidelines, applied for this repository.
Avoid large PRs, help reviewers by making them as simple and short as possible.

Description

Fixes #2795 - [🚀 Feature]: Build "magic node" with all popular browsers in a single container

#2944 - Fix deploy multi-arch images
#2946 - Support switch binary Chrome/Chromium in Node/Standalone all browsers image (image arch linux/amd64).

Single Node/Standalone Image With All Browsers

From image tag 4.35.0 onwards, a single Node/Standalone image is available with all browsers are pre-installed. Those images are selenium/standalone-all-browsers (standalone all in one), selenium/node-all-browsers (for Hub-Node mode).

These two images are suitable for users:

  • Prefer a single container with "all-in-one" includes Selenium Grid and popular browsers.
  • Don't care about the image size, prefer the convenience.
  • Lightweight workload, able to figure out for yourself the resource consumption.

According to multi-arch support, browsers are available in images selenium/node-all-browsers and selenium/standalone-all-browsers would be different per architecture.

Browser / Arch x86_64 (aka amd64) aarch64 (aka arm64/armv8)
Chrome
Edge
Firefox
Chromium

Both Chrome and Chromium browser binary are available in image arch linux/amd64. However, Chrome browser binary is activated by default. In case you want to switch to Chromium browser binary, you can set environment variable SE_BROWSER_BINARY_LOCATION_CHROME=/usr/bin/chromium.

Motivation and Context

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • I have read the contributing document.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

PR Type

Enhancement


Description

  • Add support for all browsers in single Node/Standalone container

  • Create new node-all-browsers and standalone-all-browsers images

  • Update build system to support multi-browser containers

  • Enhance configuration generation for multiple browsers per container


Diagram Walkthrough

flowchart LR
  A["Single Browser Images"] --> B["Multi-Browser Support"]
  B --> C["node-all-browsers"]
  B --> D["standalone-all-browsers"]
  C --> E["Chrome + Edge + Firefox"]
  D --> E
  F["Build System"] --> G["Enhanced Makefile"]
  H["Config Generation"] --> I["Multi-Browser Config"]
Loading

File Walkthrough

Relevant files
Miscellaneous
3 files
generate_release_notes.sh
Remove unused RCLONE_TAG_VERSION variable                               
+0/-1     
Dockerfile
Remove duplicate config generation script                               
+0/-3     
generate_config
Remove standalone-specific config generation script           
+0/-68   
Tests
1 files
test.py
Add test mappings for all-browsers images                               
+12/-0   
Enhancement
6 files
Makefile
Add build targets for all-browsers containers                       
+77/-1   
generate_config
Support multi-browser configuration generation                     
+124/-38
Dockerfile
Update browser info storage structure                                       
+4/-3     
Dockerfile
Update browser info storage and fix apt sources                   
+7/-6     
Dockerfile
Update browser info storage structure                                       
+4/-3     
Dockerfile
Update browser info storage structure                                       
+4/-3     
Documentation
1 files
README.md
Document all-browsers container support                                   
+57/-14 
Configuration changes
1 files
docker-compose-v3-node-all-browsers.yml
Add docker-compose example for all-browsers                           
+41/-0   

Copy link
Contributor

qodo-merge-pro bot commented Sep 2, 2025

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

2795 - Partially compliant

Compliant requirements:

  • Provide a "magic node" image that includes the 3 popular browsers in a single container.
  • Support both AMD64 and ARM64 architectures.
  • Reuse current tests for the new image in CI.
  • Provide example usage and documentation for the new images.

Non-compliant requirements:

  • Keep Dockerfiles, build and deploy process simple and maintainable.
  • Maintain the same functions/behavior as current Node/Standalone.

Requires further human verification:

  • Keep Dockerfiles, build and deploy process simple and maintainable.
  • Maintain the same functions/behavior as current Node/Standalone.
  • Validate CI actually runs and passes for new all-browsers variants across architectures.
  • Runtime validation that all browsers work concurrently and respect per-browser env overrides.
⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Build Logic

The 'all_browsers' target builds multiple images with the same final tag sequentially, potentially overwriting layers and relying on build order; also the AMD64 branch skips Chromium while ARM branch skips Edge/Chrome. Validate the intent and ensure correct multi-arch manifests are produced without accidental overwrites.

all_browsers: node_base
	case "$(PLATFORMS)" in \
    *linux/amd64*) \
			cd ./NodeFirefox && docker buildx build --platform linux/amd64 $(BUILD_ARGS) $(FROM_IMAGE_ARGS) --build-arg BASE=node-base --build-arg FIREFOX_DOWNLOAD_URL=$(FIREFOX_DOWNLOAD_URL) -t $(NAME)/node-all-browsers:$(TAG_VERSION) . ; \
			cd .. ; \
			cd ./NodeChrome && docker buildx build --platform linux/amd64 $(BUILD_ARGS) $(FROM_IMAGE_ARGS) --build-arg BASE=node-all-browsers -t $(NAME)/node-all-browsers:$(TAG_VERSION) . ; \
			cd .. ; \
			cd ./NodeEdge && docker buildx build --platform linux/amd64 $(BUILD_ARGS) $(FROM_IMAGE_ARGS) --build-arg BASE=node-all-browsers -t $(NAME)/node-all-browsers:$(TAG_VERSION) . ; \
      ;; \
    *) \
			cd ./NodeFirefox && docker buildx build --platform $(PLATFORMS) $(BUILD_ARGS) $(FROM_IMAGE_ARGS) --build-arg BASE=node-base --build-arg FIREFOX_DOWNLOAD_URL=$(FIREFOX_DOWNLOAD_URL) -t $(NAME)/node-all-browsers:$(TAG_VERSION) . ; \
			cd .. ; \
			cd ./NodeChromium && docker buildx build --platform $(PLATFORMS) $(BUILD_ARGS) $(FROM_IMAGE_ARGS) --build-arg BASE=node-all-browsers --build-arg CHROMIUM_VERSION=$(CHROMIUM_VERSION) -t $(NAME)/node-all-browsers:$(TAG_VERSION) . ; \
      ;; \
  esac

standalone_all_browsers: all_browsers
	cd ./Standalone && docker buildx build --platform $(PLATFORMS) $(BUILD_ARGS) $(FROM_IMAGE_ARGS) --build-arg BASE=node-all-browsers -t $(NAME)/standalone-all-browsers:$(TAG_VERSION) .
Config Robustness

Environment backup/restore and per-browser overrides are added, but failure handling and validation of JSON merges and env presence are minimal. Confirm that missing files or malformed env vars do not produce invalid TOML and that multiple [[node.driver-configuration]] entries are generated as expected.

# A global array of environment variable prefixes supports different browser suffixes
ENV_PREFIXES=(
	"SE_NODE_STEREOTYPE"
	"SE_NODE_BROWSER_NAME"
	"SE_NODE_BROWSER_VERSION"
	"SE_NODE_PLATFORM_NAME"
	"SE_BROWSER_BINARY_LOCATION"
	"SE_NODE_STEREOTYPE_EXTRA"
	"SE_NODE_MAX_SESSIONS"
)

function backup_original_env_vars() {
	echo "Backing up original environment variables..."

	for prefix in "${ENV_PREFIXES[@]}"; do
		local backup_var="${prefix}_ORIGINAL"
		local common_var="${prefix}"

		# Backup original value if not already backed up
		if [[ -z "${!backup_var}" ]] && [[ -n "${!common_var}" ]]; then
			eval "${backup_var}=\"${!common_var}\""
			echo "Backed up original ${common_var}=${!common_var} to ${backup_var}"
		fi
	done
}

function restore_original_env_vars() {
	echo "Restoring original environment variables..."

	for prefix in "${ENV_PREFIXES[@]}"; do
		local backup_var="${prefix}_ORIGINAL"
		local common_var="${prefix}"

		# Restore original value if backup exists
		if [[ -n "${!backup_var}" ]]; then
			eval "${common_var}=\"${!backup_var}\""
			echo "Restored original ${backup_var}=${!backup_var} to ${common_var}"
		else
			# Clear the variable if no backup exists
			eval "${common_var}=\"\""
			echo "Cleared ${common_var} (no original backup)"
		fi
	done
}

function assign_browser_specific_env_vars() {
	local browser_name=$1

	# Set browser-specific values or inherit original values
	for prefix in "${ENV_PREFIXES[@]}"; do
		local browser_specific_var="${prefix}_${browser_name}"
		local common_var="${prefix}"
		local backup_var="${prefix}_ORIGINAL"

		# Check if the browser-specific environment variable exists
		if [[ -n "${!browser_specific_var}" ]]; then
			# Assign the browser-specific value to the common variable
			eval "${common_var}=\"${!browser_specific_var}\""
			echo "Assigned ${browser_specific_var}=${!browser_specific_var} to ${common_var}"
		elif [[ -n "${!backup_var}" ]]; then
			# Inherit original value if browser-specific value is not set
			eval "${common_var}=\"${!backup_var}\""
			echo "Inherited original ${backup_var}=${!backup_var} to ${common_var}"
		fi
	done
}

if [[ -z "$CONFIG_FILE" ]]; then
	FILENAME="/opt/selenium/config.toml"
else
	FILENAME="$CONFIG_FILE"
fi

if [[ -n "${SE_EVENT_BUS_HOST}" ]]; then
	echo "[events]
    publish = \"tcp://${SE_EVENT_BUS_HOST}:${SE_EVENT_BUS_PUBLISH_PORT}\"
    subscribe = \"tcp://${SE_EVENT_BUS_HOST}:${SE_EVENT_BUS_SUBSCRIBE_PORT}\"
    " >"$FILENAME"
fi

if [[ -z "${SE_NODE_HOST}" ]] && [[ -z "${SE_NODE_PORT}" ]]; then
	echo "Configuring server..."
else
	echo "[server]" >>"$FILENAME"
fi

if [[ -z "${SE_NODE_HOST}" ]]; then
	echo "Setting up SE_NODE_HOST..."
else
	echo "host = \"${SE_NODE_HOST}\"" >>"$FILENAME"
fi

if [[ -z "${SE_NODE_PORT}" ]]; then
	echo "Setting up SE_NODE_PORT..."
else
	echo "port = \"${SE_NODE_PORT}\"" >>"$FILENAME"
fi

echo "[node]" >>"$FILENAME"
# String, Url where the Grid can be reached
if [[ -z "${SE_NODE_GRID_URL}" ]]; then
	echo "Setting up SE_NODE_GRID_URL..."
else
	echo "grid-url = \"${SE_NODE_GRID_URL}\"" >>"$FILENAME"
fi
echo "session-timeout = ${SE_NODE_SESSION_TIMEOUT}" >>"$FILENAME"
echo "override-max-sessions = ${SE_NODE_OVERRIDE_MAX_SESSIONS}" >>"$FILENAME"
echo "detect-drivers = false" >>"$FILENAME"
echo "drain-after-session-count = ${DRAIN_AFTER_SESSION_COUNT:-$SE_DRAIN_AFTER_SESSION_COUNT}" >>"$FILENAME"

# Check if /opt/selenium/browsers directory exists and iterate through browser folders
if [ -d "/opt/selenium/browsers" ]; then
	# Backup original environment variables before processing browsers
	backup_original_env_vars

	for browser_dir in /opt/selenium/browsers/*/; do
		if [ -d "$browser_dir" ]; then
			browser_name=$(basename "$browser_dir" | tr '[:lower:]' '[:upper:]')
			echo "Processing browser: $browser_name"

			# Assign browser-specific environment variables to common variables
			assign_browser_specific_env_vars "$browser_name"

			if [ -f "${browser_dir}name" ]; then
				SE_NODE_BROWSER_NAME=$(cat "${browser_dir}name")
			fi
			if [ -f "${browser_dir}version" ] && [ "${SE_NODE_BROWSER_VERSION,,}" = "stable" ]; then
				SE_NODE_BROWSER_VERSION=$(short_version "$(cat "${browser_dir}version")")
			fi
			if [ -f "${browser_dir}binary_location" ] && [ -z "${SE_BROWSER_BINARY_LOCATION}" ]; then
				SE_BROWSER_BINARY_LOCATION=$(cat "${browser_dir}binary_location")
			fi
			SE_NODE_CONTAINER_NAME="${SE_NODE_CONTAINER_NAME:-$(hostname)}"

			# 'browserName' is mandatory for default stereotype
			if [[ -z "${SE_NODE_STEREOTYPE}" ]] && [[ -n "${SE_NODE_BROWSER_NAME}" ]] && ([[ -z "${SE_NODE_RELAY_URL}" ]] || [[ "${SE_NODE_RELAY_ONLY}" = "false" ]]); then
				SE_NODE_STEREOTYPE="{\"browserName\": \"${SE_NODE_BROWSER_NAME}\", \"browserVersion\": \"${SE_NODE_BROWSER_VERSION}\", \"platformName\": \"${SE_NODE_PLATFORM_NAME}\", \"se:containerName\": \"${SE_NODE_CONTAINER_NAME}\", \"container:hostname\": \"$(hostname)\"}"
				if [[ -n "${SE_BROWSER_BINARY_LOCATION}" ]]; then
					SE_NODE_STEREOTYPE="$(python3 /opt/bin/json_merge.py "${SE_NODE_STEREOTYPE}" "{${SE_BROWSER_BINARY_LOCATION}}")"
				fi
			else
				SE_NODE_STEREOTYPE="${SE_NODE_STEREOTYPE}"
			fi
			if [[ -n "${SE_NODE_STEREOTYPE_EXTRA}" ]]; then
				echo "Merging SE_NODE_STEREOTYPE_EXTRA=${SE_NODE_STEREOTYPE_EXTRA} to main stereotype for $browser_name"
				SE_NODE_STEREOTYPE="$(python3 /opt/bin/json_merge.py "${SE_NODE_STEREOTYPE}" "${SE_NODE_STEREOTYPE_EXTRA}")"
				if [[ $? -ne 0 ]]; then
					echo "Failed to merge SE_NODE_STEREOTYPE_EXTRA for $browser_name. Please check the format of the JSON string. Keep using main stereotype."
				else
					echo "Merged stereotype for $browser_name: ${SE_NODE_STEREOTYPE}"
				fi
			fi

			# 'stereotype' setting is mandatory
			if [[ -n "${SE_NODE_STEREOTYPE}" ]]; then
				echo "[[node.driver-configuration]]" >>"$FILENAME"
				echo "display-name = \"${SE_NODE_BROWSER_NAME}\"" >>"$FILENAME"
				echo "stereotype = '${SE_NODE_STEREOTYPE}'" >>"$FILENAME"
				# Validate SE_NODE_MAX_SESSIONS is a positive integer
				if [[ "${SE_NODE_MAX_SESSIONS}" =~ ^[0-9]+$ ]] && [[ "${SE_NODE_MAX_SESSIONS}" -gt 0 ]]; then
					echo "max-sessions = ${SE_NODE_MAX_SESSIONS}" >>"$FILENAME"
				fi
			fi

			# Restore original environment variables for next browser iteration
			restore_original_env_vars
		fi
	done
fi
Test Coverage

Mappings added for new 'NodeAll*' and 'StandaloneAll*' variants but no new tests were added; ensure CI actually exercises all-browsers images on both platforms and covers Edge on amd64 only.

IMAGE_NAME_MAP = {
    # Hub
    'Hub': 'hub',
    # Chrome Images
    'NodeChrome': 'node-chrome',
    'NodeAllChrome': 'node-all-browsers',
    'StandaloneChrome': 'standalone-chrome',
    'StandaloneAllChrome': 'standalone-all-browsers',
    # Edge Images
    'NodeEdge': 'node-edge',
    'NodeAllEdge': 'node-all-browsers',
    'StandaloneEdge': 'standalone-edge',
    'StandaloneAllEdge': 'standalone-all-browsers',
    # Firefox Images
    'NodeFirefox': 'node-firefox',
    'NodeAllFirefox': 'node-all-browsers',
    'StandaloneFirefox': 'standalone-firefox',
    'StandaloneAllFirefox': 'standalone-all-browsers',
    # Chromium Images
    'NodeChromium': 'node-chromium',
    'StandaloneChromium': 'standalone-chromium',
}

TEST_NAME_MAP = {
    "Android": "ChromeTests",
    # Chrome Images
    'NodeChrome': 'ChromeTests',
    'NodeAllChrome': 'ChromeTests',
    'StandaloneChrome': 'ChromeTests',
    'StandaloneAllChrome': 'ChromeTests',
    # Edge Images
    'NodeEdge': 'EdgeTests',
    'NodeAllEdge': 'EdgeTests',
    'StandaloneEdge': 'EdgeTests',
    'StandaloneAllEdge': 'EdgeTests',
    # Firefox Images
    'NodeFirefox': 'FirefoxTests',
    'NodeAllFirefox': 'FirefoxTests',
    'StandaloneFirefox': 'FirefoxTests',
    'StandaloneAllFirefox': 'FirefoxTests',
    # Chromium Images
    'NodeChromium': 'ChromeTests',
    'StandaloneChromium': 'ChromeTests',
    # Chart Parallel Test

Copy link
Contributor

qodo-merge-pro bot commented Sep 2, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Restore node-level max-sessions

The new config generator omits the [node] max-sessions value and only sets
per-driver max-sessions, which changes Grid scheduling semantics and can break
override-max-sessions behavior (potentially overcommitting the container).
Reintroduce node-level "max-sessions = ${SE_NODE_MAX_SESSIONS}" in the [node]
section, and keep per-driver limits optional or derived, so total concurrency is
correctly capped across multiple browsers. This preserves existing behavior and
provides predictable resource control for the new multi-browser images.

Examples:

NodeBase/generate_config [117-173]
echo "override-max-sessions = ${SE_NODE_OVERRIDE_MAX_SESSIONS}" >>"$FILENAME"
echo "detect-drivers = false" >>"$FILENAME"
echo "drain-after-session-count = ${DRAIN_AFTER_SESSION_COUNT:-$SE_DRAIN_AFTER_SESSION_COUNT}" >>"$FILENAME"

# Check if /opt/selenium/browsers directory exists and iterate through browser folders
if [ -d "/opt/selenium/browsers" ]; then
	# Backup original environment variables before processing browsers
	backup_original_env_vars

	for browser_dir in /opt/selenium/browsers/*/; do

 ... (clipped 47 lines)

Solution Walkthrough:

Before:

# In NodeBase/generate_config

# ... [node] section is written, but max-sessions is omitted
echo "override-max-sessions = ${SE_NODE_OVERRIDE_MAX_SESSIONS}" >>"$FILENAME"
echo "detect-drivers = false" >>"$FILENAME"
# ... no "max-sessions" here

# Loop through browsers
for browser_dir in /opt/selenium/browsers/*/; do
  # ...
  # Assigns browser-specific env vars, e.g., SE_NODE_MAX_SESSIONS_CHROME
  assign_browser_specific_env_vars "$browser_name"
  # ...
  # Writes per-driver config
  echo "[[node.driver-configuration]]" >>"$FILENAME"
  # ...
  # This can lead to total sessions being sum of all browser max_sessions
  echo "max-sessions = ${SE_NODE_MAX_SESSIONS}" >>"$FILENAME"
  # ...
done

After:

# In NodeBase/generate_config

# ... [node] section is written
echo "override-max-sessions = ${SE_NODE_OVERRIDE_MAX_SESSIONS}" >>"$FILENAME"
echo "detect-drivers = false" >>"$FILENAME"
# Restore global max-sessions to cap total concurrency
echo "max-sessions = ${SE_NODE_MAX_SESSIONS}" >>"$FILENAME"

# Loop through browsers
for browser_dir in /opt/selenium/browsers/*/; do
  # ...
  # Per-driver max-sessions can still be configured, but within the global limit
  assign_browser_specific_env_vars "$browser_name"
  # ...
  echo "[[node.driver-configuration]]" >>"$FILENAME"
  # ...
  # Optionally set per-driver max-sessions if needed
  if [[ "${SE_NODE_MAX_SESSIONS}" =~ ^[0-9]+$ ]]; then
    echo "max-sessions = ${SE_NODE_MAX_SESSIONS}" >>"$FILENAME"
  fi
  # ...
done
Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies a critical regression where removing the node-level max-sessions can lead to unintentional resource over-commitment in multi-browser containers, which is a significant flaw in the new design.

High
General
Add fallback for invalid max-sessions

The validation only writes max-sessions when valid, but doesn't provide a
fallback value when invalid. This could result in missing configuration that
might cause runtime issues.

NodeBase/generate_config [169-172]

 # Validate SE_NODE_MAX_SESSIONS is a positive integer
 if [[ "${SE_NODE_MAX_SESSIONS}" =~ ^[0-9]+$ ]] && [[ "${SE_NODE_MAX_SESSIONS}" -gt 0 ]]; then
 	echo "max-sessions = ${SE_NODE_MAX_SESSIONS}" >>"$FILENAME"
+else
+	echo "max-sessions = 1" >>"$FILENAME"
 fi
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that an invalid SE_NODE_MAX_SESSIONS value would result in a missing max-sessions configuration, and proposes a reasonable default to improve robustness.

Low
Add validation for required browser files

The script doesn't handle the case where browser directories exist but required
files are missing. This could lead to incomplete browser configurations being
written to the config file.

NodeBase/generate_config [126-136]

 for browser_dir in /opt/selenium/browsers/*/; do
 	if [ -d "$browser_dir" ]; then
 		browser_name=$(basename "$browser_dir" | tr '[:lower:]' '[:upper:]')
 		echo "Processing browser: $browser_name"
 
+		# Skip if required files are missing
+		if [ ! -f "${browser_dir}name" ]; then
+			echo "Warning: Missing name file for browser $browser_name, skipping"
+			continue
+		fi
+
 		# Assign browser-specific environment variables to common variables
 		assign_browser_specific_env_vars "$browser_name"
 
-		if [ -f "${browser_dir}name" ]; then
-			SE_NODE_BROWSER_NAME=$(cat "${browser_dir}name")
-		fi
+		SE_NODE_BROWSER_NAME=$(cat "${browser_dir}name")
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly points out a potential issue where a browser directory might lack necessary files, and adding a check to skip such directories would make the script more resilient.

Low
  • Update

@VietND96 VietND96 force-pushed the container-all-browsers branch 2 times, most recently from 6681224 to 53ae08a Compare September 2, 2025 21:04
@VietND96 VietND96 force-pushed the container-all-browsers branch from 53ae08a to 8df8298 Compare September 2, 2025 22:48
@VietND96 VietND96 merged commit 1af2496 into trunk Sep 3, 2025
53 of 55 checks passed
@VietND96 VietND96 deleted the container-all-browsers branch September 3, 2025 00:15
dnegreira added a commit to wolfi-dev/os that referenced this pull request Sep 10, 2025
Upstream info:
From image tag 4.35.0 onwards, a single Node/Standalone image is
available with all browsers are pre-installed. Those images are
selenium/standalone-all-browsers (standalone all in one),
selenium/node-all-browsers (for Hub-Node mode).
SeleniumHQ/docker-selenium#2942

Signed-off-by: David Negreira <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[🚀 Feature]: Build "magic node" with all popular browsers in a single container
1 participant