Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: August 15, 2025
Browser automation is commonly integrated into DevOps pipelines for tasks like running tests, scraping dynamic content, or generating PDFs. In all of these cases, running Google Chrome in headless mode allows us to run browser actions without a visible interface, making automation both faster and resource-efficient.
However, getting Chrome up and running in a containerized environment like Docker isn’t the same as launching a typical CLI tool. Chrome comes with a list of system dependencies and behaves differently in sandboxed or minimal environments. This makes its setup in Docker trickier.
In this tutorial, we’ll walk through a practical approach to running Google Chrome in headless mode inside a Docker container. We’ll cover the base image selection, dependency setup, Dockerfile creation, and common gotchas. Regardless of whether we’re executing automated tests in CI/CD or just need a minimal browser instance, this tutorial is designed to make the task easy and reproducible.
Choosing a proper base image is an important choice when installing Chrome in a Docker container. Common choices include Debian-based images for compatibility and Alpine for lightweight needs. Preconfigured images, such as Chrome or Puppeteer, help avoid manual setup and configuration.
Here, to run Headless Chrome in Docker, we’ll use the official debian:bullseye image as our base. Debian provides reliable support for Chrome’s system dependencies, making it a solid foundation for browser automation tasks.
This tutorial assumes that Docker is already installed on our system and that the required base images have been pulled to proceed with the following steps.
To run Chrome headlessly in Docker, we require a suitable combination of system dependencies, fonts, a non-root user, and the correct Chrome flags. By making a custom Dockerfile, we can control what goes in so that the image is clean, predictable, and ready for automation tasks.
Let’s first create a basic Dockerfile using a Debian base and install Google Chrome with all required dependencies:
# Dockerfile
FROM debian:bullseye
# Install system dependencies, including ca-certificates
RUN apt-get update && apt-get install -y \
wget \
curl \
gnupg \
ca-certificates \
unzip \
fonts-liberation \
libappindicator3-1 \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libcups2 \
libdbus-1-3 \
libgdk-pixbuf2.0-0 \
libnspr4 \
libnss3 \
libx11-xcb1 \
libxcomposite1 \
libxdamage1 \
libxrandr2 \
xdg-utils \
--no-install-recommends && \
rm -rf /var/lib/apt/lists/*
# Install Google Chrome
RUN apt-get update && \
curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-linux-keyring.gpg && \
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-linux-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list && \
apt-get update && apt-get install -y google-chrome-stable && \
rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN useradd -m chromeuser
# Switch to non-root user
USER chromeuser
# Set Chrome as entrypoint with headless configuration
ENTRYPOINT ["google-chrome", "--headless", "--no-sandbox", "--disable-gpu", "--remote-debugging-port=9222", "--disable-dev-shm-usage"]
After saving the file, we can build the image and run it:
# Build the Docker image
$ docker build -t headless-chrome .
# Run the Docker container
$ docker run --rm headless-chrome
In the example, we start with a base of debian:bullseye. We install all the libraries that Chrome needs to run headless properly. We also include Google’s signing key and Chrome repository prior to installing google-chrome-stable. Then we create a non-root user, chromeuser, change to the user, and finally set the ENTRYPOINT with standard Chrome headless parameters.
The flags are commonly used with Chrome in Docker for better compatibility and debugging:
This setup gives us a reusable image to run Chrome headless anywhere, from local environments to CI pipelines.
Once we’ve built the image with Chrome installed, we can verify that it runs in headless mode inside the container. This confirms that all dependencies are installed and Chrome initializes without a screen. Grabbing a quick test, like taking a screenshot of a webpage, is an easy way of verifying.
To run the container and capture a screenshot from a website, we can use:
docker run --rm \
-v $PWD:/screenshots \
headless-chrome \
--headless \
--disable-gpu \
--no-sandbox \
--screenshot=/screenshots/example.png \
https://google.com
Here, we’re mounting the present directory to /screenshots using the -v flag so that Chrome can write the output file to our host. Then, inside the container, Chrome is started in headless mode. GPU acceleration is disabled by –disable-gpu, sandboxing is circumvented for –no-sandbox compatibility, and https://google.com is screenshot-ed into /screenshots/example.png. The image will be in our local working directory if all goes well.
This quick test shows that Chrome starts properly, loads pages, and interacts with the filesystem. We can now confidently integrate it into any automation or CI setup.
Now that Chrome is properly functional in a Docker container, the next logical step is to incorporate it into a CI/CD pipeline. This enables us to run tests, capture screenshots, or test frontends automatically on each deployment cycle. Most CI tools have native support for Docker, so integration is quite straightforward.
If we’re using GitHub Actions, we can define a workflow that pulls our image and runs a headless Chrome test. First, make sure Docker is available in the runner:
$ sudo apt update && sudo apt install -y docker.io
Then, let’s prepare a minimal example of a GitHub Actions workflow using our headless-chrome Docker image:
# .github/workflows/chrome-test.yml
name: Run Chrome Headless Test
on: [push]
jobs:
headless-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker
run: sudo apt-get update && sudo apt-get install -y docker.io
- name: Build Chrome Docker image
run: docker build -t headless-chrome .
- name: Run screenshot test
run: |
mkdir screenshots
docker run --rm \
-v ${{ github.workspace }}/screenshots:/screenshots \
headless-chrome \
--headless \
--disable-gpu \
--no-sandbox \
--screenshot=/screenshots/test.png \
https://google.com
- name: Upload screenshot artifact
uses: actions/upload-artifact@v3
with:
name: screenshot
path: screenshots/test.png
Here, we first check out the repo, then install Docker inside the GitHub runner. We then build our local headless-chrome image from our Dockerfile. Last but not least, we run a test that takes a screenshot of https://google.com and saves it to a common screenshots directory. This makes sure that Chrome initializes correctly in the CI environment and runs headless jobs as desired.
In this article, we explored running Google Chrome in headless mode inside a Docker container. With the right base image and setup, we can avoid common dependency issues and ensure Chrome runs smoothly in a containerized environment.
We also saw how this setup integrates seamlessly into CI/CD pipelines using tools like GitHub Actions. By running everything inside Docker, we minimize environmental differences between development and production. Whether we’re automating visual testing or validating frontend logic, this approach brings flexibility, portability, and efficiency to any DevOps workflow.
As always, the complete code is available over on GitHub.