mrkeyoor.com_
Wed 23 Sept 00:34 UTC
npmTestingupdated 22 Sept 2026

testcontainers review

testcontainers 12.1.0 controls disposable Docker-compatible containers from Node tests. `GenericContainer` pulls or builds an image, publishes random host ports, waits for readiness, and returns connection details. The package also manages networks, Compose projects, file copies, commands, and cleanup through Ryuk; service-specific `@testcontainers/*` packages add database or broker helpers. Version 12.1 adds MongoDB Atlas Local and Mosquitto modules. Our base-package check loaded through both CommonJS and ESM, but it is a Node-only tool and brought a sizeable dependency tree.

Verdict

testcontainers 12.1.0 took 27.4 seconds, installed 142 packages using 47 MB, and produced 0 audit findings in our sandbox; both Node module paths loaded, while the browser build failed. Add it for real-service integration tests only when every developer and CI runner has proven daemon, image, readiness, and cleanup wiring.

We installed it

Lab card: what happened when we installed testcontainersScreenshot of testcontainers documentation
Install✓ · 27.4s142 packages on disk · 47 MB · 1 deprecation warning
ImportESM import works · require() works · CommonJS package
Browsern/acould not be bundled for the browser (Node-only code, most likely)
Typesno TypeScript types found
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does testcontainers install cleanly?

Yes. In a fresh container with an empty cache, npm install testcontainers finished in 27 seconds, leaving 142 packages and 47 MB on disk. npm audit reported no known vulnerabilities. The install printed 1 deprecation warning.

Can testcontainers run in a browser?

Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.

Does testcontainers work with both ESM and CommonJS?

Yes. Both import 'testcontainers' and require('testcontainers') worked in Node 22 in our run. The package is published as CommonJS.

Does testcontainers include TypeScript types?

No type declarations were found in our install, so TypeScript users need their own declarations.

testcontainers or dockerode: which should you use?

dockerode: Use it when direct Docker API access is needed and the team will implement waits, port lookup, and cleanup itself. testcontainers 12.1.0 took 27.4 seconds, installed 142 packages using 47 MB, and produced 0 audit findings in our sandbox; both Node module paths loaded, while the browser build failed.

When should you not use testcontainers?

The runner cannot reach a Docker, Podman, or compatible daemon. testcontainers is a client and has no in-process container engine.

API stability4/5`GenericContainer`, fluent configuration, started-container accessors, `Wait` factories, networks, and Compose support remain recognizable across current releases. Major version 12 did make 2 material changes: Node 20 was dropped for a Node 22.22 floor, and image health checks replaced listening ports as the first default when available. Explicit wait strategies and pinned runtime assumptions reduce migration surprises.
Docs5/5The Node documentation explains each wait strategy, Docker and Podman setup, Colima and Rancher Desktop failures, daemon environment variables, networks, Compose teardown, copies versus bind mounts, reuse, Ryuk, image pulls, and host-port exposure. It labels fixed names, bind mounts, and custom hostnames as poor defaults. The pages include exact environment variables and runnable code instead of hiding runtime limits in an FAQ.
Maintenance5/5npm published 12.1.0 on August 4, 2026, and GitHub records a push on August 24, 2026. The repository is unarchived, has 2,591 stars, and reports 7 open issues and pull requests. The latest release added MongoDB Atlas Local and Mosquitto support, declared the Node floor, updated audit-sensitive dependencies, and followed a 12.0 series with Podman, exec-output, wait, and module fixes.
Ecosystem5/5The npm downloads endpoint counted 6,031,197 installations in the latest completed week. Official modules cover many databases, brokers, emulators, and infrastructure services, while `GenericContainer` accepts an arbitrary OCI image. That reach comes with infrastructure dependencies outside npm: container runtimes, registries, helper images, sockets, host networking, and CI privileges all affect whether the same test passes on another machine.

Use it if

  • An integration test must run against the same database, broker, search engine, or emulator family used in production.
  • Developers and CI runners can expose a Docker-compatible daemon and pull pinned images from the required registry.
  • Random host ports and isolated containers are needed so several test workers can run without fixed-port collisions.
  • A custom OCI image needs lifecycle, readiness, networking, logs, and cleanup from the test process.
Skip it if

Setup reality

We installed testcontainers 12.1.0 in a fresh Node 22 Bookworm sandbox. npm took 27.4 seconds, printed 1 deprecation warning, and left 142 packages using 47 MB. The package declared 15 direct dependencies, 0 peers, and 908 KB unpacked. npm audit found 0 vulnerabilities at all severities. It requires Node 22.22 or newer, and our package scan found no TypeScript declarations.

Both CommonJS require() and ESM import worked under Node 22.23.2 even though the package has no exports map. Our esbuild browser build failed, which fits a library that talks to a container daemon and local sockets. A successful import does not prove tests can start: Docker must be reachable through the local socket or a configured DOCKER_HOST, with registry credentials supplied through Docker configuration when images are private.

Readiness changed in major version 12. An image health check is now the default when present; otherwise Testcontainers waits for mapped ports for up to 60 seconds. Many services accept a socket before recovery or migrations finish, so add a log, HTTP, command, or composite wait tied to an application-level signal. Always read getHost() and getMappedPort() after startup instead of assuming localhost and the container port.

Ryuk removes leaked resources when the process exits unexpectedly, but runtime wiring varies. Rootless Podman on macOS requires TESTCONTAINERS_RYUK_DISABLED=true; Colima and Rancher Desktop document IPv6 resolution and delayed port-forwarding problems. Stop containers and networks in teardown anyway. Container reuse is enabled by default when TESTCONTAINERS_REUSE_ENABLE is unset, so disable it for state-isolation tests and pin every image tag to keep CI runs repeatable.

