run Google Chrome in container (as a perfect headless browser)
Switch branches/tags
Nothing to show
Clone or download

google-chrome in docker

Need to test some latest Chrome version's features? but hestitant to upgrade your main browser to unstable? this chrome-in-docker project can help you



You may either just pull my prebuilt docker image at

$ docker pull c0b0/chrome-stable
$ docker run -it --rm c0b0/chrome-stable /opt/google/chrome/google-chrome --version
Google Chrome 52.0.2743.116

Or build it locally with Dockerfile here

$ docker build -t chrome-stable:20160813 .

Check what Chrome version is builtin, and tag a version:

$ docker run -it --rm chrome-stable:20160813 /opt/google/chrome/google-chrome --version
Google Chrome 52.0.2743.116
$ docker tag chrome-stable:20160813 chrome-stable:52.0.2743.116

The extra script here is to get latest versions of Chrome Stable, Beta, or Unstable version, for testing some latest features, here you may modify the Dockerfile to build a different image with each one, while, since the beta and unstable versions are changing fast, may be updating every week or every day, you don't have to rebuild docker images everyday, with this and local volume bind, you can run a different container with the same image; that way, within a relatively longer time range you don't have to rebuild the base docker image; the reasons of a same base image can be reused is dependencies of the different channels (stable, beta, or -dev) are most probably the same, or changing much less often; anyway, if there is any problem that stable can run but unstable cannot, you may always have a no-cache rebuild: by docker build --pull --no-cache ... to force pull latest ubuntu base and latest Chrome binary packages.

$ ./
[... downloading latest Chrome and extracting to ./opt ...]

You may test run it one time first to check what's exact version of each Chrome channel:

$ docker run -it --rm -v $PWD/opt:/opt:ro chrome:20160813 \
                         /opt/google/chrome-unstable/google-chrome-unstable --version
Google Chrome 54.0.2824.0 dev

$ docker run -it --rm -v $PWD/opt:/opt:ro chrome:20160813 \
                         /opt/google/chrome-beta/google-chrome-beta --version
Google Chrome 53.0.2785.57 beta

$ docker run -it --rm -v $PWD/opt:/opt:ro chrome:20160813 \
                         /opt/google/chrome/google-chrome --version
Google Chrome 52.0.2743.116

Then run 3 different containers with the same base docker image:

$ docker run -dt \
             --name Chrome-dev-54.0.2824.0 \
             -h chrome-dev-54.local \
             -v $PWD/opt:/opt:ro \
             -e CHROME=/opt/google/chrome-unstable/google-chrome-unstable \
$ docker run -dt \
             --name Chrome-beta-53.0.2785.57 \
             -h chrome-beta-53.local \
             -v $PWD/opt:/opt:ro \
             -e CHROME=/opt/google/chrome-beta/google-chrome-beta \
$ docker run -dt \
             --name Chrome-stable-52.0.2743.116 \
             -h chrome-beta-52.local \
             -v $PWD/opt:/opt:ro \

$ docker ps -a
35974a5247cf  chrome:20160813  "/"  ...                     Chrome-stable-52.0.2743.116
d5b784cbe9ac  chrome:20160813  "/"  ...                     Chrome-beta-53.0.2785.57
56417156ffea  chrome:20160813  "/"  ...                     Chrome-dev-54.0.2824.0

To connect the chrome in docker, you may either use port mappings, let it call proper iptables to set up proper mappings; or use inspect to find out the ip addresses of each container:

$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' Chrome-dev-54.0.2824.0

That means the chrome browser's Chrome Debugging Protocol can be accessed by

$ curl -s
   "Browser": "Chrome/54.0.2824.0",
   "Protocol-Version": "1.1",
   "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2824.0 Safari/537.36",
   "WebKit-Version": "537.36 (@facabd3224aecbcab4bea9daadad31c67488d78c)"

Or, if you use docker port mapping, like:

         #  this one is not using any local volume binding on /opt, so it's using the builtin Chrome at build time,
$ docker run -dt \
             --name Chrome-stable-builtin-52.0.2743.116 \
             -h chrome-stable-52.local \
             -p 9222:9222 \
$ docker ps -a
CONTAINER ID  IMAGE            COMMAND      CREATED        STATUS         PORTS                     NAMES
e9a3738f2d64  chrome:20160813  "/"  3 seconds ago  Up 3 seconds>9222/tcp   Chrome-stable-builtin-52.0.2743.116

        # by inspect we know we can access this container by
$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' Chrome-stable-builtin-52.0.2743.116
$ curl -s
   "Browser": "Chrome/52.0.2743.116",
   "Protocol-Version": "1.1",
   "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
   "WebKit-Version": "537.36 (@9115ecad1cae66fd5fe52bd9120af643384fd6f3)"
        # by above port mapping, this container can also be accessed by; if it's from localhost Linux, 
$ curl -s localhost:9222/json/version
   "Browser": "Chrome/52.0.2743.116",
   "Protocol-Version": "1.1",
   "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
   "WebKit-Version": "537.36 (@9115ecad1cae66fd5fe52bd9120af643384fd6f3)"

You may try with more har capturing commands like chrome-har-capturer -t urls...


VNC session listens default on the container's 5900 port, if you figured out the container's IP address (by above inspect command), an VNC session can be opened by your favorite VNC client connect to this ip address, or you may use another -p localport:5900 to set up another port forwarding to be able to use it from a 3rd computer.

Env variables to customize

  1. the default VNC password is hola; you may pass additional env var to docker run by -e VNC_PASSWORD=xxx to change to use a different VNC password;
  2. the default CHROME is /opt/google/chrome/google-chrome, if you use local volume bind to have different chrome versions, you may pass additional env var by -e CHROME=/path/to/chrome or chromium


Docker Image Build Time

  1. The Dockerfile defined process of where as start, it's starting from latest

  2. Ubuntu as base image, then install VNC and some network utilties like curl and socat, xvfb, x11vnc as Graphic layer for Chrome graphical output, xterm as debugging term window supervisor as processes manager, sudo also for debugging, not technically required.

  3. Then add Google-Chrome's apt source and install google-chrome-stable version, and it will handle all runtime dependencies by Chrome; This static version will be packed as part of the docker image, when you're not using local volume bind, this version will be used. It depends how often do you rebuild, but with above ./ script, you don't have to rebuild very often.

  4. Then add a regular user at 1000:100 for improved security and run all services under this regular user; sudo can be used for debugging. Copying supervisord.conf as definition of process structure; and as container entrypoint.

Container Spawn

At container spawn time (docker run ...), it starts from the entrypoint there it handles default VNC password hola, and check CHROME environment, set it default to the stable version /opt/google/chrome/google-chrome;

Then it exec to supervisord to spawn more processes defined in supervisord.conf

Process Management

Supervisord is the process manager, it spawns 4 processes:

  1. Xvfb ... as X server
  2. x11vnc ... as VNC on top of X Server
  3. fluxbox as window manager, this is technically not required, any X11 application can directly run on X server, but with a window manager, it's easier for debugging, when need to move window, resize, maximize, and minimize, etc.
  4. xterm, same for debugging
  5. start chrome from CHROME environment variable, with --remote-debugging-port=19222 to enable Remote Debugging Protocol
  6. socat, as a forwarding channel, chrome can only listen on local loopback interface (; hence not accepting any request from outside so a tcp forwarding tool like socat is necessary here.

Supervisord will respawn any managed processes if it crashed.

Ideally here should define dependencies between the processes, but due to it lacks such feature.

Some further improvements