View all services
Talk to QA Advisor
Browse the Knowledge Hub211 resources
Tools Library · We run this

Docker review for testing: pricing, alternatives and setup

Docker packages an application and everything it needs into an image, and runs that image as an isolated container on any machine with a container engine. For testing it is the standard way to pin browsers, drivers and dependencies together, start databases and mock services on demand, and make a CI run behave the same way as a run on a laptop.

QAble runs this tool on client engagements. The assessment below is from that experience.

last reviewed
13 October 2026
version covered
Docker Engine 29, Docker Desktop 4.95
licence
Freemium

Reviewed by Viral Patel

Co-Founder, QAbleLast reviewed 13 October 2026

At a glance

Docker at a glance

Official site
Vendor
Docker Inc.
Category
Containers
Licence
Freemium (Apache 2.0 for Docker Engine, CLI and Compose; Docker Desktop needs a paid subscription in larger organisations)
Platforms
Linux, Windows, macOS
Integrates with
Kubernetes, Jenkins, GitHub Actions, Testcontainers, Selenium Grid
First released
2013
Version reviewed
Docker Engine 29, Docker Desktop 4.95

Verdict

Our verdict on Docker

Strengths

  • One Compose file starts the app, database, mocks and browser, so every machine runs the same environment
  • Full image tags pin browser, driver and Grid versions together, so browser upgrades arrive as reviewed changes
  • Health checks plus docker compose up --wait hold the suite until dependencies are actually ready
  • Engine, CLI and Compose are Apache 2.0, so Linux servers and CI runners need no subscription
  • Official or vendor images exist for Selenium Grid, Playwright, PostgreSQL and most services a suite needs
  • Testcontainers, GitHub Actions service containers and Kubernetes all run the same images

Weaknesses

  • Docker Desktop needs a paid plan unless the organisation is under 250 employees and $10 million revenue
  • On macOS and Windows everything runs in a Linux VM, so bind mounts are slower and memory is capped
  • Anonymous Docker Hub pulls are limited to 100 per six hours per IP address
  • The docker group is root-equivalent on the host, and containers are not a security boundary
  • No GUI desktop sessions and no meaningful native mobile device testing

Best for

Making the test environment a versioned file rather than a wiki page. A Compose file that starts the application, its database, a mock service and a pinned browser lets a new tester run the whole suite on day one, and makes CI behave like a laptop. For browser automation in particular, an image with the browser and its driver pinned together removes the classic overnight failure where a browser auto-update breaks every session. We reach for it whenever a test environment has to be rebuilt more than once.

Not for

Anything that needs a real desktop session, such as UFT or other Windows GUI automation, and native mobile testing, which needs real devices, macOS for iOS simulators or hardware virtualisation for Android emulators. It is also the wrong tool for isolating untrusted code: containers share the host kernel, and membership of the docker group is equivalent to root on the host. If the requirement is many environments across many machines, Docker is the building block, not the answer; that is a job for Kubernetes or a managed service.

Setup cost

Low to consume and moderate to produce. Installing Docker Desktop or Docker Engine and running a published image such as selenium/standalone-chrome takes minutes. A Compose file with proper health checks, so the suite waits for real readiness instead of sleeping, takes an afternoon. A lean, cache-friendly, pinned test image is a real skill, and the first attempt is usually far larger and slower to build than it needs to be. In a larger organisation, settle the Desktop licence question before anyone installs it.

Maintenance burden

Mostly version hygiene. Base images need rebuilding for security fixes, browser images need bumping deliberately rather than through floating tags, and Compose files drift from the real environment unless someone owns them. Disk on laptops and self-hosted runners fills quietly with old images and build cache, so prune on a schedule. Engine majors change defaults: Engine 29 made the containerd image store the default for fresh installs and removed Docker Content Trust from the CLI, so read the release notes before upgrading runners.

Honest limitations

Docker Desktop is free only for personal use, education, non-commercial open source and small businesses with fewer than 250 employees and under $10 million in annual revenue; everyone else needs a paid Pro, Team or Business subscription. On macOS and Windows, containers run inside a Linux virtual machine, so bind-mounted files are slower and memory is whatever the VM was given. Anonymous Docker Hub pulls are rate limited per IP, which shared CI runners hit. And containers isolate processes; they are not a security boundary.

