You've written tests. Good tests — Playwright tests, Selenium tests, API tests with Supertest. They pass on your machine. Then CI runs them, and they fail. "Element not found." "Connection refused." "Timeout waiting for selector." You check the CI logs: the database wasn't seeded. The Selenium Grid was on a different version. A background service timed out starting up. And your interview panel — hearing you describe this — leans forward and asks: "How do you ensure deterministic test execution across environments? Walk me through your containerisation strategy. What's the difference between ENTRYPOINT and CMD — and which would you use for a test runner image? When would you use TestContainers instead of a shared Docker Compose stack? Design a multi-container test environment that spins up PostgreSQL, Redis, Selenium Grid, and your test suite — and explain how you ensure service readiness before the first test executes. When does Docker Compose suffice for test infrastructure, and when do you graduate to Kubernetes?" And suddenly you realise: you've been running tests, but you haven't been engineering the infrastructure they run on. You've been treating Docker as a deployment tool — not as a test reliability engineering tool. And in 2026, that gap costs candidates the offer.

Docker has moved from a nice-to-have to a foundational SDET skill. In 2026, interview panels expect you to containerise tests, compose multi-service test environments, use TestContainers for integration tests with real dependencies, and design container-based CI/CD pipelines that produce deterministic, reproducible results. Not because Docker is trendy — because tests that depend on the host environment are unreliable tests. A test that passes on your macOS machine but fails in CI's Ubuntu runner is a test whose failure costs engineer-hours diagnosing environmental drift. Docker eliminates that class of failure by making the test environment identical everywhere. And that's exactly the argument interview panels want to hear: not "I use Docker because everyone uses Docker," but "I use Docker because it makes my test suite deterministic across environments — and here's how."

This guide covers every Docker topic that SDET interview panels probe in 2026 — from the Dockerfile fundamentals that gate junior-to-mid transitions, to the TestContainers patterns that signal senior-level integration testing maturity, to the Docker-vs-Kubernetes infrastructure decisions that separate test engineers from test architects. Complement it with our CI/CD deep-dive at CI/CD Pipeline Testing Interview Questions, our system design coverage at SDET System Design Interview Questions, and our test data patterns at SDET Test Data Management Interview Questions. The SDET Interview Coach iOS app includes dedicated infrastructure and containerisation mock interviews — with AI-scored rounds covering Docker, TestContainers, Docker Compose, and CI/CD integration at five seniority levels.

Docker Fundamentals Interviewers Test — Dockerfile, Images, and the Container Lifecycle

Every Docker interview starts with fundamentals. But in 2026, panels aren't asking "what's a Docker image?" — they're asking questions that test whether you've built and debugged Docker images in production test pipelines. A candidate who can explain the difference between ENTRYPOINT and CMD with use cases from a real test suite demonstrates something the junior candidate can't: they've hit the sharp edges and learned from them.

Dockerfile Instructions — The Essentials Interviewers Probe

FROM: Specifies the base image. For test images, the base image choice is the first architectural decision: node:20-alpine for Jest/Playwright tests (small, fast), mcr.microsoft.com/playwright:v1.44.0-focal for Playwright with all browser dependencies pre-installed (larger but no manual browser dependency hunting), openjdk:17-slim for Selenium/Maven-based test suites. RUN: Executes commands during image build. Each RUN creates a new layer. The optimisation: chain related commands with && to reduce layer count and image size. COPY vs ADD: COPY copies files from the build context. ADD does the same but also auto-extracts tarballs and supports URLs. The interview rule: use COPY unless you specifically need tar extraction — ADD's URL support is deprecated practice. ENTRYPOINT vs CMD — the most-asked Docker interview question: ENTRYPOINT defines the executable that the container always runs. CMD provides default arguments to that executable (or, if ENTRYPOINT is absent, CMD defines the default command). The critical distinction: CMD can be overridden at docker run time, ENTRYPOINT cannot (without --entrypoint). For test images: use ENTRYPOINT for the test runner binary (["npx", "playwright", "test"]) and CMD for default arguments (["--config=playwright.ci.config.ts"]) — so CI can override the config file without changing the runner. WORKDIR: Sets the working directory for subsequent instructions. Always use absolute paths. EXPOSE: Documents which ports the container listens on. Declarative, not functional — doesn't actually publish ports. VOLUME: Creates a mount point for external volumes. For test images, use volumes for test reports, screenshots, and video artifacts — so they survive container termination.

Multi-Stage Builds — The Pattern That Demonstrates Image Optimisation Maturity

The multi-stage build is the architectural pattern that senior candidates cite when asked "how do you optimise your test Docker images?" The problem: building a test environment requires development tools (TypeScript compiler, dependency installers, browser binary downloaders) — but the runtime test image doesn't need any of those. Keeping build tools in the runtime image bloats it from 200MB to 1.2GB. The solution: a multi-stage Dockerfile where Stage 1 ("builder") installs all dependencies, compiles TypeScript, and downloads browser binaries — and Stage 2 ("runtime") copies only the compiled output, production dependencies, and browser binaries from Stage 1. The result: a lean runtime image that's fast to pull in CI, starts quickly, and only contains what the tests actually need to run. The SDET-specific pattern: Stage 1: install all devDependencies, run npx playwright install to download Chromium/Firefox/WebKit browser binaries, compile TypeScript to JavaScript. Stage 2: start from the same base image, copy node_modules (production only) and compiled output from Stage 1, copy the browser binaries from Stage 1, set the test entrypoint. The browser binary copy is the critical step — Playwright's browsers are ~400MB and installing them from scratch in CI takes 30-60 seconds. Copying them from the builder stage takes milliseconds.

