Docker gives QA engineers a repeatable way to run applications, test dependencies and automation in isolated environments. Instead of relying on whatever happens to be installed on a laptop or CI runner, you can package the required runtime and dependencies into images and start disposable containers whenever testing needs them.

Containers in simple terms

A container is an isolated process that packages an application together with the user-space files, libraries, configuration and dependencies it needs to run. Containers share the host operating system kernel rather than booting a complete guest operating system for every application.

The useful mental model: an image is the reusable blueprint. A container is a running instance created from that image.
ConceptMeaningQA relevance
ImageRead-only package used to create containers.Lets the team test against a known application or dependency version.
ContainerRunning isolated process created from an image.Provides disposable environments for applications, databases and test tools.
RegistryService used to store and distribute images.CI can pull exactly the image that should be tested.
VolumePersistent or mounted storage used by containers.Useful for databases, reports, logs and test artefacts.
NetworkConnectivity between containers and external systems.Lets test environments reproduce multi-service application relationships.
DockerfileInstructions used to build an image.Makes the execution environment version-controlled and reproducible.
ComposeConfiguration for running multiple related services together.Excellent for local integration and end-to-end test environments.

Containers vs virtual machines

Containers and virtual machines both provide isolation, but they operate at different layers. A VM virtualises hardware and normally boots a complete guest operating system. Containers isolate processes while sharing the host kernel.

AreaContainerVirtual machine
Operating systemShares the host kernel.Runs a complete guest OS.
StartupUsually very fast.Typically slower because an OS must boot.
Resource usageGenerally lightweight.Generally heavier.
IsolationProcess-level isolation.Stronger machine-level isolation.
PortabilityExcellent when the target supports the image platform.Portable as a complete machine image, but larger.
QA useDisposable test services, runners and environments.Full OS/browser labs, legacy systems or stronger isolation needs.

They are not mutually exclusive. Docker is often run inside virtual machines in CI platforms and cloud infrastructure.

How Docker is structured

Docker follows a client-server model. The Docker CLI sends requests through the Docker API to the Docker daemon, which manages images, containers, networks and volumes.

Docker CLI │ │ Docker API ▼ Docker daemon │ ├── Images ├── Containers ├── Networks └── Volumes Registry / Docker Hub ▲ │ └─ push / pull ─┘

Docker client

The docker command you use in a terminal or script.

Docker daemon

The service that builds images and creates, starts and manages containers.

Image

A layered, reusable package from which containers are created.

Registry

A location such as Docker Hub or a private registry where images are stored.

Container

A running image with its own writable container layer and runtime configuration.

Docker Desktop

A convenient desktop environment for running Docker on Windows, macOS and Linux.

Essential Docker commands for QA

You do not need dozens of commands to start using Docker effectively in testing. These cover most everyday QA workflows.

CommandPurpose
docker pull nginxDownload an image from a registry.
docker imagesList images available locally.
docker run nginxCreate and start a container from an image.
docker run -d nginxRun a container in the background.
docker psList running containers.
docker ps -aList running and stopped containers.
docker logs <container>Read container output.
docker exec -it <container> shOpen a shell inside a running container when one is available.
docker stop <container>Stop a running container.
docker rm <container>Remove a stopped container.
docker rmi <image>Remove a local image.
docker inspect <container>Inspect detailed runtime configuration.

Useful run options

# Give the container a readable name docker run --name qa-api my-api # Map host port 8080 to container port 3000 docker run -p 8080:3000 my-api # Set an environment variable docker run -e NODE_ENV=test my-api # Remove the container automatically when it stops docker run --rm my-test-runner # Run in the background docker run -d -p 8080:3000 my-api
QA tip: use --rm for disposable test runners and one-off utilities. It keeps local and CI environments from accumulating stopped containers.

Build your own Docker image

A Dockerfile describes how an image should be built. Keeping it in source control means the environment evolves together with the application.

Simple Node.js example

FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . EXPOSE 3000 CMD ["npm", "start"]
InstructionPurpose
FROMSelects the base image.
WORKDIRSets the working directory for following instructions.
COPYCopies files from the build context into the image.
RUNExecutes a command while the image is being built.
EXPOSEDocuments the port the application expects to use.
ENVDefines environment variables in the image.
CMDDefines the default command when a container starts.
HEALTHCHECKDefines how Docker can assess container health.

Build and run it

# Build an image from the Dockerfile in the current directory docker build -t qa-demo-api:1.0 . # Run the image docker run --rm -p 3000:3000 qa-demo-api:1.0

Tags matter

Tags identify image variants or versions. Avoid depending only on latest in controlled test environments because it can move over time.

docker build -t registry.example.com/qa-demo-api:1.4.2 . docker push registry.example.com/qa-demo-api:1.4.2

Image layers and build efficiency

