2

For the life of me I cannot figure out how to create a lightweight Linux container that uses Selenium WebDriver, Chrome, and NodeJS to execute tests against a website. I seem to keep running into permissions issues when starting ChromeDriver.

I am also looking to use a package that automatically updates ChromeDriver or - just as well - lock down Chrome from updating automatically.

I am not looking to run Selenium Grid - just need a simple, lightweight container.

Dockerfile

# Use alpine-based NodeJS base image
FROM node:latest

#USER root

# Install latest stable Chrome
# https://gerg.dev/2021/06/making-chromedriver-and-chrome-versions-match-in-a-docker-image/
RUN echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | \
    tee -a /etc/apt/sources.list.d/google.list && \
    wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | \
    apt-key add - && \
    apt-get update && \
    apt-get install -y google-chrome-stable libxss1

# Determine major browser version
ARG BROWSER_MAJOR=$(google-chrome --version | sed 's/Google Chrome \([0-9]*\).*/\1/g')

# Install the Chromedriver version that corresponds to the installed major Chrome version
# https://blogs.sap.com/2020/12/01/ui5-testing-how-to-handle-chromedriver-update-in-docker-image/
RUN google-chrome --version | grep -oE "[0-9]{1,10}.[0-9]{1,10}.[0-9]{1,10}" > /tmp/chromebrowser-main-version.txt
RUN wget --no-verbose -O /tmp/latest_chromedriver_version.txt https://chromedriver.storage.googleapis.com/LATEST_RELEASE_$(cat /tmp/chromebrowser-main-version.txt)
RUN wget --no-verbose -O /tmp/chromedriver_linux64.zip https://chromedriver.storage.googleapis.com/$(cat /tmp/latest_chromedriver_version.txt)/chromedriver_linux64.zip && rm -rf /opt/selenium/chromedriver && unzip /tmp/chromedriver_linux64.zip -d /opt/selenium && rm /tmp/chromedriver_linux64.zip && mv /opt/selenium/chromedriver /opt/selenium/chromedriver-$(cat /tmp/latest_chromedriver_version.txt) && chmod 755 /opt/selenium/chromedriver-$(cat /tmp/latest_chromedriver_version.txt) && ln -fs /opt/selenium/chromedriver-$(cat /tmp/latest_chromedriver_version.txt) /usr/bin/chromedriver

# Set the variable for the container working directory
ARG WORK_DIRECTORY=/program

# Create app directory
RUN mkdir -p $WORK_DIRECTORY
WORKDIR $WORK_DIRECTORY

# Install npm packages (do this AFTER setting the working directory)
RUN npm config set unsafe-perm true
RUN npm i selenium-webdriver
ENV NODE_ENV=development NODE_PATH=$WORK_DIRECTORY

# Copy script to execute to working directory
COPY runtest.js .

EXPOSE 8080

# Execute the script in NodeJS
CMD ["node", "runtest.js"]

runtest.js

const { Builder } = require('selenium-webdriver');
const { Options } = require('selenium-webdriver/chrome');

(async function hello() {
    //Browser Setup
    let builder = new Builder().forBrowser('chrome');
    let options = new Options();
    options.headless();                             // run headless Chrome
    options.excludeSwitches(['enable-logging']);    // disable 'DevTools listening on...'
    const driver = await builder.setChromeOptions(options).build();

    console.log('Hello World!');
})();

My current error when building and running this image is this:

/program/node_modules/selenium-webdriver/lib/error.js:522

    let err = new ctor(data.message)

              ^


WebDriverError: unknown error: Chrome failed to start: crashed.

  (unknown error: DevToolsActivePort file doesn't exist)

  (The process started from chrome location /usr/bin/google-chrome is no longer running, so ChromeDriver is assuming that Chrome has crashed.)

    at Object.throwDecodedError (/program/node_modules/selenium-webdriver/lib/error.js:522:15)

    at parseHttpResponse (/program/node_modules/selenium-webdriver/lib/http.js:548:13)

    at Executor.execute (/program/node_modules/selenium-webdriver/lib/http.js:474:28)
Simon
  • 1,630
  • 1
  • 17
  • 23
  • 1
    Hi Simon, why not using standalone chrome? I usually use two containers, one for selenium and another for a standalone chrome. You can then use the remote driver in your code instead of a local one. Hope that helps – M. Villanueva Feb 19 '22 at 22:16
  • Hi, I don't need to run against Selenium, I just need to use chromedriver to execute some automated tests. I'm trying to keep this setup very simple. Locally, it works fine, but when I package it in a container, I have issues with Chromedriver permissions. – Simon Feb 20 '22 at 01:17
  • 2
    Hi, @Simon can you provide the current docker solution you tried and your automated test code, so we identify the Chromedriver issue to be able to help – djmonki Feb 21 '22 at 07:13
  • Thank you for the assistance! Essentially, my Dockerfile aims to 1) use the latest NodeJS image, 2) install latest stable Chrome, 3) install latest corresponding chromedriver, 4) install npm packages, and 5) execute a simple script. I'll edit the post to show the files. – Simon Feb 22 '22 at 16:33