# Dockerfile — Multi-stage build for a Playwright TypeScript test suite
# Stage 1: Builder — compile TypeScript, install browsers
FROM mcr.microsoft.com/playwright:v1.44.0-focal AS builder

WORKDIR /app

# Copy package files first — leverage Docker layer caching
COPY package*.json ./
RUN npm ci

# Copy source and compile TypeScript
COPY tsconfig.json ./
COPY src/ ./src/
RUN npx tsc

# Install Playwright browsers (Chromium, Firefox, WebKit)
RUN npx playwright install --with-deps

# Stage 2: Runtime — lean test execution image
FROM mcr.microsoft.com/playwright:v1.44.0-focal

WORKDIR /app

# Copy only production dependencies from builder
COPY --from=builder /app/node_modules ./node_modules

# Copy compiled JavaScript output from builder
COPY --from=builder /app/dist ./dist

# Copy Playwright browser binaries from builder (avoids re-download)
COPY --from=builder /root/.cache/ms-playwright /root/.cache/ms-playwright

# Copy test configuration and fixtures
COPY playwright.config.ts ./
COPY fixtures/ ./fixtures/

# ENTRYPOINT: the test runner (not overridable without --entrypoint)
ENTRYPOINT ["npx", "playwright", "test"]

# CMD: default arguments (overridable at docker run time)
CMD ["--config=playwright.ci.config.ts"]

The interview answer that demonstrates depth: "I structure test Docker images as multi-stage builds. Stage 1 — the builder — installs all dependencies including devDependencies, compiles TypeScript to JavaScript, and downloads browser binaries. Stage 2 — the runtime — copies only production node_modules, compiled output, and browser binaries from Stage 1. This produces an image that's 60-70% smaller than a single-stage build, pulls faster in CI (reducing cold-start pipeline time), and doesn't expose build tools inside the runtime container. I use ENTRYPOINT for the test runner binary and CMD for default flags — so CI can override flags without a new image build. I mount test reports as a Docker volume so they survive container termination, and I use a .dockerignore file to exclude node_modules, .git, and local test artifacts from the build context — which speeds up the COPY step and prevents cache invalidation from local cruft."

Containerising Playwright and Selenium Tests — The Practical Patterns

Every SDET has run Playwright or Selenium tests locally. Containerising those tests — and making them run identically in CI — is the skill panels probe for evidence of operational maturity. The questions aren't "can you run a Playwright test in Docker?" — they're "explain your container strategy for browser-based tests, how you handle browser binaries, how you mount test reports, and how you debug a containerised test that's failing in CI but passing locally."

Playwright in Docker — The Official Image and Custom Approaches

Playwright publishes an official Docker image: mcr.microsoft.com/playwright:v1.44.0-focal. It includes Node.js, all three browser engines (Chromium, Firefox, WebKit), and all system dependencies (libgtk, libnotify, libgconf, etc.) — everything needed to run headless browser tests. When to use the official image: For CI pipelines and production test runs — it's battle-tested, maintained by the Playwright team, and eliminates the "missing system dependency" debugging nightmare. When to build a custom image: When your tests need additional tools (kubectl for Kubernetes test fixtures, AWS CLI for S3 test data seeding, database clients for test data setup) or when you need a specific Node.js version that isn't bundled. The headless configuration: Playwright runs headless by default in Docker because there's no display server. For headed debugging, you can mount an X11 socket or use Playwright's --headed flag with a VNC server in the container — but this is a development convenience, not a CI pattern. Test report persistence: Mount a volume (-v $(pwd)/test-results:/app/test-results) to persist Playwright HTML reports, screenshots, and trace viewer files after the container exits. Without a volume mount, all test artifacts are lost when the container terminates.

Selenium Grid with Docker Compose — The Hub-Node Architecture

Selenium Grid's hub-node architecture is the classic Docker Compose test stack. The Hub acts as the central router — test scripts connect to the Hub, which distributes test sessions to registered Nodes. Each Node runs a specific browser (Chrome, Firefox, Edge) and can handle multiple concurrent sessions. The Docker Compose pattern: Define a selenium-hub service (the Hub), one or more browser node services (chrome, firefox, edge), and your test runner service — all connected via a Docker network. The browser nodes register with the Hub via the SE_EVENT_BUS_HOST and SE_EVENT_BUS_PUBLISH_PORT / SE_EVENT_BUS_SUBSCRIBE_PORT environment variables. Your test runner connects to http://selenium-hub:4444 (the Docker Compose service name becomes the hostname). Scaling browser nodes: docker compose up --scale chrome=3 — spins up 3 Chrome nodes behind the Hub, enabling 3 concurrent Chrome test sessions. This is how you parallelise Selenium tests without a cloud provider. Video recording: Selenium Grid 4 supports session video recording — mount a volume to /videos on the browser node and each test session is recorded to an MP4 file. Invaluable for debugging flaky CI failures. The VNC debugging pattern: Map port 5900 on a browser node to your host, connect with a VNC client, and watch the browser execute your test in real time — this is the Docker equivalent of seeing the browser on your desktop.

