Real Docker Containers in Playwright Tests — Zero Boilerplate

You want real infrastructure in your integration tests. You don’t want to write cleanup code. Here is how to get both.

The problem with containers in Playwright today

Testcontainers works great in Node.js, but fitting it into Playwright req…


This content originally appeared on DEV Community and was authored by Vitali Haradkou

You want real infrastructure in your integration tests. You don't want to write cleanup code. Here is how to get both.

The problem with containers in Playwright today

Testcontainers works great in Node.js, but fitting it into Playwright requires manual lifecycle management:

// the old way — lots of ceremony
let container: StartedTestContainer;

test.beforeAll(async () => {
  container = await new GenericContainer("redis:8")
    .withExposedPorts(6379)
    .start();
});

test.afterAll(async () => {
  await container.stop(); // what if beforeAll threw halfway through?
});

test("my test", async () => {
  const port = container.getMappedPort(6379);
  // ...
});

Problems:

  • If beforeAll fails midway, afterAll still runs and may throw on container.stop() against an undefined value.
  • All tests in the file share one container — isolation suffers.
  • The container setup has nothing to do with what you're testing, but it's taking up a third of your file.

The new way

npm install -D @playwright-labs/fixture-testcontainers testcontainers
import { test } from "@playwright-labs/fixture-testcontainers";

test("redis test", async ({ useContainer }) => {
  const container = await useContainer("redis:8", { ports: 6379 });
  const port = container.getMappedPort(6379);
  // container.stop() is called automatically after the test
});

That's it. One import change, and you get:

  • ✅ Container starts when the test needs it
  • ✅ Container stops after the test ends (even on failure)
  • ✅ Multiple containers tracked and stopped in parallel
  • ✅ Full ContainerOpts support — all GenericContainer.with* methods

Quick examples

Postgres with wait strategy

import { test } from "@playwright-labs/fixture-testcontainers";
import { Wait } from "testcontainers";

test("postgres integration", async ({ useContainer }) => {
  const pg = await useContainer("postgres:16", {
    ports: 5432,
    environment: { POSTGRES_PASSWORD: "secret" },
    waitStrategy: Wait.forLogMessage("ready to accept connections"),
    startupTimeout: 30_000,
  });

  const connStr = `postgresql://postgres:secret@localhost:${pg.getMappedPort(5432)}/postgres`;
  // connect and run queries
});

Multiple containers at once

test("full stack", async ({ useContainer }) => {
  const [redis, pg] = await Promise.all([
    useContainer("redis:8", { ports: 6379 }),
    useContainer("postgres:16", {
      ports: 5432,
      environment: { POSTGRES_PASSWORD: "secret" },
    }),
  ]);
  // both stop in parallel after the test
});

Build from Dockerfile

test("custom service", async ({ useContainerFromDockerFile }) => {
  const app = await useContainerFromDockerFile("./docker", "Dockerfile", {
    ports: 3000,
    waitStrategy: Wait.forHttp("/health", 3000),
  });
});

Compose with your own fixtures

Because useContainer is a Playwright fixture, it plugs directly into your existing fixture chain:

// fixtures.ts
import { test as base } from "@playwright-labs/fixture-testcontainers";

export const test = base.extend<{ redisUrl: string }>({
  redisUrl: async ({ useContainer }, use) => {
    const container = await useContainer("redis:8", { ports: 6379 });
    await use(`redis://${container.getHost()}:${container.getMappedPort(6379)}`);
  },
});

// my.spec.ts — tests never touch Docker at all
import { test } from "./fixtures";

test("cache behavior", async ({ redisUrl }) => {
  // just use the URL
});

Custom matchers

Import expect from the package to unlock container-specific assertions:

import { test, expect } from "@playwright-labs/fixture-testcontainers";

test("container assertions", async ({ useContainer }) => {
  const container = await useContainer("postgres:16", {
    ports: 5432,
    environment: { POSTGRES_PASSWORD: "secret" },
    healthCheck: { test: ["CMD-SHELL", "pg_isready -U postgres"], interval: 1_000, retries: 5 },
    waitStrategy: Wait.forHealthCheck(),
  });

  await expect(container).toBeContainerRunning();
  await expect(container).toBeContainerHealthy();
  expect(container).toBeContainerPort(5432);
  await expect(container).toMatchContainerLogMessage("ready to accept connections");
  expect(container).toMatchContainerPortInRange(5432, { min: 1024 });
});

Full matcher list:

Matcher What it checks
toBeContainerRunning() State.Running === true
toBeContainerStarted() State.Status === "running"
toBeContainerStopped() State.Status === "exited"
toBeContainerHealthy() State.Health.Status === "healthy"
toMatchContainerLogMessage(pattern) Logs contain string or match RegExp
toBeContainerPort(port) Port is exposed and mapped
toMatchContainerPortInRange(port, range?) Mapped port is within bounds
toHaveContainerLabel(key, value?) Label exists (optionally with value)
toHaveContainerName(name) Exact name match
toMatchContainerName(pattern) Name contains / matches RegExp
toHaveContainerNetwork(name) Connected to the network
toHaveContainerUser(user?) Exact user or any non-empty user
toMatchContainerUser(pattern) User contains / matches RegExp

All .not variants are supported.

Requirements

  • @playwright/test >= 1.57.0
  • testcontainers >= 10.0.0
  • Docker (local or CI)

Source: github.com/vitalics/playwright-labs

If you've been putting off writing integration tests because of the Docker lifecycle boilerplate, this is the package that removes that excuse. Give it a try and let me know what you think in the comments!


This content originally appeared on DEV Community and was authored by Vitali Haradkou


Print Share Comment Cite Upload Translate Updates
APA

Vitali Haradkou | Sciencx (2026-03-27T09:30:31+00:00) Real Docker Containers in Playwright Tests — Zero Boilerplate. Retrieved from https://www.scien.cx/2026/03/27/real-docker-containers-in-playwright-tests-zero-boilerplate/

MLA
" » Real Docker Containers in Playwright Tests — Zero Boilerplate." Vitali Haradkou | Sciencx - Friday March 27, 2026, https://www.scien.cx/2026/03/27/real-docker-containers-in-playwright-tests-zero-boilerplate/
HARVARD
Vitali Haradkou | Sciencx Friday March 27, 2026 » Real Docker Containers in Playwright Tests — Zero Boilerplate., viewed ,<https://www.scien.cx/2026/03/27/real-docker-containers-in-playwright-tests-zero-boilerplate/>
VANCOUVER
Vitali Haradkou | Sciencx - » Real Docker Containers in Playwright Tests — Zero Boilerplate. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2026/03/27/real-docker-containers-in-playwright-tests-zero-boilerplate/
CHICAGO
" » Real Docker Containers in Playwright Tests — Zero Boilerplate." Vitali Haradkou | Sciencx - Accessed . https://www.scien.cx/2026/03/27/real-docker-containers-in-playwright-tests-zero-boilerplate/
IEEE
" » Real Docker Containers in Playwright Tests — Zero Boilerplate." Vitali Haradkou | Sciencx [Online]. Available: https://www.scien.cx/2026/03/27/real-docker-containers-in-playwright-tests-zero-boilerplate/. [Accessed: ]
rf:citation
» Real Docker Containers in Playwright Tests — Zero Boilerplate | Vitali Haradkou | Sciencx | https://www.scien.cx/2026/03/27/real-docker-containers-in-playwright-tests-zero-boilerplate/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.