Patterns

Start Redis on a random host port start-generic-container

import { GenericContainer } from 'testcontainers'

const redis = await new GenericContainer('redis:8.2-alpine')
  .withExposedPorts(6379)
  .start()

try {
  const url = `redis://${redis.getHost()}:${redis.getMappedPort(6379)}`
  await runTests(url)
} finally {
  await redis.stop()
}

Read both values after startup; the 1 internal port is rarely the host port assigned by the runtime.

Pass disposable database settings configure-service

const postgres = await new GenericContainer('postgres:18-alpine')
  .withEnvironment({
    POSTGRES_USER: 'test',
    POSTGRES_PASSWORD: 'test',
    POSTGRES_DB: 'app_test',
  })
  .withExposedPorts(5432)
  .start()

These 3 credentials belong only to the disposable test instance; use the PostgreSQL module when its URI helper is useful.

Wait for a readiness log wait-for-log

import { GenericContainer, Wait } from 'testcontainers'

const app = await new GenericContainer('example/api:1.4.2')
  .withExposedPorts(8080)
  .withWaitStrategy(Wait.forLogMessage(/ready on port 8080/i))
  .withStartupTimeout(120_000)
  .start()

Match 1 stable readiness message; incidental startup logs can let a test run before recovery finishes.

Probe an application health route wait-for-http

const api = await new GenericContainer('example/api:1.4.2')
  .withExposedPorts(8080)
  .withWaitStrategy(
    Wait.forHttp('/health/ready', 8080)
      .forStatusCode(200)
      .forResponsePredicate((body) => body === 'ready')
  )
  .start()

The probe targets container port 8080 and waits for both the status and body conditions.

Wait for a port and log together combine-waits

const wait = Wait.forAll([
  Wait.forListeningPorts(),
  Wait.forLogMessage('Ready to accept connections'),
]).withDeadline(90_000)

const redis = await new GenericContainer('redis:8.2-alpine')
  .withExposedPorts(6379)
  .withWaitStrategy(wait)
  .start()

The 90-second deadline caps the composite; without it, inner wait timeouts can lengthen the total.

Check a file inside the container execute-command

const result = await container.exec([
  'sh', '-c', 'test -f /opt/app/config.json && echo ready',
])

if (result.exitCode !== 0) {
  throw new Error(result.stderr)
}

`exec()` returns the 1 exit code; it does not automatically fail the JavaScript test for a nonzero result.

Copy configuration before startup copy-config

const nginx = await new GenericContainer('nginx:1.29-alpine')
  .withCopyContentToContainer([{
    content: 'server { listen 8080; location / { return 200 "ok"; } }',
    target: '/etc/nginx/conf.d/default.conf',
    mode: parseInt('0644', 8),
  }])
  .withExposedPorts(8080)
  .start()

A copied file works with remote daemons and Docker-in-Docker; 1 host bind mount may not.

Connect services with an alias connect-network

const network = await new Network().start()
const redis = await new GenericContainer('redis:8.2-alpine')
  .withNetwork(network)
  .withNetworkAliases('cache')
  .start()

const worker = await new GenericContainer('example/worker:2.0.0')
  .withNetwork(network)
  .withEnvironment({ REDIS_URL: 'redis://cache:6379' })
  .start()

The alias works between containers on 1 network; host-side test code still uses mapped ports.

Call a host server from a container expose-host-service

import { TestContainers } from 'testcontainers'

await TestContainers.exposeHostPorts(8000)
const client = await new GenericContainer('curlimages/curl:8.16.0')
  .withCommand([
    'curl', '-f', 'http://host.testcontainers.internal:8000'
  ])
  .start()

Start the host listener before exposing its 1 port; Testcontainers creates an SSH helper for this route.

Get a PostgreSQL connection URI use-postgresql-module

import { PostgreSqlContainer } from '@testcontainers/postgresql'

const postgres = await new PostgreSqlContainer('postgres:18-alpine')
  .withDatabase('app_test')
  .withUsername('test')
  .withPassword('test')
  .start()

const uri = postgres.getConnectionUri()

Install the companion module separately; the base package does not export this 1 service wrapper.

Attach logs during a failing test stream-container-logs

const stream = await container.logs()
stream
  .on('data', (line) => process.stdout.write(`[service] ${line}`))
  .on('err', (line) => process.stderr.write(`[service] ${line}`))

Container output can expose test credentials and payloads, so do not retain the full stream from every CI run.

Wait for a migration to exit run-one-shot-job

const migration = await new GenericContainer('example/migrations:3.1.0')
  .withEnvironment({ DATABASE_URL: internalDatabaseUrl })
  .withWaitStrategy(Wait.forOneShotStartup())
  .start()

The 1-shot strategy treats exit code 0 as readiness; it is wrong for a service that should keep running.

Alternatives

PackageRegistryPick it when
dockerodenpmUse it when direct Docker API access is needed and the team will implement waits, port lookup, and cleanup itself.
docker-composenpmUse it when the complete test stack already lives in Compose and JavaScript only needs project up and down controls.
mongodb-memory-servernpmUse it for MongoDB-only tests where a specialized ephemeral server is enough and a general container API adds little.
@testcontainers/postgresqlnpmUse this companion instead of raw `GenericContainer` when PostgreSQL credentials and connection-URI helpers save setup code.

More testing guides

pytest · chai · jsdom · vitest · playwright · coverage · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.