# docker-compose.yml — Selenium Grid for parallel cross-browser testing
type DockerCompose = (services: {
  selenium-hub: {
    image: string;
    container_name: string;
    ports: string[];
    environment: Record<string, string>;
  };
  chrome: {
    image: string;
    depends_on: string[];
    environment: Record<string, string>;
    volumes: string[];
  };
  firefox: {
    image: string;
    depends_on: string[];
    environment: Record<string, string>;
    volumes: string[];
  };
  test-runner: {
    build: { context: string; dockerfile: string };
    depends_on: string[];
    environment: Record<string, string>;
    volumes: string[];
    command: string;
  };
}) => void;

# Actual docker-compose.yml:
version: '3.8'

services:
  selenium-hub:
    image: selenium/hub:4.18
    container_name: selenium-hub
    ports:
      - "4442:4442"  # Subscriber
      - "4443:4443"  # Publisher
      - "4444:4444"  # UI + W3C WebDriver endpoint
    environment:
      SE_SESSION_REQUEST_TIMEOUT: 300
      SE_SESSION_RETRY_INTERVAL: 5

  chrome:
    image: selenium/node-chrome:4.18
    depends_on:
      - selenium-hub
    environment:
      SE_EVENT_BUS_HOST: selenium-hub
      SE_EVENT_BUS_PUBLISH_PORT: 4442
      SE_EVENT_BUS_SUBSCRIBE_PORT: 4443
      SE_NODE_MAX_SESSIONS: 4
      SE_NODE_SESSION_TIMEOUT: 300
    volumes:
      - ./videos/chrome:/videos  # Session video recording

  firefox:
    image: selenium/node-firefox:4.18
    depends_on:
      - selenium-hub
    environment:
      SE_EVENT_BUS_HOST: selenium-hub
      SE_EVENT_BUS_PUBLISH_PORT: 4442
      SE_EVENT_BUS_SUBSCRIBE_PORT: 4443
      SE_NODE_MAX_SESSIONS: 4
    volumes:
      - ./videos/firefox:/videos

  test-runner:
    build:
      context: .
      dockerfile: Dockerfile.selenium
    depends_on:
      selenium-hub:
        condition: service_healthy
    environment:
      SELENIUM_HUB_URL: http://selenium-hub:4444
      TEST_ENV: ci
    volumes:
      - ./test-results:/app/test-results
      - ./screenshots:/app/screenshots
    command: ["npm", "run", "test:e2e:ci"]

# Scale up: docker compose up --scale chrome=3 --scale firefox=2
# Result: 12 Chrome sessions + 8 Firefox sessions = 20 parallel tests

TestContainers — Integration Testing with Real Dependencies

TestContainers is the topic that most separates mid-level from senior SDETs in Docker interviews. When a panel asks "how do you test database integrations?" and the candidate says "I mock the database layer" — alarm bells ring. Mocking a database means you're not testing the actual SQL, the actual connection pool behaviour, or the actual transaction isolation. TestContainers solves this by spinning up real infrastructure — PostgreSQL, Redis, Kafka, Elasticsearch, Selenium browser containers — directly from your test code, using Docker under the hood. The containers are ephemeral: they start before the test, run during the test, and are destroyed after. Every test gets a clean, isolated instance.

TestContainers with Java/JUnit 5 — The Mature Ecosystem

TestContainers was born in the Java ecosystem and its JUnit 5 integration is the most mature. The @Testcontainers annotation combined with @Container fields automatically manages container lifecycle — start before tests, stop after. GenericContainer lets you spin up any Docker image; specialised modules (PostgreSQLContainer, KafkaContainer, RedisContainer, LocalStackContainer for AWS services) provide preconfigured, opinionated wrappers. Key patterns: Singleton containers (shared across test class via static field — starts once, reused across all tests), per-test containers (new container per test method — maximum isolation, slower), and the @DynamicPropertySource annotation for injecting container connection details into Spring Boot tests. The interview insight that scores highest: knowing when to use singleton vs per-test containers. Singleton: fast, suitable for read-heavy tests where data isolation between tests is managed through test-scoped transactions or unique keys. Per-test: maximum isolation, slower, necessary when tests mutate shared state that can't be transactionally isolated (schema changes, extension installations, configuration changes).

TestContainers with Node.js/TypeScript — The Growing Ecosystem

The Node.js TestContainers library (testcontainers on npm, v10+) provides the same capabilities with a promise-based API. The core class: GenericContainer which wraps any Docker image. Specialised modules: PostgreSqlContainer, RedisContainer, KafkaContainer, ElasticsearchContainer. Key patterns: await container.start() returns connection details (host, port), await container.stop() for cleanup. Integration with Jest/Vitest: use beforeAll to start containers, afterAll to stop them, or use a global setup file for containers shared across the entire test suite. Playwright + TestContainers: You can even use TestContainers to spin up browser containers for Playwright — though Playwright's official Docker image is usually more practical for CI. The TestContainers browser pattern is more useful when you need programmatic control over the browser container lifecycle within a Node.js test suite that's also managing other infrastructure containers.

