A Docker image is an environment recipe; a container is one running copy, like identical science kits.
Dockerfile → image → container → test
Series: Playwright Zero to Expert | Demo app used throughout this series: saucedemo.com
Part 32 ended by noting that runs-on: ubuntu-latest already gives you a fresh environment, and that Docker takes this idea further by letting you fully define that environment yourself. This part explains exactly what that means, and why it matters enough to be a standard part of many real Playwright frameworks.
Images and Containers
Recall Part 4’s node_modules discussion — regenerable, disposable, defined entirely by package.json and package-lock.json. Docker applies exactly this same idea to an entire computing environment, not just a set of packages.
A Docker image is a complete, self-contained snapshot of everything needed to run something — an operating system, installed dependencies (Node.js, Playwright’s browsers and their OS-level requirements), your actual code — packaged together as a single, portable, reproducible unit.
A container is a running instance of an image — the same relationship between a class and an object from Part 20’s Page Object Model discussion, genuinely: an image is the template, a container is one specific, running instantiation of it.
Why does this matter, concretely, beyond abstraction?
Recall Part 32’s honest observation about CI environments being “fresh” by default — but “fresh” doesn’t necessarily mean “exactly what your team needs, every single time, with zero drift.” A generic ubuntu-latest image gets updated by the CI provider over time, potentially introducing subtle differences in pre-installed system libraries between one CI run and another, weeks apart — a genuinely real, if often overlooked, source of the exact “works before, mysteriously doesn’t work now, nothing in our code changed” class of problem Part 28 spent an entire part addressing from a different angle.
A Docker image you define and control yourself removes this variable entirely — the environment is exactly, precisely, byte-for-byte the same, every single time it’s used, regardless of when.
Analogy: The Standardized Cargo Shipping Container Before standardized shipping containers:
- Loose Cargo (No Docker): Workers loaded crates, bags, and items of varying shapes loose onto a cargo ship. If the ship’s cargo hold was damp, or the temperature fluctuated, the goods were damaged. If you loaded the same crates onto a truck in another country, they fit differently and slid around.
- Shipping Container (Docker Image): You pack all your cargo (operating system, Node runtime, Playwright dependencies, and test scripts) into a standardized, weather-sealed metal shipping container. It locks shut. Whether that container sits on a dock in London (local laptop), a cargo ship in the Atlantic (GitHub Actions VM), or a railcar in Tokyo (AWS container runner), the internal environment remains completely dry, identical, and sealed.
📊 Visual Flowchart: Dockerfile Build Layer Caching Optimization
Here is how Docker caches intermediate image layers to save execution time during rebuilds:
graph TD
Trigger["Run: docker build -t tests ."] --> CheckLock{"Did package.json or<br>package-lock.json change?"}
CheckLock -->|No| CacheDeps["1. Reuse cached npm ci layer<br>(Instant / skips dependency download)"]
CheckLock -->|Yes| RunDeps["1. Execute fresh npm ci command<br>(Downloads packages from npm registry)"]
CacheDeps --> CheckCode{"Did any test code<br>files change?"}
RunDeps --> CheckCode
CheckCode -->|No| CacheCode["2. Reuse cached COPY . . layer<br>(Instant)"]
CheckCode -->|Yes| RunCode["2. Execute fresh COPY . . command<br>(Copies modified scripts)"]
CacheCode --> Done["3. Generate final image tags"]
RunCode --> Done
Dockerfile — Defining Your Own Environment
FROM mcr.microsoft.com/playwright:v1.45.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
Read this the way you now read a playwright.config.ts — line by line, each existing for a specific, understandable reason:
FROM mcr.microsoft.com/playwright:v1.45.0-jammy— rather than starting from a generic, bare Ubuntu image and manually installing Node.js, Playwright, and every browser dependency yourself, Microsoft publishes an official Playwright image, already containing all of that, pre-installed and version-matched to a specific Playwright release. This is worth genuinely appreciating: it means the exact browser binaries and their exact OS dependencies are already correctly set up, removing an entire category of setup friction and version-mismatch risk before your own code is even involved.WORKDIR /app— sets the working directory inside the container, exactly analogous tocd-ing into a project folder on your own machine before running commands in it.COPY package*.json ./thenRUN npm ci— notice this happens before copying the rest of your code. This ordering is a deliberate, genuinely important optimization worth understanding: Docker caches each step, and re-uses a cached step’s result if nothing relevant to it has changed since the last build. By copying justpackage.json/package-lock.jsonfirst and installing dependencies before copying the rest of your code, a later build where only your test code changed (not your dependencies) can reuse the cachednpm cistep entirely, skipping a potentially slow reinstall — a real, meaningful speed difference for a team building this image repeatedly, many times a day.COPY . .— now copies the rest of your actual project code into the image.CMD [...]— the default command that runs when a container is actually started from this image.
docker build -t my-playwright-tests .
docker run my-playwright-tests
docker build constructs the actual image from the Dockerfile’s instructions, step by step. docker run starts a genuine, running container from that built image, executing the CMD — your entire Playwright suite running inside a completely self-contained, precisely controlled environment, identical whether it’s run on your laptop, a teammate’s laptop, or any CI machine anywhere.
Docker Compose, Briefly
For a real application under test, “the thing being tested” is often more than just your test suite alone — it might genuinely need a running instance of the application itself, plus a database, running alongside your tests. Docker Compose lets you define and coordinate multiple containers together, as one coherent, connected system:
# docker-compose.yml (simplified example)
services:
app:
build: ./app
ports:
- "3000:3000"
tests:
build: .
depends_on:
- app
environment:
- BASE_URL=http://app:3000
docker-compose up --abort-on-container-exit
Notice BASE_URL=http://app:3000 — directly, concretely applying Part 16’s environment-variable configuration pattern, but pointed here at a container running inside the same Docker Compose network, rather than a real, external URL. This is genuinely useful for a realistic, end-to-end CI setup: the application itself, freshly built from source, running alongside a fresh, isolated test run against it — no dependency at all on some separately, manually maintained staging server that might be in an unknown, inconsistent state at any given moment.
How It Works in a Real Test Run
A Docker image is the immutable recipe result containing operating-system libraries, Node, and browser dependencies. A container is one running process environment created from that image; test source and reports may be mounted or copied according to the CI design.
Match the Playwright image version with the project package version, run untrusted browsing with appropriate user and sandbox controls, use an init process, and export reports outside the disposable container.
Interview Questions
Q: What is the actual relationship between a Docker image and a Docker container?
Ans: An image is a complete, self-contained template — an operating system, dependencies, and code, packaged together. A container is a running instance of that image, in the same relationship as a class and an object: one image can be used to start many independent, identical containers.
Q: Why might a CI pipeline running on a generic ubuntu-latest image still experience “it worked before, now it mysteriously doesn’t, nothing in our code changed” problems, and how does Docker address this?
Ans: A generic base image is maintained and updated over time by the CI provider, which can introduce subtle differences in pre-installed system libraries or tool versions between runs occurring weeks apart, even with no changes to the project’s own code. A Docker image you define and control yourself is exact and unchanging — built once and reused identically every time it’s actually used — removing that source of environmental drift entirely.
Q: Why does the example Dockerfile copy package.json and run npm ci before copying the rest of the project’s code, rather than copying everything at once?
Ans: Docker caches each build step and reuses a cached result if nothing relevant to that specific step has changed since the previous build. By installing dependencies before copying the rest of the code, a later build where only test code changed — not dependencies — can reuse the cached dependency-installation step entirely, meaningfully speeding up the build process compared to reinstalling dependencies on every single build regardless of what actually changed.
Q: What genuine advantage does Microsoft’s official Playwright Docker image provide, compared to manually building your own image from a bare Ubuntu base?
Ans: It comes with Node.js, Playwright, and all the correct browser binaries and their OS-level dependencies already pre-installed and precisely version-matched to a specific Playwright release. This removes an entire category of manual setup effort and version-mismatch risk that would otherwise be required to correctly configure a bare base image to run Playwright’s browsers reliably.
Q: What problem does Docker Compose solve that a single Dockerfile alone doesn’t?
Ans: A single Dockerfile defines one image, for one container. Many real testing scenarios need multiple, coordinated pieces running together — the application itself, a database, and the test suite — as one connected system, with the ability to communicate between them (like a test suite reaching an application running in a sibling container). Docker Compose defines and coordinates exactly this kind of multi-container setup as a single, unified configuration.
Q: A team’s Playwright suite behaves inconsistently across different developers’ machines, despite everyone using the same package-lock.json. What additional factor might Docker help control for, that package-lock.json alone doesn’t cover?
Ans: package-lock.json guarantees identical Node.js package versions, but says nothing about the underlying operating system, its installed system libraries, or other OS-level dependencies — factors that can genuinely differ between developers’ machines (different OS versions, different pre-existing system configurations) and can affect how browsers actually render or behave. Running the suite inside a Docker container built from a shared, precisely defined image ensures the entire environment — not just the Node.js package versions — is identical across every machine it runs on.
Exercises — Part 33
Understand:
Explain, in your own words, why “the same package-lock.json” doesn’t automatically guarantee “the exact same environment” across two different machines, and what Docker specifically adds on top of that guarantee.
Simple Practice:
Write a Dockerfile for a Playwright project you’ve built earlier in this series, using the official Microsoft Playwright base image, and successfully build and run it locally using docker build and docker run.
Real-World Scenario:
Design (in writing, pseudocode/YAML is fine) a docker-compose.yml for a hypothetical scenario where your own small web application (built from a local Dockerfile) needs to be running before your Playwright tests can execute against it, using the depends_on and environment-variable pattern shown in this part.
Challenge:
Research how to integrate a Dockerfile-based Playwright setup into the GitHub Actions workflow from Part 32 — specifically, running your test suite inside your custom Docker image as part of the CI pipeline, rather than relying on GitHub’s generic ubuntu-latest runner installing Playwright directly.
Next: Part 34 — Reporting
— built-in reporters, Allure integration, and getting CI failures noticed by the team automatically, not just recorded silently.
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed