Containers
Starting a container
Create and start any container using a Generic Container:
const { GenericContainer } = require("testcontainers");
const container = await new GenericContainer("alpine").start();
To use a specific image version:
const container = await new GenericContainer("alpine:3.10").start();
With a pull policy
Testcontainers will automatically pull an image if it doesn't exist. This is configurable:
const { GenericContainer, PullPolicy } = require("testcontainers");
const container = await new GenericContainer("alpine")
.withPullPolicy(PullPolicy.alwaysPull())
.start();
Create a custom pull policy:
const { GenericContainer, ImagePullPolicy } = require("testcontainers");
class CustomPullPolicy implements ImagePullPolicy {
public shouldPull(): boolean {
return true;
}
}
const container = await new GenericContainer("alpine")
.withPullPolicy(new CustomPullPolicy())
.start();
With a command
const container = await new GenericContainer("alpine")
.withCommand(["sleep", "infinity"])
.start();
With an entrypoint
const container = await new GenericContainer("alpine")
.withEntrypoint(["cat"])
.start();
With environment variables
const container = await new GenericContainer("alpine")
.withEnvironment({ ENV: "VALUE" })
.start();
With a platform
const container = await new GenericContainer("alpine")
.withPlatform("linux/arm64") // similar to `--platform linux/arm64`
.start();
With bind mounts
Not recommended.
Bind mounts are not portable. They do not work with Docker in Docker or in cases where the Docker agent is remote. It is preferred to copy files/directories/content into the container instead.
const container = await new GenericContainer("alpine")
.withBindMounts([{
source: "/local/file.txt",
target:"/remote/file.txt"
}, {
source: "/local/dir",
target:"/remote/dir",
mode: "ro"
}])
.start();
With labels
const container = await new GenericContainer("alpine")
.withLabels({ label: "value" })
.start();
With a name
Not recommended.
If a container with the same name already exists, Docker will raise a conflict. If you are specifying a name to enable container to container communication, look into creating a network and using network aliases.
const container = await new GenericContainer("alpine")
.withName("custom-container-name")
.start();
With files/directories/content
Copy files/directories or content to a container before it starts:
const container = await new GenericContainer("alpine")
.withCopyFilesToContainer([{
source: "/local/file.txt",
target: "/remote/file1.txt"
}])
.withCopyDirectoriesToContainer([{
source: "/localdir",
target: "/some/nested/remotedir"
}])
.withCopyContentToContainer([{
content: "hello world",
target: "/remote/file2.txt"
}])
.start();
Or after it starts:
const container = await new GenericContainer("alpine").start();
container.copyFilesToContainer([{
source: "/local/file.txt",
target: "/remote/file1.txt"
}])
container.copyDirectoriesToContainer([{
source: "/localdir",
target: "/some/nested/remotedir"
}])
container.copyContentToContainer([{
content: "hello world",
target: "/remote/file2.txt"
}])
An optional mode
can be specified in octal for setting file permissions:
const container = await new GenericContainer("alpine")
.withCopyFilesToContainer([{
source: "/local/file.txt",
target: "/remote/file1.txt",
mode: parseInt("0644", 8)
}])
.withCopyDirectoriesToContainer([{
source: "/localdir",
target: "/some/nested/remotedir",
mode: parseInt("0644", 8)
}])
.withCopyContentToContainer([{
content: "hello world",
target: "/remote/file2.txt",
mode: parseInt("0644", 8)
}])
.start();
Copy archive from container
Files and directories can be fetched from a started or stopped container as a tar archive. The archive is returned as a readable stream:
const container = await new GenericContainer("alpine").start();
const tarArchiveStream = await container.copyArchiveFromContainer("/var/log")
And when a container is stopped but not removed:
const container = await new GenericContainer("alpine").start();
const stoppedContainer = await container.stop({ remove: false });
const tarArchiveStream = await stoppedContainer.copyArchiveFromContainer("/var/log/syslog")
With working directory
const container = await new GenericContainer("alpine")
.withWorkingDir("/opt")
.start();
With default log driver
May be necessary when the driver of your docker host does not support reading logs, and you want to use the log output wait strategy.
See log drivers.
const container = await new GenericContainer("alpine")
.withDefaultLogDriver()
.start();
With a tmpfs mount
const container = await new GenericContainer("alpine")
.withTmpFs({ "/temp_pgdata": "rw,noexec,nosuid,size=65536k" })
.start();
With user
Value can be a username or UID (format: <name|uid>[:<group|gid>]
).
const container = await new GenericContainer("alpine")
.withUser("bob")
.start();
With privileged mode
const container = await new GenericContainer("alpine")
.withPrivilegedMode()
.start();
With added capabilities
See capabilities.
const container = await new GenericContainer("alpine")
.withAddedCapabilities("NET_ADMIN", "IPC_LOCK")
.start();
With dropped capabilities
See capabilities.
const container = await new GenericContainer("alpine")
.withDroppedCapabilities("NET_ADMIN", "IPC_LOCK")
.start();
With ulimits
Not supported in rootless container runtimes.
const container = await new GenericContainer("aline")
.withUlimits({
memlock: {
hard: -1,
soft: -1
}
})
.start();
With IPC mode
See IPC mode.
const container = await new GenericContainer("alpine")
.withIpcMode("host")
.start();
With resources quota
Not supported in rootless container runtimes.
See NanoCpu and Memory in ContainerCreate method.
- Memory – Limit in Gigabytes
- CPU – Quota in units of CPUs
const container = await new GenericContainer("alpine")
.withResourcesQuota({ memory: 0.5, cpu: 1 })
.start();
With shared memory size
const container = await new GenericContainer("alpine")
.withSharedMemorySize(512 * 1024 * 1024)
.start();
Stopping a container
Testcontainers by default will not wait until the container has stopped. It will simply issue the stop command and return immediately. This is to save time when running tests.
const container = await new GenericContainer("alpine").start();
await container.stop();
If you need to wait for the container to be stopped, you can provide a timeout:
const container = await new GenericContainer("alpine").start();
await container.stop({ timeout: 10000 }); // ms
You can disable automatic removal of the container, which is useful for debugging, or if for example you want to copy content from the container once it has stopped:
const container = await new GenericContainer("alpine").start();
await container.stop({ remove: false });
Volumes created by the container are removed when stopped. This is configurable:
const container = await new GenericContainer("alpine").start();
await container.stop({ removeVolumes: false });
Restarting a container
const container = await new GenericContainer("alpine").start();
await container.restart();
Reusing a container
Enabling container re-use means that Testcontainers will not start a new container if a Testcontainers managed container with the same configuration is already running.
This is useful for example if you want to share a container across tests without global set up.
const container1 = await new GenericContainer("alpine")
.withCommand(["sleep", "infinity"])
.withReuse()
.start();
const container2 = await new GenericContainer("alpine")
.withCommand(["sleep", "infinity"])
.withReuse()
.start();
expect(container1.getId()).toBe(container2.getId());
Creating a custom container
You can create your own Generic Container as follows:
import {
GenericContainer,
TestContainer,
StartedTestContainer,
AbstractStartedContainer
} from "testcontainers";
class CustomContainer extends GenericContainer {
constructor() {
super("alpine");
}
public withCustomMethod(): this {
// ...
return this;
}
public override async start(): Promise<StartedCustomContainer> {
return new StartedCustomContainer(await super.start());
}
}
class StartedCustomContainer extends AbstractStartedContainer {
constructor(startedTestContainer: StartedTestContainer) {
super(startedTestContainer);
}
public withCustomMethod(): void {
// ...
}
}
const customContainer: TestContainer = new CustomContainer();
const startedCustomContainer: StartedTestContainer = await customContainer.start();
Lifecycle callbacks
Define your own lifecycle callbacks for better control over your custom containers:
import {
GenericContainer,
AbstractStartedContainer,
StartedTestContainer,
InspectResult
} from "testcontainers";
class CustomContainer extends GenericContainer {
protected override async beforeContainerCreated(): Promise<void> {
// ...
}
protected override async containerCreated(containerId: string): Promise<void> {
// ...
}
protected override async containerStarting(
inspectResult: InspectResult,
reused: boolean
): Promise<void> {
// ...
}
protected override async containerStarted(
container: StartedTestContainer,
inspectResult: InspectResult,
reused: boolean
): Promise<void> {
// ...
}
public override async start(): Promise<CustomStartedContainer> {
return new CustomStartedContainer(await super.start());
}
}
class CustomStartedContainer extends AbstractStartedContainer {
protected override async containerStopping(): Promise<void> {
// ...
}
protected override async containerStopped(): Promise<void> {
// ...
}
}
Exposing container ports
Specify which container ports you want accessible by the host:
const container = await new GenericContainer("alpine")
.withExposedPorts(22, 80, 443)
.start();
Testcontainers will automatically bind an available, random port on the host to each exposed container port. This is to avoid port conflicts when running tests quickly or in parallel.
Retrieve the mapped port as follows:
const container = await new GenericContainer("alpine")
.withExposedPorts(80)
.start();
const httpPort = container.getMappedPort(80);
If a container exposes a single port, you can use the following convenience method:
const container = await new GenericContainer("alpine")
.withExposedPorts(80)
.start();
const httpPort = container.getFirstMappedPort();
Specify fixed host port bindings (not recommended):
const container = await new GenericContainer("alpine")
.withExposedPorts({
container: 80,
host: 80
})
.start();
Running commands
To run a command inside an already started container use the exec
method. The command will be run in the container's
working directory, returning the command output and exit code:
const container = await new GenericContainer("alpine")
.withCommand(["sleep", "infinity"])
.start();
const { output, exitCode } = await container.exec(["echo", "hello", "world"]);
The following options can be provided to modify the command execution:
-
user
: The user, and optionally, group to run the exec process inside the container. Format is one of:user
,user:group
,uid
, oruid:gid
. -
workingDir
: The working directory for the exec process inside the container. -
env
: A map of environment variables to set inside the container.
const container = await new GenericContainer("alpine")
.withCommand(["sleep", "infinity"])
.start();
const { output, exitCode } = await container.exec(["echo", "hello", "world"], {
workingDir: "/app/src/",
user: "1000:1000",
env: {
"VAR1": "enabled",
"VAR2": "/app/debug.log",
}
});
Streaming logs
Logs can be consumed either from a started container:
const container = await new GenericContainer("alpine").start();
(await container.logs())
.on("data", line => console.log(line))
.on("err", line => console.error(line))
.on("end", () => console.log("Stream closed"));
Or a consumer can be provided before start. This is useful for example if your container is failing to start:
const container = await new GenericContainer("alpine")
.withLogConsumer(stream => {
stream.on("data", line => console.log(line));
stream.on("err", line => console.error(line));
stream.on("end", () => console.log("Stream closed"));
})
.start();
You can specify a point in time as a UNIX timestamp from which you want the logs to start:
const msInSec = 1000;
const tenSecondsAgoMs = new Date().getTime() - 10 * msInSec;
const since = tenSecondsAgoMs / msInSec;
(await container.logs({ since }))
.on("data", line => console.log(line))