How it works

How Docker works

Images are layers, containers are processes

An image is a stack of read-only filesystem layers built from a Dockerfile, one layer per instruction. A container is an ordinary process on the host, given its own view of the filesystem, network and process tree through Linux namespaces, and limited by cgroups. Nothing is emulated, which is why a container starts as fast as the process inside it. Layers are cached and shared: when you rebuild, Docker reuses every layer up to the first instruction whose inputs changed and rebuilds from there. That single rule explains most slow test-image builds, where copying the whole repository before installing dependencies forces a full dependency install on every code change.

A client, a daemon and containerd

The docker command is only a client. It sends API calls to the Docker daemon, dockerd, which manages images, networks and volumes and hands the running of containers to containerd and runc. Because everything goes through that API, the same CLI can drive a remote engine through DOCKER_HOST, and libraries such as Testcontainers can start containers from inside a test. It is also why access to the daemon socket, or membership of the docker group, gives root-level control of the host. Since Engine 29.0, released on 10 November 2025, fresh installs use the containerd image store by default instead of the classic image store.

On macOS and Windows, a Linux VM sits in the middle

Linux containers need a Linux kernel. Docker Desktop on macOS and Windows runs a Linux virtual machine, on Windows through WSL 2 or Hyper-V, and the containers run inside it. Bind mounts cross the VM boundary through a file-sharing layer, which is why a suite mounted from the host runs slower than on Linux, and why Docker sells Synchronized File Shares on its paid plans. CPU and memory are what the VM was allocated, not the whole machine, so a Grid with several Chrome nodes can starve on a laptop with plenty of RAM. Linux CI runners have no VM in the way, which is one reason CI can outrun a developer’s Mac.

Compose turns containers into a fixture

Compose reads a YAML file of services, networks and volumes and creates them as one project, with a private network on which services reach each other by name. depends_on with condition: service_healthy makes a service wait for another’s health check to pass, and docker compose up --wait returns only when services are running and healthy. Those two features are what turn a pile of containers into a test fixture you can depend on. Compose is now a CLI plugin invoked as docker compose, at version 5.6 on 13 October 2026; the old Python docker-compose stopped receiving updates in July 2023, and the top-level version key is informative only.

Strengths

What it does well

Pinned, disposable browsers

The official Selenium images bundle a browser, its driver and Grid under one tag, such as selenium/standalone-chrome:4.49.0-20260909, and Microsoft publishes a Playwright image for every release. Pinning the full tag turns a browser upgrade into a reviewed change in a pull request rather than a surprise, and a failing run from months ago can be reproduced with exactly the same browser.

Real dependencies on demand

PostgreSQL, Redis, Kafka, WireMock and most services a suite talks to have official or vendor images. Starting them per run gives each job a clean database instead of a shared environment someone else has dirtied, and lets integration tests run on a laptop with nothing installed except Docker.

Readiness you can wait on

A healthcheck runs a command inside the container on an interval and marks it healthy or unhealthy. Compose can hold dependent services until a dependency is healthy, and docker compose up --wait blocks the calling script until everything is. That replaces the sleep 30 at the top of so many CI scripts, which is either too short on a slow runner or wasted time on a fast one.

Build caching that keeps test images cheap

BuildKit caches each layer and supports cache mounts, so a dependency install reuses its package cache between builds. In CI the cache can live in an external backend, including the GitHub Actions cache through docker/build-push-action, so a pipeline that changes only test code rebuilds in seconds rather than reinstalling every dependency.

Networks shaped like the real topology

User-defined networks give containers DNS by service name, so the test runner calls http://app:3000 exactly as one service calls another in production. Docker Desktop provides host.docker.internal for reaching the host, and on Linux the host-gateway value does the same through extra_hosts or --add-host. Running a service on a separate network is also a cheap way to test what happens when a dependency is unreachable.

The same image everywhere

