|

Docker for QA Engineers: Everything You Need to Know (And Nothing You Don’t)

Every QA engineer has heard the phrase “it works on my machine.” Docker eliminates that problem entirely by packaging your application, its dependencies, and your test framework into portable, reproducible containers that behave identically on your laptop, your colleague’s laptop, and your CI server. But most Docker tutorials are written for DevOps engineers and system administrators, burying the QA-relevant information under layers of orchestration, networking, and deployment concepts you do not need. This guide covers exactly what QA engineers need to know about Docker and nothing more. You will learn to containerize your Playwright test suite, build full-stack test environments with Docker Compose, use Testcontainers for disposable databases, extract test artifacts from containers, and integrate everything into GitHub Actions.

🎭 Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.

Contents

What Containers Are: The QA Perspective

A container is a lightweight, isolated environment that packages an application and all its dependencies into a single unit that runs consistently everywhere. Think of it as a virtual machine without the overhead. Containers share the host operating system’s kernel but have their own filesystem, network, and process space. For QA engineers this means you can run your test suite inside a container and guarantee that it uses the exact same versions of Node.js, Playwright, browsers, and system libraries every time, regardless of what is installed on the host machine.

The key concepts you need are images and containers. An image is a read-only template that defines the filesystem and configuration for a container. A container is a running instance of an image. You build images from Dockerfiles, which are recipes that specify the base operating system, installed packages, copied files, and startup commands. Once built, you can run as many containers from the same image as you want, and each container is isolated from the others.

For QA purposes, containers solve several critical problems. Test environment consistency means that a test that passes in your container will pass in CI because both environments are identical. Disposable environments mean you can spin up a fresh database for each test run and tear it down afterward, eliminating state leakage between tests. Parallel isolation means you can run multiple test suites simultaneously in separate containers without port conflicts or resource contention.

Essential Docker Commands for QA Engineers

You do not need to memorize the entire Docker command reference. These twelve commands cover ninety percent of what QA engineers use daily. Each command is shown with the flags most relevant to testing scenarios.

# Build an image from a Dockerfile
docker build -t playwright-tests:latest .

# Run a container from an image
docker run --rm playwright-tests:latest

# Run with environment variables
docker run --rm -e BASE_URL=https://staging.example.com playwright-tests:latest

# Run with volume mount for test artifacts
docker run --rm -v $(pwd)/test-results:/app/test-results playwright-tests:latest

# List running containers
docker ps

# View container logs
docker logs <container-id>

# Execute a command in a running container
docker exec -it <container-id> /bin/bash

# Stop a running container
docker stop <container-id>

# Remove unused images to free disk space
docker image prune -a

# Start multi-container environment
docker compose up -d

# Stop and remove multi-container environment
docker compose down

# View resource usage
docker stats

Dockerfile for Playwright Tests

The Dockerfile is the recipe for building your test image. Microsoft provides official Playwright Docker images that include all browser binaries and system dependencies pre-installed, which saves you from dealing with browser installation issues. Here is a complete, production-ready Dockerfile for a Playwright test suite.

# Dockerfile
FROM mcr.microsoft.com/playwright:v1.48.0-jammy

# Set working directory
WORKDIR /app

# Copy package files first for better layer caching
COPY package.json package-lock.json ./

# Install dependencies (ci for deterministic installs)
RUN npm ci

# Copy test configuration files
COPY playwright.config.ts tsconfig.json ./

# Copy test source code
COPY src/ ./src/
COPY tests/ ./tests/
COPY fixtures/ ./fixtures/

# Create directory for test artifacts
RUN mkdir -p /app/test-results /app/playwright-report

# Set environment variables
ENV CI=true
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright

# Default command: run all tests
CMD ["npx", "playwright", "test", "--reporter=html"]

The layering strategy matters for build performance. By copying package.json and package-lock.json before the source code, Docker caches the npm ci layer. When you change only test code without modifying dependencies, the rebuild skips the expensive dependency installation step. This reduces build times from minutes to seconds during iterative test development.

Docker Compose for Full-Stack Test Environments

Docker Compose lets you define multi-container environments in a single YAML file. For integration testing and E2E testing, you typically need the application server, a database, possibly a cache layer, and the test runner. Docker Compose orchestrates all of these services, manages their networking, and handles startup ordering.

# docker-compose.yml
version: '3.9'

