On this page8 sections
CommandsIntermediate7-9 min reference

Docker for QA

What you actually need to know about Docker as a tester: spinning up disposable databases, running browsers in CI, isolating dependencies, and composing multi-service test environments.

Core Concepts

ConceptWhat it is
ImageA read-only blueprint — code + dependencies + config baked together. Identified by name:tag (e.g. postgres:16).
ContainerA running instance of an image. Many containers from one image. Cheap to create and destroy.
DockerfileThe recipe for building an image — FROM base, copy code, install deps, set entry point.
RegistryWhere images live: Docker Hub (public), GitHub Container Registry, AWS ECR, etc.
VolumePersistent storage that survives container removal. Use for DB data, test artifacts.
NetworkA virtual network containers share. Containers on the same network reach each other by name.

Essential Commands

Pull and run

docker pull postgres:16
docker pull selenium/standalone-chrome:latest
 
# Run in background, expose port, name it
docker run -d \
  --name test-db \
  -p 5432:5432 \
  -e POSTGRES_PASSWORD=dev \
  postgres:16
 
# Run interactively, remove on exit
docker run --rm -it ubuntu:22.04 bash

Inspect

docker ps                          # running containers
docker ps -a                       # include stopped
docker images                      # local images
docker inspect test-db             # full JSON of config + state
docker logs test-db                # stdout/stderr
docker logs -f --tail 100 test-db  # follow last 100 lines
docker stats                       # live CPU/mem per container

Lifecycle

docker stop test-db
docker start test-db
docker restart test-db
docker rm test-db                  # remove (must be stopped)
docker rm -f test-db               # force-remove a running container
docker rmi postgres:16             # remove an image

Shell into a running container

docker exec -it test-db bash
docker exec -it test-db psql -U postgres

Cleanup

docker container prune             # removes all stopped containers
docker image prune                 # removes dangling images
docker volume prune                # removes unused volumes (DESTRUCTIVE)
docker system prune                # all of the above
docker system prune -a --volumes   # nuclear: removes unused images + volumes

Disk usage

docker system df
docker system df -v                # per-image and per-volume breakdown

Dockerfile Basics

A typical Node.js test runner image:

FROM node:20-alpine
 
WORKDIR /app
 
# Copy manifests first for better layer caching
COPY package*.json ./
RUN npm ci
 
# Then copy source
COPY . .
 
# Bake in any build step the tests need
RUN npm run build
 
EXPOSE 3000
CMD ["npm", "start"]

Build and run:

docker build -t my-app:dev .
docker run --rm -p 3000:3000 my-app:dev

Multi-stage builds

Smaller final image, faster CI:

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=build /app/.next ./.next
COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules
EXPOSE 3000
CMD ["npm", "start"]

.dockerignore

Skip files that don't belong in the image — same syntax as .gitignore. Saves build time and image size:

node_modules
.git
.next
dist
test-results
videos
screenshots
*.log
.env
.env.*

Docker Compose for Test Environments

docker-compose.yml describes multi-service stacks declaratively. Same machine, one command up.

A test stack: app + database + browser

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: app
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10
 
  app:
    build: .
    environment:
      DATABASE_URL: postgres://postgres:dev@db:5432/app
      NODE_ENV: test
    ports:
      - "3000:3000"
    depends_on:
      db:
        condition: service_healthy
 
  cypress:
    image: cypress/included:13
    working_dir: /e2e
    environment:
      CYPRESS_baseUrl: http://app:3000
    volumes:
      - ./cypress:/e2e/cypress
      - ./cypress.config.ts:/e2e/cypress.config.ts
    depends_on:
      - app
 
volumes:
  db-data:

Running it

docker compose up -d                  # start everything in the background
docker compose logs -f app            # follow app logs
docker compose exec app bash          # shell into the app container
docker compose run --rm cypress       # one-shot test run (auto-removed)
docker compose down                   # stop and remove containers
docker compose down -v                # also remove named volumes (destructive)

Environment variables

Inline:

environment:
  NODE_ENV: test
  API_URL: http://api:8080

From a file (auto-loaded by docker compose):

# .env in the same directory as docker-compose.yml
DATABASE_URL=postgres://postgres:dev@db:5432/app
API_TOKEN=abc123

Or explicitly per-service:

services:
  app:
    env_file:
      - .env.test

depends_on + healthchecks

depends_on only waits for a container to start, not to be ready. Pair it with a healthcheck and condition: service_healthy so dependent services wait for actual readiness.

db:
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U postgres"]
    interval: 5s
    timeout: 3s
    retries: 10
 
app:
  depends_on:
    db: { condition: service_healthy }

Test Infrastructure Patterns

Selenium Grid