An image built once runs unchanged under Docker on a laptop, as a service container in GitHub Actions, inside Testcontainers in a JUnit test and as a pod in Kubernetes, because they all use the OCI image format. The test image you debug locally is the one CI runs, which removes a whole category of works-on-my-machine arguments.

Pricing

Docker pricing and licensing

Docker Engine, the CLI and Compose are open source under Apache 2.0 and cost nothing on Linux servers and CI runners. Docker Desktop, the packaged app for macOS, Windows and Linux desktops, is free for personal use, education, non-commercial open source and small businesses, and otherwise needs a paid subscription per user. The plans are Personal (free), Pro, Team and Business, and they also set Docker Hub pull limits, private repositories, Docker Scout coverage and Testcontainers Cloud minutes.

  • Licence: Apache 2.0 for Docker Engine (the Moby project), the Docker CLI and Compose. Docker states that the Desktop subscription terms do not change the licensing of these open-source projects.
  • Docker Desktop free use: under Docker’s subscription agreement, free for small businesses (fewer than 250 employees and less than $10 million in annual revenue), personal use, education and non-commercial open source. Larger organisations and government entities need Pro, Team or Business.
  • Plans: Personal is free for one user. Pro is for individuals, Team covers up to 100 users with role-based access and audit logs, and Business adds SSO, SCIM, Image and Registry Access Management and Enhanced Container Isolation. List prices on docker.com on 13 October 2026 were $9, $15 and $24 per user per month billed annually.
  • Docker Hub: anonymous pulls are limited to 100 per six hours per IPv4 address or IPv6 /64 subnet, a signed-in Personal account gets a higher allowance, and Pro, Team and Business pulls are unlimited.
  • Testcontainers Cloud: Pro, Team and Business include 100, 500 and 1,500 runtime minutes a month, with more available to buy; Personal gets a trial only.
  • Hidden costs: CI minutes spent pulling and building images, registry storage, disk on self-hosted runners, and the engineering time to keep base images patched and pinned versions current.

Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.

Getting started

Docker tutorial: getting started

Setup, step by step

  1. On macOS or Windows, check the Desktop terms for your organisation, then install Docker Desktop; on Linux, install Docker Engine from Docker’s package repository.
  2. Run docker run hello-world to confirm the engine works.
  3. Sign in with docker login using a Docker Hub access token, so pulls count against your account rather than your IP address.
  4. Save the compose.yaml below and run docker compose up --wait; it returns once the database and browser report healthy.
  5. Point an existing WebDriver test at http://localhost:4444 and watch the browser at http://localhost:7900 (the password is secret).
  6. Run docker compose down -v when you finish, so the next run starts from a clean database.

Install

# Ubuntu: Docker’s convenience script, for development and test machines only
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo docker run hello-world
# macOS and Windows: install Docker Desktop from docker.com

A first example

# compose.yaml: a pinned browser and a throwaway database for the suite
services:
  db:
    image: postgres:18
    environment:
      POSTGRES_PASSWORD: test
      POSTGRES_DB: shop_test
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d shop_test"]
      interval: 2s
      timeout: 5s
      retries: 30

  chrome:
    # The full tag pins Chrome, ChromeDriver and Grid together
    image: selenium/standalone-chrome:4.49.0-20260909
    shm_size: 2gb          # Chrome crashes with Docker's small default /dev/shm
    ports:
      - "4444:4444"        # WebDriver endpoint for your tests
      - "7900:7900"        # noVNC: watch the browser in a web page
    healthcheck:
      # Script shipped in the Selenium images; healthy once Grid is ready
      test: ["CMD", "/opt/bin/check-grid.sh", "--host", "0.0.0.0", "--port", "4444"]
      interval: 5s
      timeout: 30s
      retries: 10

# docker compose up --wait   returns when both services are healthy
# docker compose down -v     removes containers, network and volumes

What trips people up first

Working out what localhost means. On the host, a published port such as 4444 is reachable on localhost. Inside a container, localhost is the container itself, so a browser in the chrome container that opens http://localhost:3000 looks for your app inside the browser container and fails. Either run the app as a Compose service and use its service name (http://app:3000), or, when the app runs on the host, use host.docker.internal on Docker Desktop and add extra_hosts with host-gateway on Linux. Most first-day “the page will not load” problems are this, not a firewall.