Docker images are built from layers. Docker can reuse unchanged layers from its build cache, so Dockerfile ordering has a direct effect on build speed.

# Better cache behaviour for Node projects COPY package*.json ./ RUN npm ci # Application code changes frequently, so copy it afterwards COPY . .

If only application source code changes, Docker can often reuse the dependency layer instead of reinstalling every package.

Use a .dockerignore file

node_modules .git playwright-report test-results coverage .env *.log

This prevents unnecessary files from being sent into the build context and accidentally copied into images.

Docker Compose for test environments

Docker Compose defines a multi-container application in YAML. It is especially useful when a test needs an application plus supporting services such as a database, cache, mock server or message broker.

Example: application + PostgreSQL

services: app: build: . ports: - "3000:3000" environment: DATABASE_URL: postgres://qa:qa@db:5432/qadb depends_on: db: condition: service_healthy db: image: postgres:17-alpine environment: POSTGRES_USER: qa POSTGRES_PASSWORD: qa POSTGRES_DB: qadb healthcheck: test: ["CMD-SHELL", "pg_isready -U qa -d qadb"] interval: 5s timeout: 5s retries: 10

Everyday Compose commands

# Build and start the stack docker compose up --build # Run in the background docker compose up -d # See service status docker compose ps # Follow logs docker compose logs -f # Stop and remove containers/networks docker compose down # Also remove named volumes when you want fresh state docker compose down -v
Modern Compose: use docker compose. Older examples may show the legacy docker-compose command and a top-level version: "3" property; modern Compose uses the Compose Specification and does not require that version field.

Why Docker is useful for QA engineers

Repeatable environments

Developers, QA and CI can run the same versions of application dependencies.

Disposable test data

Start a clean database for a test run and remove it afterwards.

Dependency control

Pin PostgreSQL, Redis, Kafka, mock services or other dependencies to known versions.

Automation runners

Package Playwright, API tests or performance tools into predictable runner images.

Parallel execution

Run multiple isolated test jobs without requiring manually prepared machines.

Reproduction

Share the exact image and configuration associated with a failure.

Common QA scenarios

ScenarioDocker approach
API testingStart the API and its database locally with Compose, then execute the API suite.
End-to-end testingStart frontend, backend, database and mock dependencies as one temporary stack.
Database compatibilityRun the same test suite against different database image versions.
Upgrade testingChange a dependency image tag and compare behaviour before and after the upgrade.
Bug reproductionPull the affected application image and reproduce with the same dependency versions.
CI executionRun tests in a clean container rather than depending on runner machine state.
Service virtualisationRun WireMock, MockServer or another test double beside the system under test.
Performance testingRun k6 or another load generator from a dedicated container.

Example: Playwright in a containerised stack

A useful pattern is to let Compose start the application dependencies and run the Playwright suite only when the application is ready.

services: web: image: my-company/web-app:1.4.2 ports: - "8080:8080" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 5s timeout: 3s retries: 20 e2e: image: mcr.microsoft.com/playwright:v1.55.0-noble working_dir: /tests volumes: - ./:/tests environment: BASE_URL: http://web:8080 depends_on: web: condition: service_healthy command: npx playwright test

The important concept is not the exact image version above. Keep the runner version aligned with the Playwright version used by your project and pin the image deliberately in real CI configuration.

Why this pattern is useful

  • The test runner gets a predictable browser/runtime environment.
  • The application is addressed by its Compose service name, such as http://web:8080.
  • Health checks prevent tests from starting simply because a container process exists.
  • The entire stack can be destroyed after the test run.
  • The same model can be executed on a developer machine and in CI.

Docker in CI/CD

Containers are particularly valuable in CI because jobs should be reproducible and independent from previous executions.

build image │ ▼ run unit tests │ ▼ publish versioned image │ ▼ start temporary test stack │ ├── API tests ├── integration tests └── UI smoke / regression │ ▼ promote the same tested image

A useful delivery rule

Where possible, build the application image once, test that exact artefact and then promote the same immutable image through later environments. Rebuilding separately for QA, staging and production introduces the possibility that each environment receives a slightly different artefact.

Example shell flow

docker build -t my-app:$CI_COMMIT_SHA . docker compose up -d npm run test:api npm run test:e2e TEST_EXIT=$? docker compose logs > docker-services.log docker compose down -v exit $TEST_EXIT
Always clean up: CI jobs that create containers, networks and volumes should remove them even when tests fail. In real pipelines, use an after_script, trap or equivalent cleanup mechanism.

Volumes, data and test isolation

Containers themselves should be considered disposable. Persistent state normally belongs in volumes or external systems.

Storage typeTypical useQA example
Container writable layerTemporary runtime state.Files that can disappear when the container is removed.
Named volumeDocker-managed persistent storage.Database state that should survive a container restart.
Bind mountMap a host file or directory into a container.Mount test source code or export Playwright reports.

