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
| Concept | What it is |
|---|---|
| Image | A read-only blueprint — code + dependencies + config baked together. Identified by name:tag (e.g. postgres:16). |
| Container | A running instance of an image. Many containers from one image. Cheap to create and destroy. |
| Dockerfile | The recipe for building an image — FROM base, copy code, install deps, set entry point. |
| Registry | Where images live: Docker Hub (public), GitHub Container Registry, AWS ECR, etc. |
| Volume | Persistent storage that survives container removal. Use for DB data, test artifacts. |
| Network | A 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 bashInspect
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 containerLifecycle
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 imageShell into a running container
docker exec -it test-db bash
docker exec -it test-db psql -U postgresCleanup
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 + volumesDisk usage
docker system df
docker system df -v # per-image and per-volume breakdownDockerfile 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:devMulti-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:8080From 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=abc123Or explicitly per-service:
services:
app:
env_file:
- .env.testdepends_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: 4443Scale browser nodes for parallelism:
docker compose up -d --scale chrome=4 --scale firefox=2Playwright in Docker
services:
playwright:
image: mcr.microsoft.com/playwright:v1.45.0-jammy
working_dir: /tests
volumes:
- .:/tests
command: npx playwright testRun once:
docker compose run --rm playwrightCypress 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:3000Ad-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 -vDatabase 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 -vLayer caching
Slow CI builds are usually a layer-caching problem.
- Copy lockfiles (
package-lock.json,yarn.lock) and runnpm cibefore 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-fromto 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/resultsThen 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/healthservices:
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
| Image | Use |
|---|---|
selenium/standalone-chrome | Single-container Chrome + WebDriver. selenium/standalone-firefox and -edge available too. |
selenium/hub + selenium/node-chrome | Selenium Grid for parallel cross-browser runs. |
mcr.microsoft.com/playwright | Pre-installed Chromium, Firefox, WebKit + system deps. |
cypress/included | Cypress CLI + browsers + Node — single image runs cypress run out of the box. |
postgres, mysql, mongo | Database-backed integration testing. |
wiremock/wiremock | API mocking for contract / chaos testing. |
mailhog/mailhog | SMTP capture for email verification (http://mailhog:8025). |
localstack/localstack | AWS service emulator (S3, SQS, SNS, Lambda, DynamoDB). |
grafana/k6 | Performance testing — docker run --rm grafana/k6 run script.js. |
swaggerapi/swagger-ui | Local interactive API docs. |
node:20-alpine / python:3.12-slim | Lightweight base for custom test runners. |