CI/CD Language Docker PRs Welcome

Testcontainers for Perl 5

Perl 5 implementation of Testcontainers, inspired by the Go reference implementation.

Testcontainers makes it simple to create and clean up container-based dependencies for automated integration/smoke tests.

Requirements

Installation

cpanm --installdeps .
perl Build.PL && ./Build && ./Build install

Quick Start

use Testcontainers qw( run );
use Testcontainers::Wait;

# Run an nginx container
my $container = run('nginx:alpine',
    exposed_ports => ['80/tcp'],
    wait_for      => Testcontainers::Wait::for_listening_port('80/tcp'),
);

# Get connection details
my $host = $container->host;        # "localhost"
my $port = $container->mapped_port('80/tcp');  # e.g., "32789"

# Use the container...
use HTTP::Tiny;
my $response = HTTP::Tiny->new->get("http://$host:$port/");
say $response->{status};  # 200

# Clean up when done
$container->terminate;

Container Modules

Pre-built modules provide sensible defaults for popular services:

PostgreSQL

use Testcontainers::Module::PostgreSQL qw( postgres_container );

my $pg = postgres_container(
    username => 'myuser',
    password => 'mypass',
    database => 'mydb',
);

my $dsn  = $pg->dsn;               # "dbi:Pg:dbname=mydb;host=localhost;port=32789"
my $conn = $pg->connection_string;  # "postgresql://myuser:mypass@localhost:32789/mydb"

# Use with DBI
use DBI;
my $dbh = DBI->connect($dsn, 'myuser', 'mypass');

$pg->terminate;

MySQL

use Testcontainers::Module::MySQL qw( mysql_container );

my $mysql = mysql_container(
    username => 'myuser',
    password => 'mypass',
    database => 'mydb',
);

my $dsn = $mysql->dsn;  # "dbi:mysql:database=mydb;host=localhost;port=32790"
$mysql->terminate;

Redis

use Testcontainers::Module::Redis qw( redis_container );

my $redis = redis_container();
my $url = $redis->connection_string;  # "redis://localhost:32791"
$redis->terminate;

Nginx

use Testcontainers::Module::Nginx qw( nginx_container );

my $nginx = nginx_container();
my $url = $nginx->base_url;  # "http://localhost:32792"
$nginx->terminate;

Wait Strategies

Wait strategies determine when a container is "ready" for use:

use Testcontainers::Wait;

# Wait for a TCP port to be listening
Testcontainers::Wait::for_listening_port('5432/tcp');

# Wait for the lowest exposed port
Testcontainers::Wait::for_exposed_port();

# Wait for an HTTP endpoint
Testcontainers::Wait::for_http('/health');
Testcontainers::Wait::for_http('/api/status',
    port        => '8080/tcp',
    status_code => 200,
    method      => 'GET',
);

# Wait for a log message (string or regex)
Testcontainers::Wait::for_log('ready to accept connections');
Testcontainers::Wait::for_log(qr/listening on port \d+/);

# Wait for Docker health check
Testcontainers::Wait::for_health_check();

# Combine multiple strategies
Testcontainers::Wait::for_all(
    Testcontainers::Wait::for_listening_port('5432/tcp'),
    Testcontainers::Wait::for_log('ready to accept connections'),
);

Container API

my $container = Testcontainers::run('myimage:latest', ...);

# Connection
$container->host;                    # Host address
$container->mapped_port('80/tcp');   # Mapped host port
$container->endpoint('80/tcp');      # "host:port"
$container->id;                      # Container ID
$container->name;                    # Container name

# Lifecycle
$container->stop;
$container->start;
$container->terminate;               # Stop + remove
$container->is_running;

# Interaction
$container->exec(['echo', 'hello']); # Execute command
$container->logs;                    # Get stdout/stderr
$container->logs(tail => 100);       # Last 100 lines

Advanced Usage

Custom Container Configuration

my $container = run('myimage:latest',
    exposed_ports   => ['8080/tcp', '9090/tcp'],
    env             => {
        DB_HOST     => 'localhost',
        DB_PORT     => '5432',
        LOG_LEVEL   => 'debug',
    },
    labels          => {
        'app'       => 'mytest',
        'version'   => '1.0',
    },
    cmd             => ['--config', '/etc/myapp.conf'],
    entrypoint      => ['/usr/local/bin/myapp'],
    tmpfs           => { '/tmp' => 'rw,size=100m' },
    privileged      => 1,
    network_mode    => 'bridge',
    startup_timeout => 120,
    wait_for        => Testcontainers::Wait::for_http('/health'),
);

Using in Test::More

use Test::More;
use Testcontainers qw( run terminate_container );
use Testcontainers::Wait;

my $container;

# Setup
$container = run('redis:7-alpine',
    exposed_ports => ['6379/tcp'],
    wait_for      => Testcontainers::Wait::for_listening_port('6379/tcp'),
);

# Tests
ok($container->is_running, 'redis is running');
my $port = $container->mapped_port('6379/tcp');
ok($port, "redis port: $port");

# Cleanup
terminate_container($container);

done_testing;

Architecture

Testcontainers                       # Main entry point, run() function
├── Container                        # Running container instance
├── ContainerRequest                 # Container configuration builder
├── DockerClient                     # WWW::Docker wrapper
├── Wait                             # Wait strategy factory
│   ├── Base                         # Base role for strategies
│   ├── HostPort                     # TCP port listening
│   ├── HTTP                         # HTTP endpoint check
│   ├── Log                          # Log message matching
│   ├── HealthCheck                  # Docker health check
│   └── Multi                        # Composite (all must pass)
└── Module                           # Pre-built container modules
    ├── PostgreSQL
    ├── MySQL
    ├── Redis
    └── Nginx

Running Tests

# Unit tests (no Docker required)
prove -l t/01-load.t t/02-container-request.t t/03-wait-strategies.t

# Integration tests (requires Docker)
TESTCONTAINERS_LIVE=1 prove -l t/04-integration.t t/05-modules.t

Environment Variables

| Variable | Description | Default | |---|---|---| | DOCKER_HOST | Docker daemon URL | unix:///var/run/docker.sock | | TESTCONTAINERS_LIVE | Enable integration tests | (unset) |

Comparison with Go Testcontainers

| Go | Perl | |---|---| | testcontainers.Run(ctx, image, opts...) | Testcontainers::run($image, %opts) | | container.Host(ctx) | $container->host | | container.MappedPort(ctx, "80/tcp") | $container->mapped_port('80/tcp') | | testcontainers.TerminateContainer(c) | $container->terminate | | wait.ForListeningPort("80/tcp") | Testcontainers::Wait::for_listening_port('80/tcp') | | wait.ForHTTP("/") | Testcontainers::Wait::for_http('/') | | wait.ForLog("ready") | Testcontainers::Wait::for_log('ready') | | testcontainers.WithEnv(map) | env => { ... } | | testcontainers.WithExposedPorts(...) | exposed_ports => [...] |

License

This software is copyright (c) 2026 by Testcontainers Contributors.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.

Acknowledgments

This project includes code derived from WWW::Docker by Torsten Raudssus, licensed under Perl license.