Skip to main content

Overview

Running browser automation in Docker has traditionally been challenging, especially with GPU acceleration. Zendriver includes first-class Docker support through the zendriver-docker project template, making deployment straightforward.
Zendriver’s Docker solution runs a real, GPU-accelerated browser (not headless) inside containers using Wayland and VNC for remote access.

Why Docker with Zendriver?

Production ready

Deploy browser automation reliably across different environments

GPU acceleration

Full hardware acceleration for realistic browser behavior

VNC debugging

See what the browser is doing in real-time via VNC

Isolation

Separate browser instances with clean profiles

Quick start

1

Clone the template

git clone https://github.com/cdpdriver/zendriver-docker.git
cd zendriver-docker
2

Configure render group

Find your render group GID (for GPU access):
stat -c "%g" /dev/dri/renderD128
Update docker-compose.yml with the GID:
environment:
  - RENDER_GROUP_GID=107  # Use your GID here
3

Build and run

docker compose build
docker compose up
4

Connect with VNC

Connect to localhost:5911 using any VNC client:
  • Username: wayvnc
  • Password: wayvnc
You’ll see the browser running in real-time!

Docker Compose configuration

Here’s the complete docker-compose.yml structure:
services:
  zendriver:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 5911:5911  # VNC server port
    volumes:
      - ./zendriver:/app/zendriver
      - ./scripts:/app/scripts
      - ./tests:/app/tests
    environment:
      # Render group for GPU access (required)
      - RENDER_GROUP_GID=107
      
      # Wayland/Sway configuration
      - SWAY_RESOLUTION=1920x1080
      
      # VNC settings
      - WAYVNC_PORT=5911
      - WAYVNC_ENABLE_AUTH=true
      - WAYVNC_USERNAME=wayvnc
      - WAYVNC_PASSWORD=wayvnc
      
      # Debug: Pause after tests for inspection
      - ZENDRIVER_PAUSE_AFTER_TEST=false
    
    # Required for GPU access
    privileged: true

volumes:
  wayvnc-certs:

Environment variables

RENDER_GROUP_GID
int
required
The GID of the group that owns /dev/dri/renderD128. Find it with:
stat -c "%g" /dev/dri/renderD128
SWAY_RESOLUTION
string
default:"1920x1080"
The resolution of the virtual display. Common values: 1920x1080, 1280x720, 2560x1440
WAYVNC_PORT
int
default:"5911"
The port where VNC server will listen
WAYVNC_ENABLE_AUTH
bool
default:"true"
Enable VNC authentication. Set to false for testing (not recommended in production)
WAYVNC_USERNAME
string
default:"wayvnc"
VNC authentication username
WAYVNC_PASSWORD
string
default:"wayvnc"
VNC authentication password. Change this in production!
ZENDRIVER_PAUSE_AFTER_TEST
bool
default:"false"
Pause after each test completes. Useful for debugging. Resume with Mod+Return in VNC.

Dockerfile structure

The Zendriver Dockerfile is based on a specialized Wayland+Chrome+VNC base image:
FROM ghcr.io/stephanlensky/swayvnc-chrome:latest

ENV PYTHONUNBUFFERED=1

# Install uv package manager
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy

# Create app directory
RUN mkdir /app
RUN chown $DOCKER_USER:$DOCKER_USER /app

# Switch to non-root user
USER $DOCKER_USER

# Configure sway hotkey for test debugging
RUN echo 'bindsym Mod4+Return exec /app/tests/next_test.sh' >> ~/.config/sway/config

WORKDIR /app

# Install Python 3.13
RUN uv python install 3.13

# Install dependencies
COPY --chown=$DOCKER_USER:$DOCKER_USER pyproject.toml uv.lock /app/
RUN --mount=type=cache,target=/home/$DOCKER_USER/.cache/uv,uid=$PUID,gid=$PGID \
    uv sync --frozen --no-install-project

# Copy and install project
COPY --chown=$DOCKER_USER:$DOCKER_USER . /app

ENV PATH="/app/.venv/bin:$PATH"

RUN --mount=type=cache,target=/home/$DOCKER_USER/.cache/uv,uid=$PUID,gid=$PGID \
    uv sync --frozen

USER root
ENTRYPOINT ["/entrypoint.sh", "./scripts/test.sh"]

