Docker
Package your app and its dependencies into a container that runs identically everywhere — eliminating the "works on my machine" problem forever.
1 The Hook — The Friday Night "Works on my Machine"
A dev team in Auckland was ready to ship a critical update. It passed every test on the lead developer's MacBook. But as soon as it hit the staging server (running Linux), it crashed. The reason? A tiny version difference in the database driver that only existed on Linux. The team spent all Friday night debugging environment variables and library paths instead of celebrating their release.
Docker solves this. By packaging the application, the database driver, and the exact version of the runtime into an Image, you ensure that the code running on your laptop is byte-for-byte identical to the code running in Production. Parity isn't just a goal; it's a guarantee.
2 The Rule — Parity is Priority
If it's not in the Docker image, it doesn't exist. Your test environment should be defined in code (Docker Compose).
Don't rely on "pre-installed" software on your laptop or a CI runner. Define everything your app needs in a Dockerfile and a docker-compose.yml file.
3 The Analogy — The Shipping Container
The Standard Container.
Before shipping containers, loading a ship was slow and prone to error — every box was a different size. Today, it doesn't matter if you're shipping iPhones or sheep; as long as they are inside a standard 20ft container, the crane can lift it and the ship can carry it. Docker is that standard container for your code. The "crane" (Cloud/CI) doesn't need to know what's inside; it just knows how to run the container.
Senior engineer insight
The moment that changed how I think about Docker for testing: I was on an integration project at a Wellington-based fintech and we had eight developers on eight different laptops, each running a slightly different version of PostgreSQL. Our test suite had 200 "randomly flaky" failures per week that nobody could reproduce consistently. We Dockerised the database in an afternoon and that entire class of failures vanished permanently — not reduced, gone. The container was the contract.
The second thing that shifted my thinking was health checks. We added depends_on in our Compose file and assumed the DB was ready when the API started — it wasn't. The DB container was running but not yet accepting connections. You need healthcheck and condition: service_healthy or your integration tests will flake on startup timing every single time.
The most common mistake: teams treat Docker as a deployment tool and never apply it to the test environment, so they keep the "works on my machine" problem for years while shipping containers to production.
From the field
A QA team at an NZ government digital services agency was running end-to-end tests against a shared staging environment that four squads used simultaneously. They assumed the environment was stable enough — it was managed by a platform team and "always up". What they discovered after three months of mysterious test failures was that other squads were running database migrations against the same schema mid-test-run, causing foreign key violations and phantom data that broke assertions. The tests weren't wrong; the environment was being mutated underneath them.
They switched to Docker Compose: each CI pipeline spin up its own isolated stack, runs against it, then tears it down. Flaky failures dropped from roughly thirty per hundred runs to near zero. The lesson that generalises: shared test environments are a shared liability — isolation isn't just a Docker selling point, it's the only way to make test results trustworthy at scale.
4 Watch Me Do It — A Test Environment with Docker Compose
Scenario: Spinning up a clean PostgreSQL database and a Node.js API together for a suite of integration tests.
# docker-compose.yml
version: '3.8'
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: password123
ports:
- "5432:5432"
api:
build: .
environment:
DATABASE_URL: postgres://postgres:password123@db:5432/postgres
depends_on:
- db
ports:
- "3000:3000"
With this one file, any developer can run docker-compose up and have a perfect, isolated replica of the production environment running locally in seconds.
5 Decision Tool — When to Dockerise?
✅ Dockerise when...
- You have external dependencies (DB, Redis, etc.)
- You have "works on my machine" flakiness
- You want identical local and CI environments
- You are deploying to AWS, Azure, or GCP
❌ Avoid Docker for...
- Simple "Hello World" or scripts with no deps
- Native Mobile App testing (Use emulators)
- When build speed is the ONLY priority (Docker adds overhead)
6 Common Mistakes
🚫 Using "localhost" Inside a Container
Mistake: Trying to connect to localhost:5432 from your app container to reach the DB.
Why: Inside a container, localhost refers to the container itself. Use the service name (e.g., db:5432) instead.
🚫 Massive Images
Mistake: Including your .git folder and local node_modules in the image.
Do: Use a .dockerignore file to exclude unnecessary files. This keeps images small and fast to pull.
7 Now You Try — Setup
Download Docker Desktop for Windows/Mac. Once installed, open your terminal and run the "Hello World" test:
docker run hello-world
If you see a "Hello from Docker!" message, you're ready to start containerising your test environments.
Why teams fail here
- Race conditions on startup: Using
depends_onwithouthealthcheckmeans the dependent service starts before the dependency is actually ready to accept connections — integration tests fail sporadically on CI but pass locally where the DB is already warm. - Hardcoded credentials and ports in Compose files committed to the repo: Fine for local dev; a security problem when the same file is used in staging or when the repo is public — NZ financial sector audits flag this immediately.
- Image layers bloated with test data and dev tools: Teams copy in seed SQL files, debugging utilities, and test fixtures directly into the production image rather than a separate test-only Compose override — the production image ends up 3× larger and contains data that should never ship.
- Not cleaning up volumes between test runs: Docker volumes persist between
docker-compose upruns by default, so leftover test data from a previous run poisons the next suite — tests pass in isolation, fail in sequence, and the team blames the tests rather than the environment state.
Key takeaway
A test environment you can spin up from a single command and tear down completely is not a luxury — it is the minimum viable standard for any team that wants to trust its own test results.
8 Self-Check
Q1. What is the difference between an Image and a Container?
An Image is the blueprint (the "file" you download). A Container is the running instance of that blueprint (the "process" that executes).
Q2. Do I need to install a database on my laptop if I use Docker?
No. Docker pulls the database image and runs it inside a container. You don't need to install anything on your host machine except Docker itself.
9 Interview Prep
"How does Docker help with test reproducibility?"
Answer: "Docker ensures environment parity. By defining the entire test environment — including the OS, runtime version, and dependencies — in a Dockerfile, we eliminate 'environmental noise'. If a test fails in the Docker container on my machine, it will fail the exact same way in the CI pipeline, making debugging significantly easier."
10 Summary
You've explored the modern automation stack. From UI testing with Playwright to CI/CD with GitHub Actions and environment parity with Docker, you now have the knowledge to build professional, reliable test systems.