// TestContainers with TypeScript + Jest — Integration test with real PostgreSQL
import {
  PostgreSqlContainer,
  type StartedPostgreSqlContainer,
} from '@testcontainers/postgresql';
import { RedisContainer } from '@testcontainers/redis';
import { GenericContainer } from 'testcontainers';
import { Client } from 'pg';
import { createApp } from '../src/app';

describe('Order Service Integration Tests', () => {
  let postgresContainer: StartedPostgreSqlContainer;
  let redisContainer: any;
  let dbClient: Client;
  let app: any;

  beforeAll(async () => {
    // Start PostgreSQL container with test-specific configuration
    postgresContainer = await new PostgreSqlContainer('postgres:16-alpine')
      .withDatabase('orders_test')
      .withUsername('test_user')
      .withPassword('test_password')
      .withExposedPorts(5432)
      .start();

    // Start Redis container for caching layer
    redisContainer = await new RedisContainer('redis:7-alpine')
      .withExposedPorts(6379)
      .start();

    // Connect and run migrations — using real database, real SQL
    dbClient = new Client({
      host: postgresContainer.getHost(),
      port: postgresContainer.getPort(),
      database: postgresContainer.getDatabase(),
      user: postgresContainer.getUsername(),
      password: postgresContainer.getPassword(),
    });
    await dbClient.connect();
    await dbClient.query(`
      CREATE TABLE orders (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        user_id VARCHAR(50) NOT NULL,
        total DECIMAL(10,2) NOT NULL,
        status VARCHAR(20) DEFAULT 'pending',
        created_at TIMESTAMP DEFAULT NOW()
      )
    `);

    // Start the app with real infrastructure
    app = createApp({
      postgresUrl: postgresContainer.getConnectionUri(),
      redisUrl: redisContainer.getConnectionUrl(),
    });
  }, 60000); // 60s timeout for container startup

  afterAll(async () => {
    await dbClient.end();
    await postgresContainer.stop();
    await redisContainer.stop();
  });

  beforeEach(async () => {
    // Clean data between tests — each test gets a clean slate
    await dbClient.query('DELETE FROM orders');
  });

  it('creates an order and returns it with calculated total', async () => {
    const response = await app.createOrder({
      userId: 'user-001',
      items: [
        { productId: 'prod-1', quantity: 2, price: 29.99 },
        { productId: 'prod-2', quantity: 1, price: 49.99 },
      ],
    });

    expect(response.status).toBe(201);
    expect(response.body.total).toBe(109.97); // Real calculation tested

    // Verify the data actually persisted correctly
    const { rows } = await dbClient.query(
      'SELECT * FROM orders WHERE id = $1',
      [response.body.id]
    );
    expect(rows[0].status).toBe('pending');
    expect(parseFloat(rows[0].total)).toBe(109.97);
  });

  it('handles concurrent order creation with optimistic locking', async () => {
    // Test actual transaction isolation — impossible with mocks
    const order1 = app.createOrder({ userId: 'user-001', items: [{ productId: 'last-item', quantity: 1, price: 10.00 }] });
    const order2 = app.createOrder({ userId: 'user-001', items: [{ productId: 'last-item', quantity: 1, price: 10.00 }] });

    const results = await Promise.allSettled([order1, order2]);
    const succeeded = results.filter(r => r.status === 'fulfilled').length;

    // Only one should succeed if inventory logic is correct
    expect(succeeded).toBe(1);
  });
});

Docker Volumes and Networks — The Multi-Container Plumbing Interviewers Probe

Docker volumes and networks are the "plumbing" concepts that most candidates skip over — and that senior panels probe specifically. When you understand volumes, you understand how test artifacts survive container termination. When you understand networks, you understand how containers discover each other and why "localhost" means different things inside and outside a container. These aren't trivia — they're the operational knowledge that determines whether your containerised test suite works on the first try or consumes three days of debugging.

Volumes — Test Artifact Persistence and Test Data Injection