services:
  selenium-hub:
    image: selenium/hub:4
    ports:
      - "4444:4444"
 
  chrome:
    image: selenium/node-chrome:4
    shm_size: 2gb            # avoid Chrome crashes
    depends_on: [selenium-hub]
    environment:
      SE_EVENT_BUS_HOST: selenium-hub
      SE_EVENT_BUS_PUBLISH_PORT: 4442
      SE_EVENT_BUS_SUBSCRIBE_PORT: 4443
 
  firefox:
    image: selenium/node-firefox:4
    shm_size: 2gb
    depends_on: [selenium-hub]
    environment:
      SE_EVENT_BUS_HOST: selenium-hub
      SE_EVENT_BUS_PUBLISH_PORT: 4442
      SE_EVENT_BUS_SUBSCRIBE_PORT: 4443

Scale browser nodes for parallelism:

docker compose up -d --scale chrome=4 --scale firefox=2

Playwright in Docker

services:
  playwright:
    image: mcr.microsoft.com/playwright:v1.45.0-jammy
    working_dir: /tests
    volumes:
      - .:/tests
    command: npx playwright test

Run once:

docker compose run --rm playwright

Cypress in Docker

services:
  cypress:
    image: cypress/included:13
    working_dir: /e2e
    volumes:
      - ./cypress:/e2e/cypress
      - ./cypress.config.ts:/e2e/cypress.config.ts
    environment:
      CYPRESS_baseUrl: http://host.docker.internal:3000

Ad-hoc test run in CI

# Start stack, run tests, tear down — single command
docker compose up -d --wait                # waits for all healthchecks
docker compose run --rm cypress
docker compose down -v

Database seeding

Run a one-shot migrate/seed container before tests:

services:
  migrate:
    image: my-app-migrate:latest
    command: ["npm", "run", "migrate:test"]
    environment:
      DATABASE_URL: postgres://postgres:dev@db:5432/app
    depends_on:
      db: { condition: service_healthy }
 
  seed:
    image: my-app-migrate:latest
    command: ["npm", "run", "seed:test"]
    depends_on:
      migrate: { condition: service_completed_successfully }
 
  cypress:
    depends_on:
      seed: { condition: service_completed_successfully }

Docker in CI/CD

GitHub Actions example

name: Tests
on: [push, pull_request]
 
jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
 
      - name: Build app image (with cache)
        uses: docker/build-push-action@v5
        with:
          context: .
          tags: my-app:ci
          load: true
          cache-from: type=gha
          cache-to: type=gha,mode=max
 
      - name: Boot stack
        run: docker compose up -d --wait
 
      - name: Run Cypress
        run: docker compose run --rm cypress
 
      - name: Upload Cypress artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: cypress-output
          path: |
            cypress/screenshots
            cypress/videos
            cypress/results
 
      - name: Tear down
        if: always()
        run: docker compose down -v

Layer caching

Slow CI builds are usually a layer-caching problem.

  • Copy lockfiles (package-lock.json, yarn.lock) and run npm ci before copying source — that way changes to source code don't bust the dependency layer.
  • In CI, persist build cache via cache-from: type=gha (GitHub Actions) or --cache-from to a registry.
  • Use Buildx: docker buildx build --cache-to=type=registry,ref=registry.example.com/cache:my-app.

Publishing test reports from inside containers

Volume-mount a host directory and write reports into it:

services:
  cypress:
    volumes:
      - ./cypress/results:/e2e/cypress/results

Then actions/upload-artifact@v4 picks them up from cypress/results after the run.

Networking for Testing

Default bridge

Compose creates a bridge network for the project automatically. Services reach each other by service name:

http://app:3000          # 'app' is the service name in compose
postgres://db:5432       # same

Custom networks

docker network create test-net
 
docker run -d --name api --network test-net my-api
docker run --rm -it --network test-net curlimages/curl curl http://api:8080/health
services:
  app:
    networks: [frontend, backend]
  db:
    networks: [backend]
 
networks:
  frontend:
  backend:

Reaching the host machine from a container

host.docker.internal     # Docker Desktop (macOS, Windows, recent Linux)

Useful when your app runs on the host and your test container needs to call it.

Port mapping

-p 3000:3000             # host_port:container_port
-p 127.0.0.1:5432:5432   # bind only to localhost (safer)
-p 3000                  # random host port → container 3000

docker port my-app lists active mappings.

Useful Docker Images for QA

ImageUse
selenium/standalone-chromeSingle-container Chrome + WebDriver. selenium/standalone-firefox and -edge available too.
selenium/hub + selenium/node-chromeSelenium Grid for parallel cross-browser runs.
mcr.microsoft.com/playwrightPre-installed Chromium, Firefox, WebKit + system deps.
cypress/includedCypress CLI + browsers + Node — single image runs cypress run out of the box.
postgres, mysql, mongoDatabase-backed integration testing.
wiremock/wiremockAPI mocking for contract / chaos testing.
mailhog/mailhogSMTP capture for email verification (http://mailhog:8025).
localstack/localstackAWS service emulator (S3, SQS, SNS, Lambda, DynamoDB).
grafana/k6Performance testing — docker run --rm grafana/k6 run script.js.
swaggerapi/swagger-uiLocal interactive API docs.
node:20-alpine / python:3.12-slimLightweight base for custom test runners.