2 Answers2

1

Adding a --no-sandbox switch remedied the issue. I realize from a security perspective this is undesirable; however, I control the container and what happens with it, so I at least perceive this to be a reasonable path. If there are other suggestions to address the error in my original post, please let me know. If there is anything in the Dockerfile specifically that could be trimmed or modified to be better, please also advise.

To recap what I wanted to achieve:

  1. Start with a base NodeJS container. I selected the alpine-based node:latest image.
  2. Install latest stable Chrome.
  3. Install the Chromedriver version that corresponds to the installed major Chrome version.
  4. Install the necessary npm dependencies. In my case, that was just selenium-webdriver. I did so in my Dockerfile but if I had more dependencies, I'd set up a package.json file instead, then run an npm i in the Dockerfile.
  5. Execute a simple NodeJS-based Selenium test to navigate to a webpage, find an element, and log information about it to the console.

Dockerfile

# 1) Use alpine-based NodeJS base image
FROM node:latest

# 2) Install latest stable Chrome
# https://gerg.dev/2021/06/making-chromedriver-and-chrome-versions-match-in-a-docker-image/
RUN echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | \
    tee -a /etc/apt/sources.list.d/google.list && \
    wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | \
    apt-key add - && \
    apt-get update && \
    apt-get install -y google-chrome-stable libxss1

# 3) Install the Chromedriver version that corresponds to the installed major Chrome version
# https://blogs.sap.com/2020/12/01/ui5-testing-how-to-handle-chromedriver-update-in-docker-image/
RUN google-chrome --version | grep -oE "[0-9]{1,10}.[0-9]{1,10}.[0-9]{1,10}" > /tmp/chromebrowser-main-version.txt
RUN wget --no-verbose -O /tmp/latest_chromedriver_version.txt https://chromedriver.storage.googleapis.com/LATEST_RELEASE_$(cat /tmp/chromebrowser-main-version.txt)
RUN wget --no-verbose -O /tmp/chromedriver_linux64.zip https://chromedriver.storage.googleapis.com/$(cat /tmp/latest_chromedriver_version.txt)/chromedriver_linux64.zip && rm -rf /opt/selenium/chromedriver && unzip /tmp/chromedriver_linux64.zip -d /opt/selenium && rm /tmp/chromedriver_linux64.zip && mv /opt/selenium/chromedriver /opt/selenium/chromedriver-$(cat /tmp/latest_chromedriver_version.txt) && chmod 755 /opt/selenium/chromedriver-$(cat /tmp/latest_chromedriver_version.txt) && ln -fs /opt/selenium/chromedriver-$(cat /tmp/latest_chromedriver_version.txt) /usr/bin/chromedriver

# 4) Set the variable for the container working directory & create the working directory
ARG WORK_DIRECTORY=/program
RUN mkdir -p $WORK_DIRECTORY
WORKDIR $WORK_DIRECTORY

# 5) Install npm packages (do this AFTER setting the working directory)
RUN npm config set unsafe-perm true
RUN npm i selenium-webdriver
ENV NODE_ENV=development NODE_PATH=$WORK_DIRECTORY

# 6) Copy script to execute to working directory
COPY runtest.js .

EXPOSE 8080

# 7) Execute the script in NodeJS
CMD ["node", "runtest.js"]

runtest.js

const { Builder, By } = require('selenium-webdriver');
const { Options } = require('selenium-webdriver/chrome');

(async function hello() {
    let driver;

    try {
        //Browser Setup
        let builder = new Builder().forBrowser('chrome');
        let options = new Options();
        options.headless();                             // run headless Chrome
        options.excludeSwitches(['enable-logging']);    // disable 'DevTools listening on...'
        options.addArguments(['--no-sandbox']);         // not an advised flag but eliminates "DevToolsActivePort file doesn't exist" error.
        driver = await builder.setChromeOptions(options).build();

        // Navigate to Google and get the "Google Search" button text.
        await driver.get('https://www.google.com');
        const btnText = await driver.findElement(By.name('btnK')).getAttribute('value');
        console.log(`Google button text: ${btnText}`);
    } catch (e) {
        console.log(e);
    } finally {
        if (driver) {
            await driver.close();   // helps chromedriver shut down cleanly and delete the "scoped_dir" temp directories that eventually fill up the harddrive.
            await driver.quit();
        }
    }
})();

Executing docker build -t "node-chrome" -f Dockerfile .:

enter image description here

Running the image in Docker Desktop shows the button text:

enter image description here

This demonstrates this setup is working.

Simon
  • 1,630
  • 1
  • 17
  • 23
0

No so sandbox is considered unsafe. The workaround I found is running docker as non-root. So before CMD you should add

USER node

You then also have to run docker in privileged mode (--privileged)

LowMan
  • 21
  • 4