services:
  # PostgreSQL database
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: testuser
      POSTGRES_PASSWORD: testpass
      POSTGRES_DB: testdb
    ports:
      - "5432:5432"
    volumes:
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
      interval: 5s
      timeout: 5s
      retries: 5

  # Redis cache
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  # Application server
  app:
    build:
      context: .
      dockerfile: Dockerfile.app
    environment:
      DATABASE_URL: postgresql://testuser:testpass@postgres:5432/testdb
      REDIS_URL: redis://redis:6379
      NODE_ENV: test
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 10

  # Playwright test runner
  tests:
    build:
      context: .
      dockerfile: Dockerfile.tests
    environment:
      BASE_URL: http://app:3000
      DATABASE_URL: postgresql://testuser:testpass@postgres:5432/testdb
    volumes:
      - ./test-results:/app/test-results
      - ./playwright-report:/app/playwright-report
    depends_on:
      app:
        condition: service_healthy
    command: npx playwright test --reporter=html

The depends_on with condition: service_healthy ensures that tests only start after the database is accepting connections and the application is responding to health checks. The volume mounts for test-results and playwright-report make test artifacts available on the host machine after the container finishes, so you can review HTML reports, screenshots, and trace files without entering the container.

Running Tests in Docker: Same Environment as CI

The primary benefit of running tests in Docker is environment parity. When your CI pipeline runs tests inside the same Docker image you use locally, you eliminate an entire class of bugs caused by environment differences. The workflow is straightforward: build the image, run the tests, collect artifacts.

# Build and run tests with artifact extraction
docker compose up --build --exit-code-from tests

# The exit code from the tests service propagates to docker compose,
# so CI correctly detects pass/fail

# After tests complete, artifacts are in ./test-results/ and ./playwright-report/
# View the HTML report
npx playwright show-report ./playwright-report

The --exit-code-from tests flag tells Docker Compose to use the exit code from the tests service as its own exit code. This is critical for CI integration because it means your pipeline correctly marks the build as failed when tests fail. Without this flag, Docker Compose always exits with code zero if the containers started successfully, regardless of what happened inside them.

Testcontainers for Disposable Databases

Testcontainers is a library that lets you spin up Docker containers programmatically from your test code. Instead of maintaining a shared test database that accumulates state between test runs, you create a fresh database container for each test suite and destroy it afterward. This guarantees test isolation and eliminates flaky tests caused by leftover data.

// tests/fixtures/database.ts
import { test as base } from '@playwright/test';
import { GenericContainer, StartedTestContainer, Wait } from 'testcontainers';

type DatabaseFixture = {
  databaseUrl: string;
  container: StartedTestContainer;
};

export const test = base.extend<DatabaseFixture>({
  container: [async ({}, use) => {
    const container = await new GenericContainer('postgres:16-alpine')
      .withEnvironment({
        POSTGRES_USER: 'test',
        POSTGRES_PASSWORD: 'test',
        POSTGRES_DB: 'testdb',
      })
      .withExposedPorts(5432)
      .withWaitStrategy(Wait.forHealthCheck())
      .start();

    await use(container);
    await container.stop();
  }, { scope: 'worker' }],

  databaseUrl: async ({ container }, use) => {
    const host = container.getHost();
    const port = container.getMappedPort(5432);
    await use(`postgresql://test:test@${host}:${port}/testdb`);
  },
});

The scope: 'worker' option means the database container is shared across all tests in the same worker process, which balances isolation with performance. If you need complete isolation between individual tests, change the scope to 'test', but be aware that starting a new PostgreSQL container takes two to five seconds each time.

Volume Mounts for Test Artifacts

When tests run inside Docker containers, the artifacts they produce (screenshots, videos, trace files, HTML reports) exist only inside the container filesystem. Without volume mounts, these artifacts disappear when the container stops. Volume mounts bridge the container filesystem to the host filesystem, making artifacts persist after the container exits.

# Mount specific artifact directories
docker run --rm   -v $(pwd)/test-results:/app/test-results   -v $(pwd)/playwright-report:/app/playwright-report   -v $(pwd)/traces:/app/traces   playwright-tests:latest

# Or use docker compose volumes (defined in docker-compose.yml)
docker compose up tests

Volume mounts use the format host-path:container-path. The host path is the directory on your local machine where you want artifacts stored, and the container path is where the test framework writes artifacts inside the container. Make sure these paths match your Playwright configuration’s outputDir, reporter output path, and trace settings.

Docker in GitHub Actions

# .github/workflows/docker-tests.yml
name: Docker E2E Tests
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  e2e-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4

      - name: Build and run tests
        run: |
          docker compose up --build --exit-code-from tests --abort-on-container-exit

      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: |
            test-results/
            playwright-report/
          retention-days: 14

      - name: Upload traces on failure
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: failure-traces
          path: traces/
          retention-days: 7

      - name: Cleanup
        if: always()
        run: docker compose down -v --remove-orphans