Running Zendriver in Docker

Once your container is running, your Zendriver scripts work the same as locally:
# app.py
import asyncio
import zendriver as zd

async def main():
    browser = await zd.start()
    page = await browser.get("https://www.browserscan.net/bot-detection")
    await page.save_screenshot("/app/output/browserscan.png")
    await browser.stop()

if __name__ == "__main__":
    asyncio.run(main())
Run it in the container:
docker compose run zendriver python app.py

VNC debugging

Connect to the container with a VNC client to see the browser in action:
# Use built-in VNC client
open vnc://localhost:5911

# Or use RealVNC Viewer
# Download from: https://www.realvnc.com/en/connect/download/viewer/
Use VNC debugging during development to:
  • Verify challenges are being solved correctly
  • Debug element interactions
  • Check page rendering issues
  • Monitor browser behavior in real-time

Production considerations

Security

1

Change VNC password

Update WAYVNC_PASSWORD in docker-compose.yml or use secrets:
environment:
  - WAYVNC_PASSWORD_FILE=/run/secrets/vnc_password
secrets:
  - vnc_password

secrets:
  vnc_password:
    file: ./secrets/vnc_password.txt
2

Disable VNC in production

If you don’t need VNC access in production:
environment:
  - WAYVNC_ENABLE_AUTH=false
ports: []  # Remove port mapping
3

Use non-privileged mode

The container needs privileged: true for GPU access. In environments without GPU:
privileged: false
environment:
  - LIBGL_ALWAYS_SOFTWARE=1  # Software rendering

Resource limits

services:
  zendriver:
    # ... other config ...
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 4G
        reservations:
          cpus: '1.0'
          memory: 2G

Persistent storage

Mount volumes for persistent data:
volumes:
  - ./output:/app/output          # Screenshots, downloads
  - ./profiles:/app/profiles      # Browser profiles
  - ./logs:/app/logs             # Application logs

Multi-container deployments

Run multiple isolated browser instances:
services:
  zendriver-1:
    build: .
    ports:
      - "5911:5911"
    environment:
      - RENDER_GROUP_GID=107
      - INSTANCE_ID=1
    volumes:
      - ./output/instance1:/app/output
  
  zendriver-2:
    build: .
    ports:
      - "5912:5911"  # Different port
    environment:
      - RENDER_GROUP_GID=107
      - INSTANCE_ID=2
    volumes:
      - ./output/instance2:/app/output

Platform requirements

Linux only: Zendriver’s Docker solution currently only works on Linux hosts due to GPU passthrough requirements.macOS and Windows users can:
  • Use WSL2 (Windows) with GPU passthrough
  • Run without GPU acceleration (slower, more detectable)
  • Deploy to Linux servers for production

Checking GPU support

# Check if GPU is available
ls -la /dev/dri/

# Should show renderD128 or similar
crw-rw----+ 1 root render 226, 128 Nov 15 10:30 renderD128

Troubleshooting

Make sure RENDER_GROUP_GID matches your system:
stat -c "%g" /dev/dri/renderD128
Update the value in docker-compose.yml.
  • Check the container is running: docker compose ps
  • Verify port mapping: docker compose port zendriver 5911
  • Check firewall rules if connecting remotely
  • The Wayland session may not have started. Check logs:
    docker compose logs zendriver
    
  • Try restarting the container:
    docker compose restart zendriver
    
Check Chrome is installed in the container:
docker compose exec zendriver which google-chrome
If missing, the base image may be incorrect. Verify you’re using:
FROM ghcr.io/stephanlensky/swayvnc-chrome:latest

Example project structure

zendriver-docker/
├── docker-compose.yml
├── Dockerfile
├── pyproject.toml
├── uv.lock
├── app.py                 # Your Zendriver script
├── scripts/
│   └── test.sh           # Test runner
├── output/               # Volume for screenshots/downloads
├── logs/                 # Volume for logs
└── secrets/              # VNC credentials (git-ignored)
    └── vnc_password.txt

Further resources

zendriver-docker

Official Docker project template

swayvnc-chrome

Base Docker image with Wayland and Chrome

Next steps

Anti-detection

Configure anti-detection for production

Cloudflare bypass

Handle Cloudflare challenges in Docker

Build docs developers (and LLMs) love