Docker containers are ephemeral by design — when a container exits, its filesystem is destroyed (unless you docker commit, which you shouldn't for test containers). Volumes solve this by providing persistent storage that exists independently of the container lifecycle. Named volumes: Managed by Docker (docker volume create), stored in Docker's data directory, survive container removal. Use for test data that should persist across CI runs (reference datasets, golden files). Bind mounts: Mount a host directory into the container (-v /host/path:/container/path). Use for test reports, screenshots, and videos — you want these on the CI host's filesystem where subsequent pipeline steps (artifact upload, Slack notification) can access them. The test artifact pattern: Mount ./test-results:/app/test-results in CI — Playwright writes its HTML report, JSON results, and screenshots to /app/test-results inside the container, which is actually ./test-results on the CI host. The container exits, the volume persists, and the next pipeline step uploads ./test-results as a CI artifact. tmpfs volumes: In-memory volumes (--tmpfs /app/temp) — faster than disk, automatically cleaned up. Use for temporary test data that doesn't need to persist — database scratch space, download caches, unpacked test fixtures. The volume permission trap: Files written by the container are owned by the container's user (often root). If the CI host runs as a different user, it can't read or delete those files. The fix: either run the container with the host's UID (--user $(id -u):$(id -g)) or use a post-test step that chowns the artifact directory.

Networks — Service Discovery and the localhost Trap

Docker networking is where most containerisation bugs originate — and where interview panels probe for operational debugging experience. Bridge network (default): Containers on the same bridge network can reach each other by container name or service name (in Compose). DNS resolution is built-in. This is the default for Docker Compose and the recommended pattern for multi-container test stacks. Host network (--network=host): The container shares the host's network stack — localhost inside the container is the host's localhost. Faster (no network address translation overhead) but less isolated. Use when your test needs to connect to services running on the host (a local dev database, a mock server on a random port). The localhost trap — the most common Docker networking bug: Inside a container, localhost refers to the container itself, not the Docker host. If your test code connects to http://localhost:4444 expecting to reach a Selenium Hub running in another container, it will fail — because localhost:4444 inside the test container points to the test container, not the Hub container. The fix: use the service name as the hostname (http://selenium-hub:4444) on a bridge network, or use host.docker.internal (macOS/Windows) to reach the host. The network debugging answer interviewers want: "I debug Docker networking issues in this order: (1) docker network inspect to verify containers are on the same network, (2) docker exec <container> ping <service-name> to test DNS resolution, (3) docker exec <container> nc -zv <service-name> <port> to test TCP connectivity, (4) check that the application code uses the service name as hostname, not localhost. The fix is almost always a hostname issue or a missing depends_on that doesn't wait for service readiness."

# Docker Compose — Multi-container networking with health checks and dependencies
type DockerComposeEnv = {
  services: {
    postgres: { host: string; port: number; db: string };
    redis: { host: string; port: number };
    localstack: { host: string; port: number };
    test_runner: { build: string; depends_on: Record<string, 'service_healthy'>; environment: Record<string, string>; volumes: string[] };
  };
  networks: { test_network: { driver: 'bridge' } };
  volumes: { pg_data: {}, test_results: {} };
}; void;

# Actual docker-compose.integration.yml:
version: '3.8'

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: tester
      POSTGRES_PASSWORD: testpass
    ports:
      - "5432"  # Random host port — avoids conflicts
    volumes:
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U tester -d testdb"]
      interval: 2s
      timeout: 5s
      retries: 10
    networks:
      - test_network

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 2s
      timeout: 3s
      retries: 10
    networks:
      - test_network

  localstack:
    image: localstack/localstack:3.0
    environment:
      SERVICES: s3,dynamodb,sqs
      DEFAULT_REGION: eu-west-2
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
      interval: 5s
      timeout: 10s
      retries: 5
    networks:
      - test_network

  test-runner:
    build:
      context: .
      dockerfile: Dockerfile.test
    depends_on:
      postgres:
        condition: service_healthy  # Wait for actual readiness, not just container start
      redis:
        condition: service_healthy
      localstack:
        condition: service_healthy
    environment:
      PG_HOST: postgres          # Service name = DNS hostname
      PG_PORT: "5432"
      PG_DB: testdb
      PG_USER: tester
      PG_PASSWORD: testpass
      REDIS_HOST: redis
      REDIS_PORT: "6379"
      AWS_ENDPOINT: http://localstack:4566
      AWS_REGION: eu-west-2
      NODE_ENV: test
    volumes:
      - ./test-results:/app/test-results  # Artifact persistence
      - ./coverage:/app/coverage
    networks:
      - test_network
    command: ["npm", "run", "test:integration"]

networks:
  test_network:
    driver: bridge

volumes:
  pg_data:
  test_results:

Docker in CI/CD Pipelines — The Deterministic Test Execution Pattern

The reason Docker dominates CI/CD test pipelines is simple: determinism. A test suite that runs in a Docker container on your laptop runs in exactly the same environment in GitHub Actions, Jenkins, or GitLab CI. Same OS, same dependencies, same browser versions, same system libraries. Docker eliminates the "it works on my machine" class of test failures by making the CI environment identical to the development environment. But getting Docker right in CI requires understanding layer caching, service containers, artifact extraction, and the pipeline patterns that make containerised tests fast enough to gate deployments. For the full CI/CD testing picture, see our dedicated guide at CI/CD Pipeline Testing Interview Questions.

GitHub Actions — Docker Service Containers and Layer Caching

GitHub Actions supports Docker natively through two mechanisms: service containers (database, Redis, Selenium Hub — defined in the workflow YAML, GitHub manages their lifecycle) and container jobs (the entire job runs inside a Docker container). Service containers: Ideal for databases and infrastructure — they start before the job and stop after. The syntax: services: { postgres: { image: postgres:16, env: {...}, ports: [...], options: --health-cmd ... } }. Your test code connects to postgres:5432 (the service name becomes the hostname). Container jobs: The entire job runs inside your custom Docker image — you get the exact same environment as local development. Syntax: jobs: { test: { container: 'my-test-image:latest', steps: [...] } }. Docker layer caching: The single biggest CI speed improvement — without it, every CI run rebuilds your Docker image from scratch (downloading dependencies, compiling TypeScript, installing browsers — easily 5-10 minutes). With layer caching, only changed layers rebuild. GitHub Actions supports Docker layer caching via docker/build-push-action with cache-from and cache-to using GitHub Cache or a Docker registry. The SDET-specific CI insight: Cache your Playwright browser binaries — they're 400MB and take 30-60 seconds to download fresh. Cache your node_modules — npm ci takes 60-120 seconds without cache. A well-cached Docker build for a Playwright test suite should take 20-40 seconds on cache hit vs 5-10 minutes on cache miss.

The Artifact Extraction Pattern — Getting Test Results Out of Containers

When your tests run inside a Docker container, their output (reports, screenshots, videos, logs) lives inside the container's filesystem — which is destroyed when the container exits. Extracting those artifacts is critical for CI observability. Pattern 1 — Volume mount (recommended): Mount a host directory into the container (-v $PWD/test-results:/app/test-results) and the test runner writes to it. Artifacts appear on the CI host immediately and survive container termination. Pattern 2 — docker cp (worst case fallback): Run your tests, then docker cp <container>:/app/test-results ./test-results before the container is removed. Less reliable — if the container crashes, docker cp may not work. Pattern 3 — CI artifact upload: After tests complete, use the CI platform's artifact upload action (actions/upload-artifact@v4 on GitHub Actions) to preserve test results, screenshots, and videos for later inspection. Set retention days appropriately — 7 days for routine runs, 30 days for release-blocking tests. The interview answer that demonstrates experience: "I always mount test results as a Docker volume — never rely on docker cp. In GitHub Actions, I use -v ${{ github.workspace }}/test-results:/app/test-results in the container options, then actions/upload-artifact@v4 to preserve them with appropriate retention. For Playwright specifically, I mount both the test-results directory and the playwright-report directory so I get both raw JSON results and the interactive HTML report."

# .github/workflows/e2e-tests.yml — Dockerised Playwright tests with service containers
name: E2E Tests (Docker)
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  e2e:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: tester
          POSTGRES_PASSWORD: testpass
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U tester -d testdb"
          --health-interval 2s
          --health-timeout 5s
          --health-retries 10
      
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 2s
          --health-timeout 3s
          --health-retries 10

    container:
      image: ghcr.io/myorg/playwright-tests:latest
      credentials:
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
      options: >-
        --network host
        -v ${{ github.workspace }}/test-results:/app/test-results
        -v ${{ github.workspace }}/playwright-report:/app/playwright-report

    steps:
      - uses: actions/checkout@v4

      - name: Run Playwright E2E tests
        run: npx playwright test --config=playwright.ci.config.ts
        env:
          DATABASE_URL: postgresql://tester:testpass@localhost:5432/testdb
          REDIS_URL: redis://localhost:6379

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

      - name: Upload screenshots on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-screenshots
          path: test-results/**/*.png
          retention-days: 7

Docker vs Kubernetes for Test Infrastructure — The Scaling Decision

This is the strategic infrastructure question that appears at the senior and lead SDET level: "When do you move from Docker Compose to Kubernetes for test infrastructure?" Panels aren't looking for Kubernetes evangelism — they're looking for engineering judgment. The candidate who says "Kubernetes for everything" has demonstrated trend-following. The candidate who says "Docker Compose suffices for most test needs; here's when Kubernetes adds value" has demonstrated infrastructure maturity.

Docker Compose — When It's the Right Tool

Docker Compose is ideal for: Local development test stacks — one developer, one docker-compose.yml, spin up Postgres + Redis + your app + test runner in 30 seconds. Single-machine CI pipelines — GitHub Actions, Jenkins on a single agent, GitLab CI on a single runner. Docker Compose is the simplest, fastest path to deterministic test environments on a single host. Teams up to ~20 engineers where test suite concurrency is manageable on a single CI machine. Integration tests that don't need horizontal scaling — the test suite takes 10 minutes, runs on every PR, and fits on one machine. Strengths: Simplicity (one YAML file, one command), speed (no cluster overhead), familiarity (every developer knows Compose), zero infrastructure management. Limitations: Single-machine ceiling — you can't add more machines when tests outgrow one host. No built-in scheduling, resource allocation, or auto-scaling. Manual parallelisation (you write CI matrix strategies yourself). No multi-tenancy — one Compose stack, one test run at a time.

Kubernetes — When You Need to Graduate

Kubernetes adds value when: Test suite concurrency exceeds single-machine capacity — your CI needs to run 20 parallel test suites simultaneously and no single VM can handle all those containers. k8s schedules test pods across a cluster of machines. Dynamic test environments for 50+ engineers — every PR gets its own isolated test namespace with Postgres, Redis, and the app, automatically provisioned and torn down. Auto-scaling test runners — k8s Jobs or CronJobs scale horizontally, and you can use KEDA (Kubernetes Event-Driven Autoscaling) to spin up test runners based on PR queue depth. Multi-cloud / multi-region testing — run tests across geographically distributed k8s clusters to measure latency and failover behaviour from different regions. Key k8s primitives for testing: Job (run a test suite to completion — the container exits, the Job succeeds or fails), CronJob (scheduled test execution — nightly full regression, weekly performance baseline), Namespace (isolate PR test environments — one namespace per PR, delete on PR close), ConfigMap/Secret (manage test configuration without hardcoding), PersistentVolumeClaim (test artifact persistence across pod restarts). The interview insight: Kubernetes doesn't replace Docker Compose — it adds orchestration when you outgrow a single machine. Most SDET teams will spend their first 2-3 years on Docker Compose and graduate to k8s only when concurrency demands it. Demonstrating you understand when to make that transition — not that you know kubectl commands — is the senior-level signal.

# Kubernetes Job — Run a Playwright test suite to completion on a k8s cluster
apiVersion: batch/v1
kind: Job
metadata:
  name: playwright-e2e-${GIT_SHA}
  namespace: test-automation
  labels:
    app: playwright-tests
    pr: "${PR_NUMBER}"
spec:
  backoffLimit: 1        # Retry once on failure
  ttlSecondsAfterFinished: 3600  # Auto-cleanup after 1 hour
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: playwright
          image: ghcr.io/myorg/playwright-tests:${GIT_SHA}
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: test-db-credentials
                  key: url
            - name: TEST_ENV
              value: "k8s-ci"
          resources:
            requests:
              memory: "2Gi"
              cpu: "2"
            limits:
              memory: "4Gi"
              cpu: "4"
          volumeMounts:
            - name: test-results
              mountPath: /app/test-results
      volumes:
        - name: test-results
          persistentVolumeClaim:
            claimName: playwright-results-pvc

Common Docker Interview Questions for SDETs — And How to Answer Them

After 20 years of SDET interview panels, Mitchell has catalogued the Docker and containerisation questions that appear most frequently. Here's the question bank with the answer patterns that separate candidates who've tinkered with Docker from those who've engineered containerised test infrastructure at scale.

⚠️

"What's the difference between ENTRYPOINT and CMD?"

This is the most-asked Docker question in SDET interviews. Wrong answer: "They both define what runs in the container." Correct answer: ENTRYPOINT defines the executable that always runs — it's not overridable without --entrypoint. CMD defines default arguments to that executable, which can be overridden at docker run time. For a test image: ENTRYPOINT ["npx", "playwright", "test"] and CMD ["--config=playwright.config.ts"]. In CI, you can run docker run my-test-image --config=playwright.ci.config.ts --grep=@smoke — CMD is replaced with your CI-specific flags, but the test runner (ENTRYPOINT) stays the same. If both are absent, the container runs whatever is specified at docker run. If CMD is a string, it's wrapped in /bin/sh -c; if it's an array (exec form), it executes directly without a shell.

⚠️

"How do you debug a containerised test that only fails in CI?"

Wrong answer: "I add more console.log statements and re-run CI." (Each CI run is 10 minutes — this is the slowest possible debug loop.) Correct answer: "My debugging workflow is: (1) pull the exact CI Docker image locally — docker pull <registry>/<image>:<tag> from the CI logs, (2) run the container interactively with the same environment variables and volume mounts as CI — docker run -it --entrypoint /bin/bash ..., (3) execute the test command manually inside the container to reproduce the failure, (4) use Playwright's --headed flag with VNC or X11 forwarding to see the browser UI, (5) check environment variable differences between local and CI — a missing env var is the most common cause of CI-only failures, (6) compare the Docker image layer digests to ensure local and CI are running the same image. This whole workflow takes 2-3 minutes vs 10+ minutes per CI iteration."

⚠️

"Explain Docker layer caching and how you'd optimise a test image build from 5 minutes to 30 seconds."

Correct answer: "Docker builds images as a sequence of layers — each instruction (FROM, RUN, COPY) creates a layer. Layers are cached: if a layer's instruction and context haven't changed, Docker reuses the cached layer instead of rebuilding. The optimisation strategy: (1) Order instructions by change frequency — least-frequently-changing first. COPY package*.json ./ before COPY src/ ./src/ — so dependency installation is cached when only source code changes. (2) Chain RUN commands — RUN apt-get update && apt-get install -y ... && rm -rf /var/lib/apt/lists/* reduces layer count and cleans up in the same layer. (3) Use multi-stage builds — the builder stage has all dev tools, the runtime stage copies only what's needed. (4) Use .dockerignore — exclude node_modules, .git, coverage, dist, local test artifacts from the build context. (5) For Playwright specifically: copy the browser binaries from the builder stage rather than reinstalling them — saves 30-60 seconds per build. (6) Use CI-specific cache mounts (--mount=type=cache,target=/root/.npm in BuildKit) for npm cache persistence across builds. A well-optimised test Dockerfile goes from 5 minutes to 30-60 seconds on cache hit — and this directly impacts how quickly CI gates PR merges."

⚠️

"depends_on vs healthcheck — which should you use and why?"

Wrong answer: "depends_on ensures Postgres is ready before tests start." Correct answer: "depends_on only waits for the container to start — it does not wait for the service inside the container to be ready. A Postgres container can be 'started' (the process is running) but not 'ready' (pg_isready still connecting, initialising the database, running init scripts). If your test runner depends on Postgres being actually ready to accept connections, depends_on alone will cause race-condition failures. The fix: combine depends_on with a healthcheck. Define healthcheck: { test: ["CMD-SHELL", "pg_isready -U tester"], interval: 2s, retries: 10 } on the Postgres service, then depends_on: { postgres: { condition: service_healthy } } on the test runner. Now Docker Compose waits until the health check passes before starting the test runner. Without this, you need application-level retry logic in your test setup — which works, but adds complexity and obscures the true cause of startup failures. The healthcheck pattern is the production-grade approach."

⚠️

"How do you handle test data seeding in Docker Compose test environments?"

Correct answer: "I have three strategies, chosen by test complexity. Strategy 1 — init scripts: mount SQL files to /docker-entrypoint-initdb.d/ on the Postgres container — Docker runs them on first startup. Works for static reference data (product catalogues, country lists, role definitions). Strategy 2 — programmatic seeding in beforeAll: my test suite connects to the database and seeds test-specific data before each test or describe block. This is the most common pattern for integration tests — it keeps the seeding logic alongside the test that depends on it, making the test self-contained and readable. Strategy 3 — seed container: a separate Docker Compose service that runs once, seeds the database, and exits with code 0. The test-runner service depends on this seed service completing successfully. This pattern is useful when seeding is complex (multiple services, large datasets) and you want a clear separation between 'preparing the environment' and 'running the tests.' The key principle across all three: test data is ephemeral and test-specific — every test run starts from a known state and creates the data it needs. Never depend on persistent shared test data that another test might mutate." This connects directly to the patterns covered in our SDET Test Data Management Interview Questions guide.

⚠️

"When would you use TestContainers vs a shared Docker Compose stack for integration tests?"

Correct answer: "TestContainers excels when tests need isolation — each test or test class gets its own database/Redis/Kafka instance, guaranteeing no test interference. It's ideal for: CI pipelines where tests run in parallel on the same machine (each test's containers are independent), tests that mutate schema or configuration (no risk of affecting other tests), and tests where the infrastructure lifecycle should match the test lifecycle (start before test, stop after — no dangling state). Docker Compose (shared stack) excels when tests need speed — starting one Postgres instance and sharing it across hundreds of tests is much faster than starting a new Postgres per test class. It's ideal for: local development (spin up the stack once, run tests repeatedly without container startup overhead), large test suites where per-test container startup would dominate execution time, and environments where the infrastructure is complex to start (10+ services) and the startup time is prohibitive for per-test isolation. The pragmatic approach: in local dev, use Docker Compose for speed. In CI, use TestContainers for isolation — CI machines are ephemeral anyway, so the isolation benefit is pure upside. For the full infrastructure architecture perspective, see our SDET System Design Interview Questions guide."

How SDET Interview Coach Prepares You for Docker and Infrastructure Interview Rounds

Docker knowledge is no longer optional for SDETs — it's table stakes at mid-level and the differentiator at senior. The SDET Interview Coach iOS app prepares you for Docker and infrastructure rounds with AI-powered mock interviews that simulate the exact flow of a containerisation-focused SDET interview — from the opening "explain the difference between ENTRYPOINT and CMD" to the architect-level "design a containerised test infrastructure for 50 parallel test suites across 3 cloud regions."

The app's infrastructure topic area covers Docker, Docker Compose, TestContainers, Kubernetes for testing, CI/CD container patterns, and cloud infrastructure for test environments — with questions calibrated to five seniority levels. At junior level, the questions focus on Dockerfile basics, running containers, and simple docker-compose.yml files. At mid level, they cover multi-stage builds, TestContainers, service containers in CI, and volume/network patterns. At senior and lead levels, they probe infrastructure architecture — Docker-vs-Kubernetes decisions, multi-region test infrastructure design, cost-optimisation of containerised CI pipelines, and the design of self-service test environment platforms that serve 100+ engineering teams.

The AI mock interviewer scores your answers on technical accuracy, completeness, communication clarity, and real-world applicability — the same criteria real interview panels use. After each mock round, you receive a detailed scorecard with specific feedback on what to improve, plus links to the relevant sections in our infrastructure guides (including this Docker guide, the CI/CD Pipeline Testing guide, and the System Design guide). The spaced repetition system ensures you retain Dockerfile instruction semantics, Docker Compose healthcheck patterns, and the TestContainers API patterns that interviewers expect you to recall under pressure. Use Job Match to generate 50 bespoke questions from any job description that mentions Docker, Kubernetes, containerisation, or CI/CD — and walk into your interview knowing you've practised the exact questions your panel will ask.

For the broader infrastructure picture, complement this guide with CI/CD Pipeline Testing Interview Questions for the full pipeline integration perspective and SDET System Design Interview Questions for the architectural patterns that Docker enables in distributed test infrastructure.

Ready to Transform Your Testing?

The AI Test Automation Playbook gives you everything you need: Playwright setup, Claude AI integration, MCP deep dive, 10+ ready-to-use prompts, CI/CD pipeline setup, and a 30-day implementation roadmap.

✅ Playwright + TypeScript✅ Claude AI Prompts✅ MCP Deep Dive✅ CI/CD with GitHub Actions✅ 30-Day Roadmap✅ Page Object Patterns
Get the AI Test Automation Playbook — $49.99

By Mitchell Agoma, Senior SDET & AI Testing Specialist with 8+ years of experience