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.
| Concept | Meaning | QA relevance |
|---|---|---|
| Image | Read-only package used to create containers. | Lets the team test against a known application or dependency version. |
| Container | Running isolated process created from an image. | Provides disposable environments for applications, databases and test tools. |
| Registry | Service used to store and distribute images. | CI can pull exactly the image that should be tested. |
| Volume | Persistent or mounted storage used by containers. | Useful for databases, reports, logs and test artefacts. |
| Network | Connectivity between containers and external systems. | Lets test environments reproduce multi-service application relationships. |
| Dockerfile | Instructions used to build an image. | Makes the execution environment version-controlled and reproducible. |
| Compose | Configuration 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.
| Area | Container | Virtual machine |
|---|---|---|
| Operating system | Shares the host kernel. | Runs a complete guest OS. |
| Startup | Usually very fast. | Typically slower because an OS must boot. |
| Resource usage | Generally lightweight. | Generally heavier. |
| Isolation | Process-level isolation. | Stronger machine-level isolation. |
| Portability | Excellent when the target supports the image platform. | Portable as a complete machine image, but larger. |
| QA use | Disposable 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.
| Command | Purpose |
|---|---|
docker pull nginx | Download an image from a registry. |
docker images | List images available locally. |
docker run nginx | Create and start a container from an image. |
docker run -d nginx | Run a container in the background. |
docker ps | List running containers. |
docker ps -a | List running and stopped containers. |
docker logs <container> | Read container output. |
docker exec -it <container> sh | Open 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
--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"]
| Instruction | Purpose |
|---|---|
FROM | Selects the base image. |
WORKDIR | Sets the working directory for following instructions. |
COPY | Copies files from the build context into the image. |
RUN | Executes a command while the image is being built. |
EXPOSE | Documents the port the application expects to use. |
ENV | Defines environment variables in the image. |
CMD | Defines the default command when a container starts. |
HEALTHCHECK | Defines 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
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
| Scenario | Docker approach |
|---|---|
| API testing | Start the API and its database locally with Compose, then execute the API suite. |
| End-to-end testing | Start frontend, backend, database and mock dependencies as one temporary stack. |
| Database compatibility | Run the same test suite against different database image versions. |
| Upgrade testing | Change a dependency image tag and compare behaviour before and after the upgrade. |
| Bug reproduction | Pull the affected application image and reproduce with the same dependency versions. |
| CI execution | Run tests in a clean container rather than depending on runner machine state. |
| Service virtualisation | Run WireMock, MockServer or another test double beside the system under test. |
| Performance testing | Run 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
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 type | Typical use | QA example |
|---|---|---|
| Container writable layer | Temporary runtime state. | Files that can disappear when the container is removed. |
| Named volume | Docker-managed persistent storage. | Database state that should survive a container restart. |
| Bind mount | Map 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.
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.
- Check status:
docker ps -aordocker compose ps. - Read logs:
docker logsordocker compose logs. - Inspect configuration: confirm ports, environment variables, mounts and networks.
- Check health: determine whether the service is running but unhealthy.
- Enter the container: use
docker execwhen an interactive shell is available. - Test connectivity: check DNS/service names and whether the expected port is listening.
- Confirm data: migrations, seed data and credentials are common failure points.
- Rebuild deliberately: use
docker compose buildor--buildif 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
| Mistake | Why it causes problems | Better approach |
|---|---|---|
Using latest everywhere | The underlying image may change unexpectedly. | Pin deliberate versions for repeatable test runs. |
| Putting secrets in a Dockerfile | They can become part of image history or shared layers. | Inject secrets at runtime using secure mechanisms. |
| Using arbitrary sleeps | Tests become slow and still fail on slower environments. | Wait for a meaningful health/readiness condition. |
| Keeping dirty databases forever | State leaks between tests and causes non-deterministic behaviour. | Seed and reset data deliberately. |
| Assuming localhost means the host | Each container has its own network namespace. | Use Compose service names or explicit host networking mechanisms. |
| Rebuilding for every environment | You may stop testing the same artefact that eventually reaches production. | Promote the tested image where possible. |
| Ignoring container logs in CI | A failed environment looks like a failed test. | Always collect service logs as job artefacts. |
| Running everything in one huge image | Dependencies 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
- Docker overview ↗ — platform, architecture, images, containers and registries.
- Docker concepts ↗ — foundational container workflows.
- Dockerfile reference ↗ — build instructions and syntax.
- Docker Compose ↗ — multi-container applications.
- Docker Compose quickstart ↗ — services, health checks, volumes and debugging.
- Running containers ↗ —
docker runoptions and runtime configuration. - Docker build checks ↗ — validate Dockerfile and build configuration.
- Playwright Docker ↗ — running browser automation in containers.