In practice

Using Docker in practice

Building a test image that rebuilds in seconds

Keep test infrastructure next to the code it tests: compose.yaml at the repository root, a Dockerfile.e2e for the suite and a .dockerignore that keeps node_modules, reports and .git out of the build context. Order the Dockerfile from least to most frequently changed, so the dependency layer is reused until the lockfile changes and only the final copy of test code reruns. For Playwright, start from Microsoft’s image, which already contains the browsers and their system libraries, and keep its tag equal to the @playwright/test version in your lockfile: the Playwright docs warn that a mismatch leaves it unable to find the browser executables. Pin every tag in full, and treat the bump as a pull request with its own test run.

# Dockerfile.e2e: the suite and its browsers in one image
# Keep this tag equal to @playwright/test in package-lock.json
FROM mcr.microsoft.com/playwright:v1.64.0-noble
WORKDIR /tests

# Dependency layer: rebuilt only when the lockfile changes
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci

# Test code last, because it changes most often
COPY . .
CMD ["npx", "playwright", "test"]

Running the suite as a Compose service

Putting the test runner on the same Compose network as the application removes the localhost problem and makes the run identical on every machine. A profile keeps the suite out of a plain docker compose up, so developers can start the environment alone, and docker compose run starts the services the runner depends on and returns the runner’s exit code, which is what CI needs to pass or fail the job. The Playwright documentation recommends --ipc=host for Chromium and --init to avoid zombie processes, and Compose exposes both as service keys. Mount the report folder back to the host so evidence survives the container.

# compose.yaml (continued): the app and the suite on one network
services:
  app:
    build: .
    environment:
      DATABASE_URL: postgres://postgres:test@db:5432/shop_test
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      # Assumes the app image includes curl and a /health endpoint
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 3s
      retries: 20

  e2e:
    build:
      context: .
      dockerfile: Dockerfile.e2e
    profiles: ["test"]      # not started by a plain docker compose up
    ipc: host               # recommended for Chromium by the Playwright docs
    init: true
    environment:
      BASE_URL: http://app:3000   # the service name, not localhost
    depends_on:
      app:
        condition: service_healthy
    volumes:
      - ./playwright-report:/tests/playwright-report

# Run the suite; the exit code is the suite's exit code:
# docker compose --profile test run --rm e2e

Running Compose environments in GitHub Actions

GitHub-hosted Ubuntu runners come with Docker and the Compose plugin, so the pipeline is the same commands you run locally. Sign in to Docker Hub first: hosted runners share IP addresses, and the anonymous limit of 100 pulls per six hours is reached sooner than you would expect. Start the environment with --wait and a timeout, so a dependency that never becomes healthy fails the job quickly with a clear message instead of a cascade of test failures. On failure, save the service logs alongside the test report, because the cause is often in the application container rather than in the test output. Always tear down, so a self-hosted runner does not accumulate stale volumes.