The --abort-on-container-exit flag stops all services when any container exits, which prevents the CI job from hanging indefinitely when tests finish but the application container keeps running. The cleanup step with docker compose down -v removes containers, networks, and volumes to prevent disk space issues on self-hosted runners.

🚀 Level Up Your Playwright

From locators to CI pipelines — build a production-grade Playwright + TypeScript framework step by step.

Common Docker Debugging for QA

Docker introduces its own category of issues that QA engineers encounter regularly. Understanding these common problems and their solutions will save you hours of debugging time.

Port Conflicts

The most common issue is port conflicts. When you run docker compose up and see “port is already allocated,” it means another process or container is using the same port. Use docker ps to find running containers and lsof -i :3000 on macOS or Linux to find any process using the port. Either stop the conflicting process or change the port mapping in your compose file. You can map any container port to any host port using the host:container syntax in the ports section.

Permission Issues

Permission issues arise when the container process runs as a different user than the host filesystem owner. Test artifacts written by the container may be owned by root, making them unreadable or undeletable on the host. Fix this by adding a USER directive in your Dockerfile that matches your CI runner’s user, or use the --user flag with docker run. In GitHub Actions, the default runner user ID is 1001.

Memory Limits

Playwright tests with multiple browser contexts can consume significant memory. If your container runs out of memory, tests crash with cryptic error messages like “browser has been closed” or “target closed.” Set memory limits explicitly in your Docker Compose file using deploy.resources.limits.memory and monitor usage with docker stats. A good starting point is 2 gigabytes for a single-worker Playwright suite and 4 gigabytes for parallel execution with multiple workers.

Network Connectivity Between Containers

When your test container cannot reach the application container, verify that both services are on the same Docker network. In Docker Compose, all services defined in the same file share a default network. Use the service name as the hostname, not localhost. For example, if your app service is named app, your test container accesses it at http://app:3000, not http://localhost:3000. The localhost inside a container refers to the container itself, not the host machine or other containers.

Slow Builds and Layer Caching

If your Docker builds take too long, check your layer ordering. Put infrequently changing layers like base image selection and system package installation at the top of the Dockerfile, and frequently changing layers like test code copying at the bottom. Use a .dockerignore file to exclude node_modules, .git, test-results, and other large directories from the build context. Enable BuildKit with DOCKER_BUILDKIT=1 for more efficient caching and parallel builds.

Docker Compose with Playwright, Postgres, and Redis: Complete Setup

Here is the complete, copy-paste-ready setup that combines everything discussed in this guide. This setup provides a full-stack test environment with a PostgreSQL database, Redis cache, application server, and Playwright test runner, all orchestrated by Docker Compose.

# Project structure
project/
  Dockerfile.app          # Application Dockerfile
  Dockerfile.tests        # Test runner Dockerfile
  docker-compose.yml      # Multi-container orchestration
  docker-compose.ci.yml   # CI-specific overrides
  .dockerignore           # Exclude unnecessary files
  db/
    init.sql              # Database seed data
  tests/
    e2e/                  # End-to-end test files
  playwright.config.ts    # Playwright configuration
# .dockerignore
node_modules
.git
test-results
playwright-report
traces
.env
.env.local
*.log
.DS_Store

This setup ensures that every developer on your team and every CI run uses the exact same environment. No more debugging environment differences, no more “it works on my machine,” and no more flaky tests caused by leftover database state. Run docker compose up --build --exit-code-from tests and get consistent, reliable test results every time.

Best Practices for Docker in Test Automation

Pin your base image versions explicitly. Using playwright:latest means your builds may break unexpectedly when a new version is released. Use playwright:v1.48.0-jammy instead, and update the version deliberately when you are ready. Apply the same principle to PostgreSQL, Redis, and any other service images.

Use multi-stage builds when your test image includes build tools that are not needed at runtime. This keeps images smaller and faster to pull. Clean up after yourself in CI by running docker compose down -v and docker system prune -f to prevent disk space exhaustion on long-running runners. Tag your test images with the Git commit SHA for traceability, so you can always reproduce a specific test run.

Conclusion

Docker transforms test automation from a fragile, environment-dependent process into a portable, reproducible system. The QA-relevant subset of Docker is manageable and focused: Dockerfiles to package your tests, Docker Compose to orchestrate multi-service environments, volume mounts to extract artifacts, and Testcontainers for programmatic database management. You do not need to become a Kubernetes expert or learn container orchestration at scale. Master these fundamentals and you will eliminate environment-related test failures while making your CI pipeline faster and more reliable.

🎓 Master Playwright End to End

Join hundreds of SDETs building real automation frameworks. Lifetime access, hands-on projects, and a job-ready portfolio.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.