For deterministic tests

  • Create known test data before execution.
  • Reset databases between suites when shared state can affect results.
  • Use unique test identifiers for tests that execute in parallel.
  • Remove volumes when the goal is a completely clean environment.
  • Never assume a container restart automatically resets persisted data.

Container networking for testers

Compose normally creates a network for the application. Services can reach each other using their service names.

services: frontend: # frontend can call http://api:3000 api: # api can call db:5432 db: image: postgres:17-alpine

This is a frequent source of confusion: localhost inside a container refers to that same container, not another Compose service and not automatically the host machine.

When debugging connection failures: first identify where the request originates. From your browser you may need localhost:8080; from one Compose service to another you normally use the service name such as api:3000.

Health checks and readiness

A running container is not necessarily a ready application. A database can still be starting, migrations may be running or an API may not yet be accepting requests.

healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 5s timeout: 3s retries: 20 start_period: 10s

For automated testing, waiting for meaningful readiness is more reliable than adding arbitrary sleeps such as “wait 20 seconds and hope the API is ready”.

Debugging failing containers

When a containerised test environment fails, work from the outside in rather than immediately blaming the automated test.

  1. Check status: docker ps -a or docker compose ps.
  2. Read logs: docker logs or docker compose logs.
  3. Inspect configuration: confirm ports, environment variables, mounts and networks.
  4. Check health: determine whether the service is running but unhealthy.
  5. Enter the container: use docker exec when an interactive shell is available.
  6. Test connectivity: check DNS/service names and whether the expected port is listening.
  7. Confirm data: migrations, seed data and credentials are common failure points.
  8. Rebuild deliberately: use docker compose build or --build if the image is stale.

Useful inspection commands

docker compose ps docker compose logs -f api docker inspect qa-api docker stats docker network ls docker volume ls docker exec -it qa-api sh

Multi-stage builds

Multi-stage builds allow one Dockerfile to use a larger environment for compilation or tests and copy only the required runtime artefacts into the final image.

FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:22-alpine AS runtime WORKDIR /app COPY --from=build /app/dist ./dist COPY package*.json ./ RUN npm ci --omit=dev CMD ["node", "dist/server.js"]

This can reduce the final image size and avoids shipping build tools and source files that the running application does not need.

Security and reliability basics

  • Pin important versions. Avoid unexpected dependency changes in test and production environments.
  • Prefer trusted base images. Review where images come from before using them in company pipelines.
  • Keep images small. Include only what the runtime or test runner needs.
  • Do not bake secrets into images. Supply credentials through an appropriate secret-management mechanism.
  • Do not commit real secrets to Compose files. Treat test credentials with the same care as other credentials.
  • Run as a non-root user where practical. Reduce unnecessary privileges.
  • Scan and update images. Containerisation does not make vulnerable dependencies safe.
  • Use health checks where readiness matters. Failures become easier to diagnose and orchestrate.
  • Clean up old resources. Stale containers, images and volumes can consume substantial disk space.

Common mistakes

MistakeWhy it causes problemsBetter approach
Using latest everywhereThe underlying image may change unexpectedly.Pin deliberate versions for repeatable test runs.
Putting secrets in a DockerfileThey can become part of image history or shared layers.Inject secrets at runtime using secure mechanisms.
Using arbitrary sleepsTests become slow and still fail on slower environments.Wait for a meaningful health/readiness condition.
Keeping dirty databases foreverState leaks between tests and causes non-deterministic behaviour.Seed and reset data deliberately.
Assuming localhost means the hostEach container has its own network namespace.Use Compose service names or explicit host networking mechanisms.
Rebuilding for every environmentYou may stop testing the same artefact that eventually reaches production.Promote the tested image where possible.
Ignoring container logs in CIA failed environment looks like a failed test.Always collect service logs as job artefacts.
Running everything in one huge imageDependencies and responsibilities become difficult to maintain.Separate application services and dedicated test runners logically.

A practical QA Docker workflow

1. Pull or build the application image │ 2. Start required dependencies with Compose │ 3. Wait for health/readiness │ 4. Seed known test data │ 5. Run API / integration / UI tests │ 6. Capture reports + service logs │ 7. Stop containers │ 8. Remove temporary volumes/networks │ 9. Publish test results

Example checklist

  • The application image version is explicit.
  • Required ports are mapped only when host access is needed.
  • Inter-container calls use service names.
  • Dependencies expose meaningful health checks.
  • Test data setup is automated.
  • Parallel test jobs do not share unsafe mutable state.
  • Container and service logs are retained on failure.
  • Tests return a non-zero exit code when they fail.
  • Cleanup runs even after a failed test.
  • The same container definitions can run locally and in CI where practical.

Useful links