# .github/workflows/e2e.yml
name: e2e
on: [pull_request]
jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      # Authenticated pulls avoid the anonymous per-IP limit
      - uses: docker/login-action@v4
        with:
          username: ${{ vars.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Start the environment and wait for health checks
        run: docker compose up --wait --wait-timeout 180
      - name: Run the suite
        run: docker compose --profile test run --rm e2e
      - name: Save service logs
        if: failure()
        run: docker compose logs --no-color > compose.log
      - uses: actions/upload-artifact@v7
        if: failure()
        with:
          name: e2e-evidence
          path: |
            playwright-report
            compose.log
      - name: Tear down
        if: always()
        run: docker compose down -v

Debugging a failing containerised run

Start with state, then logs, then the inside of the container. docker compose ps shows which services are running and whether each one is healthy; a service stuck in starting usually means its health check command is wrong rather than the service being broken. docker inspect shows the last few health check results with their output. docker compose logs with a service name and --since narrows the noise to the failure window. docker compose exec opens a shell inside a running service, which is the quickest way to check DNS, environment variables and whether a port is really listening. For browser tests on the Selenium images, port 7900 serves a noVNC view of the live browser, which shows what the test saw without adding a single screenshot call.

# Which services are up, and are they healthy?
docker compose ps

# The last health check results, including the command output
docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q app)"

# Logs for one service around the failure
docker compose logs app --since 10m

# The environment the service actually received
docker compose exec app sh -c 'env | sort'

# An interactive shell inside the running service
docker compose exec app sh

Keeping images patched and machines clean

Images age even when nobody touches them, because their base layers accumulate security fixes upstream. Rebuild test images on a schedule as well as on change, and let Dependabot or a similar bot propose base image updates as pull requests, which keeps the pinning discipline without the manual chore. Run tests as a non-root user where the image supports it, and never mount the Docker socket into a test container unless the tool genuinely needs it, as Testcontainers does when run inside a container. On self-hosted runners and developer machines, dangling images and build cache grow until a disk fills mid-pipeline: docker system df shows where the space has gone, and a scheduled docker system prune with a filter on age keeps it under control.

Pitfalls

Common pitfalls

Browser images on latest or a major-only tag

Why it happens: A floating tag pulls a new browser and driver whenever the image is republished, so a suite that passed yesterday fails today with nothing in your repository changed, and the failure cannot be reproduced later.

The fix: Pin the full tag, such as selenium/standalone-chrome:4.49.0-20260909, or the image digest. Upgrade by pull request, so the new browser gets its own test run before it reaches everyone.

Chrome crashing with tab or page crashed errors in containers

Why it happens: Chromium uses /dev/shm heavily, and Docker’s default shared memory size is too small for it, so pages crash under load in ways that look like application bugs.

The fix: Set shm_size: 2gb, or --shm-size=2g, for Selenium images, as their documentation instructs. For Playwright images, follow its docs and use ipc: host, plus init: true to reap child processes.

Tests starting before dependencies are ready

Why it happens: The short form of depends_on waits only for the dependency’s container to start, not for the database to accept connections or the app to finish booting, so the first tests fail intermittently.

The fix: Give each dependency a healthcheck, use the long form with condition: service_healthy, and start environments with docker compose up --wait so scripts block until everything is healthy.

Following tutorials that use docker-compose and version keys

Why it happens: The Python docker-compose v1 stopped receiving updates in July 2023 and GitHub removed it from hosted runner images in July 2024, so pipelines copied from older posts fail with command not found. The version key now only triggers a warning, and Engine 29 removed Docker Content Trust from the CLI.

The fix: Use docker compose with a space, delete the version line, and replace DOCKER_CONTENT_TRUST-based signing with a current signing tool, or with the separate plugin Docker’s release notes mention.

CI failing on toomanyrequests from Docker Hub

Why it happens: Anonymous pulls are limited per IP address, and hosted or NAT-ed self-hosted runners share addresses, so the limit is hit by traffic you cannot see.

The fix: Run docker login with an access token at the start of every job, use a paid plan for unlimited pulls, or mirror the images you depend on into your own registry.

Installing Docker Desktop across a company without checking the terms

Why it happens: The engine is free and Desktop looks like a free download, so it spreads across laptops before anyone reads the subscription agreement, which requires a paid plan above 250 employees or $10 million in revenue.

The fix: Decide centrally: buy Pro, Team or Business seats, or standardise on Docker Engine on Linux and a licensed or open-source alternative on desktops, such as Podman Desktop.

Compared

Docker alternatives compared

ToolLicenceArchitectureDesktop appMulti-container definitionScopeTypical test role
DockerApache 2.0 engine; Desktop by subscriptionClient plus dockerd daemonDocker Desktop, paid in larger organisationsCompose, built inOne hostLocal and CI environments, test images
PodmanApache 2.0Daemonless, rootless by defaultPodman Desktop, Apache 2.0podman compose calls an external providerOne hostDocker-compatible alternative, especially on Linux
KubernetesApache 2.0Control plane schedules pods onto nodesNone of its ownPods, Deployments and Jobs in YAMLCluster of many machinesScaled grids, per-branch environments, load generators
TestcontainersMIT; Python port Apache 2.0Library calling a Docker-compatible APITestcontainers Desktop, freeContainers declared in test codeOne test runThrowaway dependencies inside integration tests

FAQ

Frequently asked questions

What are the best Docker alternatives?

The alternatives most often weighed against Docker are Podman, Kubernetes and Testcontainers. The comparison on this page sets them side by side on licence, architecture, desktop app, multi-container definition, scope and typical test role, and the verdict above says where Docker is the wrong choice and which of them to pick instead.

Is Docker free?

Docker Engine, the Docker CLI and Compose are free and open source under Apache 2.0, with no limit on use. Docker Desktop is free for personal use, education, non-commercial open source and small businesses with fewer than 250 employees and less than $10 million in annual revenue. Other organisations need a paid Pro, Team or Business subscription for every Desktop user. Docker Hub has a free Personal plan with pull limits.

Do CI runners need a Docker Desktop subscription?

Not for Docker Engine. Linux CI runners, including GitHub-hosted Ubuntu runners, use Docker Engine, which is Apache 2.0 open source, and Docker states that its Desktop terms do not change the licensing of the engine. The subscription question applies to Docker Desktop installations, which are usually developer laptops. A paid plan may still be worth it in CI for unlimited Docker Hub pulls.

Docker or Podman for test automation?

Either can run the same images. Docker has the larger ecosystem, Compose built in and the default support in tools such as Testcontainers. Podman runs without a daemon and rootless by default, has a free Apache 2.0 desktop app, and its command line is deliberately compatible. Podman’s compose command delegates to docker-compose or podman-compose, and Testcontainers documents extra settings for it, including disabling Ryuk in rootless mode.

Docker or Kubernetes for test environments?

Start with Docker and Compose. One machine running the application, its dependencies and the browsers covers most test environments, and the same file works on a laptop and in CI. Kubernetes becomes worth it when environments must exist per branch on shared infrastructure, a Selenium Grid must scale beyond one machine, or load generators must run across many nodes. Kubernetes runs the same images, so nothing built for Docker is wasted.

Can I run Selenium or Playwright tests in Docker?

Yes. Selenium publishes standalone, hub and node images with the browser and driver pinned together, so tests anywhere connect to port 4444 as a Remote WebDriver. Playwright publishes an image per release containing its browsers and system dependencies, so the suite itself runs in the container. Give Chrome enough shared memory with shm_size for Selenium, or ipc: host for Playwright, and pin full image tags.

Why does docker-compose say command not found?

Compose v1, the standalone Python docker-compose command, stopped receiving updates in July 2023 and is no longer shipped in new Docker Desktop releases; GitHub removed it from hosted runner images in July 2024. Compose v2 is a Docker CLI plugin, invoked as docker compose with a space, and is at version 5.6 as of October 2026. Existing compose files generally work unchanged, but delete the obsolete top-level version key.

What changed in Docker Engine 29?

Engine 29.0 was released on 10 November 2025, and the current release is 29.9.0, from 8 October 2026. Fresh installs now use the containerd image store by default, Docker Content Trust was removed from the CLI, cgroup v1 is deprecated with support promised until at least May 2029, and environment variables from legacy container links are no longer added automatically. Read the release notes before upgrading shared CI runners.

Is Docker worth learning for a tester?

Yes. Reading a Dockerfile, writing a Compose file and debugging a container are now basic skills for anyone automating tests, because that is how most teams pin browsers, start dependencies and make CI repeatable. The concepts carry straight over to Podman, Testcontainers, GitHub Actions service containers and Kubernetes, so the time is not tied to one product.

Alternatives

Docker alternatives worth weighing

Podman

Daemonless, rootless container engine with a Docker-compatible command line.

Kubernetes

Container orchestration platform, and the substrate most scaled test infrastructure now runs on.

Resources

Where to go next

Ready

Picking the wrong tool costs more than picking late.

QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.

Talk to QA Advisor