diff --git a/.gitignore b/.gitignore index b00febc..7061a13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,20 @@ /vendor/ /.php-cs-fixer.cache /.phpunit.cache -/composer.lock \ No newline at end of file +/composer.lock + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json \ No newline at end of file diff --git a/composer.json b/composer.json index de7580c..2bc1b7d 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,8 @@ "integration": "paratest tests/ --bootstrap vendor/autoload.php -f", "cs": "php-cs-fixer fix --dry-run", "cs:fix": "php-cs-fixer fix", - "phpstan": "phpstan analyse --memory-limit=256M" + "phpstan": "phpstan analyse --memory-limit=256M", + "docs:serve": "docker compose up" }, "config": { "allow-plugins": { diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8dc60ba --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + docs: + image: python:3.14 + command: sh -c "pip install -r requirements.txt && mkdocs serve -a 0.0.0.0:8000" + working_dir: /docs + volumes: + - ./:/docs + ports: + - 8000:8000 diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..aa1143e --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,38 @@ +# Configuration + +## Container runtime access + +Testcontainers for PHP uses Docker APIs under the hood, so your test process must be able to reach a Docker-compatible daemon. + +- Local socket: `unix:///var/run/docker.sock` +- Remote daemon: configure `DOCKER_HOST` + +## Environment variables + +### `DOCKER_HOST` + +Defines where the Docker API is available. +Examples: + +```bash +export DOCKER_HOST=tcp://127.0.0.1:2375 +export DOCKER_HOST=unix:///var/run/docker.sock +``` + +### `TESTCONTAINERS_HOST_OVERRIDE` + +Overrides the host address returned by Testcontainers when your tests need a custom endpoint: + +```bash +export TESTCONTAINERS_HOST_OVERRIDE=127.0.0.1 +``` + +## Running inside containers + +If your tests run inside another container: + +- Mount the Docker socket. +- Ensure network routing from test container to started containers is valid. +- Set host overrides when needed for your CI/network topology. + +For startup and connectivity failures, see [troubleshooting](troubleshooting.md). diff --git a/docs/features/containers.md b/docs/features/containers.md new file mode 100644 index 0000000..0caf77d --- /dev/null +++ b/docs/features/containers.md @@ -0,0 +1,86 @@ +# Containers + +## Starting a container + +Start any image with `GenericContainer`: + +```php +withCommand(['sleep', 'infinity']) + ->start(); +``` + +## Common container options + +### Environment variables + +```php +$container = (new GenericContainer('alpine:3.20')) + ->withEnvironment([ + 'APP_ENV' => 'test', + 'FEATURE_X' => 'enabled', + ]) + ->start(); +``` + +### Exposed ports + +```php +$container = (new GenericContainer('nginx:alpine')) + ->withExposedPorts(80) + ->start(); + +$host = $container->getHost(); +$port = $container->getMappedPort(80); +``` + +### Files and directories + +```php +$container = (new GenericContainer('alpine:3.20')) + ->withCommand(['sleep', 'infinity']) + ->withCopyFilesToContainer([ + ['source' => __DIR__ . '/app.conf', 'target' => '/etc/app.conf'], + ]) + ->withCopyContentToContainer([ + ['content' => 'hello from php', 'target' => '/tmp/message.txt'], + ]) + ->start(); +``` + +### Networking + +`withNetwork()` connects the container to an existing Docker network. Create the network before starting the container, for example with `docker network create my-test-network`. + +```php +$container = (new GenericContainer('alpine:3.20')) + ->withNetwork('my-test-network') + ->withAliases(['service-a']) + ->start(); +``` + +### User, working directory, labels, and mounts + +```php +$container = (new GenericContainer('alpine:3.20')) + ->withUser('1000:1000') + ->withWorkingDir('/app') + ->withLabels(['suite' => 'integration']) + ->withMount(__DIR__, '/workspace') + ->start(); +``` + +## Stopping and restarting + +```php +$container->restart(); +$container->stop(); +``` + +`stop()` stops and removes the container. + +Related docs: [wait strategies](wait-strategies.md), [networking](networking.md). diff --git a/docs/features/networking.md b/docs/features/networking.md new file mode 100644 index 0000000..54c9e01 --- /dev/null +++ b/docs/features/networking.md @@ -0,0 +1,66 @@ +# Networking + +Testcontainers for PHP maps container ports to random host ports by default. + +## Access a service from your test process + +Use `getHost()` and `getMappedPort()`: + +```php +withExposedPorts(80) + ->withWait((new WaitForHttp(80))->withPath('/')) + ->start(); + +$url = sprintf('http://%s:%d', $container->getHost(), $container->getMappedPort(80)); +echo $url . PHP_EOL; + +$container->stop(); +``` + +## Use the first mapped port + +If a container exposes only one port, use `getFirstMappedPort()`: + +```php +$port = $container->getFirstMappedPort(); +``` + +## Join a Docker network + +Connect multiple containers to the same existing Docker network and use aliases. Testcontainers for PHP does not create Docker networks, so create the network before starting containers: + +```bash +docker network create my-test-network +``` + +```php +withCommand(['tail', '-f', '/dev/null']) + ->withNetwork('my-test-network') + ->withAliases(['service-a']) + ->start(); + +$container->stop(); +``` + +## Notes + +- Do not hardcode localhost ports in tests. +- Always resolve endpoints from `getHost()` and mapped ports. +- Named networks and aliases are useful for container-to-container communication. + +Related docs: [containers](containers.md), [configuration](../configuration.md), [troubleshooting](../troubleshooting.md). diff --git a/docs/features/wait-strategies.md b/docs/features/wait-strategies.md new file mode 100644 index 0000000..8a810bb --- /dev/null +++ b/docs/features/wait-strategies.md @@ -0,0 +1,101 @@ +# Wait strategies + +Wait strategies define when a container is ready to use. + +`GenericContainer` defaults to a running-state check (`WaitForContainer`). +For application readiness, prefer explicit strategies described below. + +## Host port (default-friendly) + +```php +withExposedPorts(6379) + ->withWait(new WaitForHostPort()) + ->start(); +``` + +## Log output + +```php +use Testcontainers\Wait\WaitForLog; + +$container = (new GenericContainer('redis:7')) + ->withExposedPorts(6379) + ->withWait(new WaitForLog('Ready to accept connections')) + ->start(); +``` + +With regular expression matching: + +```php +$container = (new GenericContainer('opensearchproject/opensearch:latest')) + ->withExposedPorts(9200) + ->withWait(new WaitForLog('/\]\s+started\?\[/', true, 30_000)) + ->start(); +``` + +## HTTP checks + +```php +use Testcontainers\Wait\WaitForHttp; + +$container = (new GenericContainer('nginx:alpine')) + ->withExposedPorts(80) + ->withWait( + (new WaitForHttp(80)) + ->withPath('/') + ->withExpectedStatusCode(200) + ) + ->start(); +``` + +## Exec command + +```php +use Testcontainers\Wait\WaitForExec; + +$container = (new GenericContainer('mysql:8.0')) + ->withExposedPorts(3306) + ->withEnvironment(['MYSQL_ROOT_PASSWORD' => 'root']) + ->withWait(new WaitForExec(['mysqladmin', 'ping', '-h', '127.0.0.1'])) + ->start(); +``` + +With custom validation: + +```php +$container = (new GenericContainer('mysql:8.0')) + ->withExposedPorts(3306) + ->withEnvironment(['MYSQL_ROOT_PASSWORD' => 'root']) + ->withWait( + new WaitForExec( + ['mysqladmin', 'ping', '-h', '127.0.0.1'], + static function ($exitCode, $output): bool { + return $exitCode === 0 && str_contains($output, 'mysqld is alive'); + } + ) + ) + ->start(); +``` + +## Docker health check + +```php +use Testcontainers\Wait\WaitForHealthCheck; + +$container = (new GenericContainer('alpine')) + ->withCommand(['tail', '-f', '/dev/null']) + ->withHealthCheckCommand('echo "healthy" || exit 1') + ->withWait(new WaitForHealthCheck()) + ->start(); +``` + +!!! tip + You can tune timeout and polling intervals in wait strategy constructors. + +Related docs: [containers](containers.md), [networking](networking.md), [troubleshooting](../troubleshooting.md). diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..6fbc1ff --- /dev/null +++ b/docs/index.md @@ -0,0 +1,44 @@ +# Testcontainers for PHP + +

Not using PHP? Here are other supported languages.

+
+ JavaJava + GoGo + .NET.NET + Node.jsNode.js + ClojureClojure + ElixirElixir + HaskellHaskell + PythonPython + RubyRuby + RustRust + PHPPHP + ScalaScala +
+ +## About + +Testcontainers for PHP is a composer package that makes it simple to create and clean up container-based dependencies for automated integration/smoke tests. The clean, easy-to-use API enables developers to programmatically define containers that should be run as part of a test and clean up those resources when the test is done. + +## Documentation + +- [Install](quickstart/install.md) +- [Usage](quickstart/usage.md) +- [Containers](features/containers.md) +- [Wait strategies](features/wait-strategies.md) +- [Networking](features/networking.md) +- [Modules](modules/redis.md) +- [Configuration](configuration.md) +- [Troubleshooting](troubleshooting.md) + +## License + +This project is open source and you can have a look at the code on [GitHub](https://github.com/testcontainers/testcontainers-php). See [LICENSE](https://raw.githubusercontent.com/testcontainers/testcontainers-php/main/LICENSE). + +## Copyright + +Copyright (c) 2022-present Soner Sayakci, Sergei Shitikov and other authors. Check out our [lovely contributors](https://github.com/testcontainers/testcontainers-php/graphs/contributors). + +--- + +Join our [Slack workspace](https://slack.testcontainers.org/) | [Testcontainers OSS](https://java.testcontainers.org/) | [Testcontainers Cloud](https://www.testcontainers.cloud/) diff --git a/docs/modules/mariadb.md b/docs/modules/mariadb.md new file mode 100644 index 0000000..d3f8fd0 --- /dev/null +++ b/docs/modules/mariadb.md @@ -0,0 +1,33 @@ +# MariaDB + +`MariaDBContainer` configures MariaDB and waits for `mariadb-admin ping`. + +## Requirements + +- PHP extension: `ext-pdo_mysql` + +```php +withMariaDBDatabase('foo') + ->withMariaDBUser('bar', 'baz') + ->start(); + +try { + $pdo = new PDO( + sprintf('mysql:host=%s;port=%d', $container->getHost(), $container->getFirstMappedPort()), + 'bar', + 'baz', + ); + + $query = $pdo->query('SHOW databases'); + $databases = $query->fetchAll(PDO::FETCH_COLUMN); +} finally { + $container->stop(); +} +``` diff --git a/docs/modules/mongodb.md b/docs/modules/mongodb.md new file mode 100644 index 0000000..995eddd --- /dev/null +++ b/docs/modules/mongodb.md @@ -0,0 +1,34 @@ +# MongoDB + +`MongoDBContainer` configures credentials and waits until `mongosh` can execute a command. + +## Requirements + +- PHP extension: `ext-mongodb` + +```php +start(); + +try { + $pingResult = $container->exec([ + 'mongosh', + 'admin', + '-u', + 'test', + '-p', + 'test', + '--eval', + '\'db.runCommand("ping").ok\'', + ]); + + echo $pingResult; +} finally { + $container->stop(); +} +``` diff --git a/docs/modules/mysql.md b/docs/modules/mysql.md new file mode 100644 index 0000000..9273a7c --- /dev/null +++ b/docs/modules/mysql.md @@ -0,0 +1,33 @@ +# MySQL + +`MySQLContainer` configures MySQL and waits for `mysqladmin ping`. + +## Requirements + +- PHP extension: `ext-pdo_mysql` + +```php +withMySQLDatabase('foo') + ->withMySQLUser('bar', 'baz') + ->start(); + +try { + $pdo = new PDO( + sprintf('mysql:host=%s;port=%d', $container->getHost(), $container->getFirstMappedPort()), + 'bar', + 'baz', + ); + + $query = $pdo->query('SHOW databases'); + $databases = $query->fetchAll(PDO::FETCH_COLUMN); +} finally { + $container->stop(); +} +``` diff --git a/docs/modules/opensearch.md b/docs/modules/opensearch.md new file mode 100644 index 0000000..f735971 --- /dev/null +++ b/docs/modules/opensearch.md @@ -0,0 +1,32 @@ +# OpenSearch + +`OpenSearchContainer` configures single-node mode and waits for startup logs. + +```php +withDisabledSecurityPlugin() + ->start(); + +try { + $ch = curl_init(); + curl_setopt( + $ch, + CURLOPT_URL, + sprintf('http://%s:%d', $container->getHost(), $container->getFirstMappedPort()) + ); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = (string) curl_exec($ch); + $data = json_decode($response, true, 512, JSON_THROW_ON_ERROR); + + echo (string) $data['cluster_name']; +} finally { + $container->stop(); +} +``` diff --git a/docs/modules/postgresql.md b/docs/modules/postgresql.md new file mode 100644 index 0000000..071de1f --- /dev/null +++ b/docs/modules/postgresql.md @@ -0,0 +1,33 @@ +# PostgreSQL + +`PostgresContainer` sets `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB`, then waits with `pg_isready`. + +## Requirements + +- PHP extension: `ext-pdo_pgsql` + +```php +withPostgresUser('bar') + ->withPostgresDatabase('foo') + ->start(); + +try { + $pdo = new PDO( + sprintf('pgsql:host=%s;port=%d;dbname=foo', $container->getHost(), $container->getFirstMappedPort()), + 'bar', + 'test', + ); + + $query = $pdo->query('SELECT datname FROM pg_database'); + $databases = $query->fetchAll(PDO::FETCH_COLUMN); +} finally { + $container->stop(); +} +``` diff --git a/docs/modules/redis.md b/docs/modules/redis.md new file mode 100644 index 0000000..aea3bf2 --- /dev/null +++ b/docs/modules/redis.md @@ -0,0 +1,31 @@ +# Redis + +`RedisContainer` exposes port `6379` and waits for the log line `Ready to accept connections`. + +## Requirements + +- PHP package: `predis/predis` + +```php +start(); + +try { + $redisClient = new Client([ + 'host' => $container->getHost(), + 'port' => $container->getFirstMappedPort(), + ]); + + $redisClient->ping(); + $redisClient->set('framework', 'testcontainers-php'); + echo (string) $redisClient->get('framework'); +} finally { + $container->stop(); +} +``` diff --git a/docs/quickstart/install.md b/docs/quickstart/install.md new file mode 100644 index 0000000..b22591f --- /dev/null +++ b/docs/quickstart/install.md @@ -0,0 +1,9 @@ +# Install + +Install the package as a development dependency: + +```bash +composer require --dev testcontainers/testcontainers +``` + +Next, see the [usage guide](usage.md) for a runnable test example. diff --git a/docs/quickstart/usage.md b/docs/quickstart/usage.md new file mode 100644 index 0000000..c3375e0 --- /dev/null +++ b/docs/quickstart/usage.md @@ -0,0 +1,96 @@ +# Usage + +**As an example, let's spin up and test a Redis container.** + + +First, install dependencies: + +```bash +composer require --dev testcontainers/testcontainers +composer require --dev predis/predis +``` + +Next, we'll write a PHPUnit test using `GenericContainer` directly: + +```php +withExposedPorts(6379) + ->withWait(new WaitForExec(['redis-cli', 'ping'])) + ->start(); + + try { + $redis = new Client([ + 'host' => $container->getHost(), + 'port' => $container->getMappedPort(6379), + ]); + + $redis->set('hello', 'world'); + + self::assertSame('world', (string) $redis->get('hello')); + } finally { + $container->stop(); + } + } +} +``` + +Run the test, and after a few seconds, it passes! + +!!! note + Why did it take a few seconds? + + Because your container runtime first had to pull the image. If you run the test again, it'll run faster. + +The complexity of configuring a container varies. + +For Redis, it's pretty simple, we just expose a port and wait until Redis responds. To define a `GenericContainer` for PostgreSQL, you'd also configure credentials and a readiness command such as `pg_isready`. For this reason there is a catalogue of PHP [pre-defined modules](../modules/postgresql.md), which abstract away this complexity. + +If a module exists for the container you want to use, it's highly recommended to use it. + +For example, using the ready-made Redis module, the example above can be simplified: + +```php +start(); + + try { + $redis = new Client([ + 'host' => $container->getHost(), + 'port' => $container->getFirstMappedPort(), + ]); + + $redis->set('hello', 'world'); + + self::assertSame('world', (string) $redis->get('hello')); + } finally { + $container->stop(); + } + } +} +``` + +See the [containers guide](../features/containers.md) for the generic builder API and the [Redis module](../modules/redis.md) for the pre-configured Redis container. diff --git a/docs/site/community-logos/github.svg b/docs/site/community-logos/github.svg new file mode 100644 index 0000000..d563c83 --- /dev/null +++ b/docs/site/community-logos/github.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/docs/site/community-logos/slack.svg b/docs/site/community-logos/slack.svg new file mode 100644 index 0000000..942265e --- /dev/null +++ b/docs/site/community-logos/slack.svg @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/docs/site/community-logos/stackoverflow.svg b/docs/site/community-logos/stackoverflow.svg new file mode 100644 index 0000000..c330762 --- /dev/null +++ b/docs/site/community-logos/stackoverflow.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/docs/site/community-logos/twitter.svg b/docs/site/community-logos/twitter.svg new file mode 100644 index 0000000..ef3fbc6 --- /dev/null +++ b/docs/site/community-logos/twitter.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/docs/site/css/extra.css b/docs/site/css/extra.css new file mode 100644 index 0000000..ff780cb --- /dev/null +++ b/docs/site/css/extra.css @@ -0,0 +1,128 @@ +h1, h2, h3, h4, h5, h6 { + font-family: 'Rubik', sans-serif; +} + +[data-md-color-scheme="testcontainers"] { + --md-primary-fg-color: #00bac2; + --md-accent-fg-color: #361E5B; + --md-typeset-a-color: #0C94AA; + --md-primary-fg-color--dark: #291A3F; + --md-default-fg-color--lightest: #F2F4FE; + --md-footer-fg-color: #361E5B; + --md-footer-fg-color--light: #746C8F; + --md-footer-fg-color--lighter: #C3BEDE; + --md-footer-bg-color: #F7F9FD; + --md-footer-bg-color--dark: #F7F9FD; +} + +.card-grid { + display: grid; + gap: 10px; +} + +.tc-version { + font-size: 1.1em; + text-align: center; + margin: 0; +} + +@media (min-width: 680px) { + .card-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +body .card-grid-item { + display: flex; + align-items: center; + gap: 20px; + border: 1px solid #C3BEDE; + border-radius: 6px; + padding: 16px; + font-weight: 600; + color: #9991B5; + background: #F2F4FE; +} + +body .card-grid-item:hover, +body .card-grid-item:focus { + color: #9991B5; +} + +.card-grid-item[href] { + color: var(--md-primary-fg-color--dark); + background: transparent; +} + +.card-grid-item[href]:hover, +.card-grid-item[href]:focus { + background: #F2F4FE; + color: var(--md-primary-fg-color--dark); +} + +.community-callout-wrapper { + padding: 30px 10px 0 10px; +} + +.community-callout { + color: #F2F4FE; + background: linear-gradient(10.88deg, rgba(102, 56, 242, 0.4) 9.56%, #6638F2 100%), #291A3F; + box-shadow: 0px 20px 45px rgba(#9991B5, 0.75); + border-radius: 10px; + padding: 20px; +} + +.community-callout h2 { + font-size: 1.15em; + margin: 0 0 20px 0; + color: #F2F4FE; + text-align: center; +} + +.community-callout ul { + list-style: none; + padding: 0; + display: flex; + justify-content: space-between; + gap: 10px; + margin-top: 20px; + margin-bottom: 0; +} + +.community-callout a { + transition: opacity 0.2s ease; +} + +.community-callout a:hover { + opacity: 0.5; +} + +.community-callout a img { + height: 1.75em; + width: auto; + aspect-ratio: 1; +} + +@media (min-width: 1220px) { + .community-callout-wrapper { + padding: 40px 0 0; + } + + .community-callout h2 { + font-size: 1.25em; + } + + .community-callout a img { + height: 2em; + } +} + +@media (min-width: 1600px) { + .community-callout h2 { + font-size: 1.15em; + } + + .community-callout a img { + height: 1.75em; + } +} \ No newline at end of file diff --git a/docs/site/css/tc-header.css b/docs/site/css/tc-header.css new file mode 100644 index 0000000..c055009 --- /dev/null +++ b/docs/site/css/tc-header.css @@ -0,0 +1,389 @@ + +:root { + --color-catskill: #F2F4FE; + --color-catskill-45: rgba(242, 244, 254, 0.45); + --color-mist: #E7EAFB; + --color-fog: #C3C7E6; + --color-smoke: #9991B5; + --color-smoke-75: rgba(153, 145, 181, 0.75); + --color-storm: #746C8F; + --color-topaz: #00BAC2; + --color-pacific: #17A6B2; + --color-teal: #027F9E; + --color-eggplant: #291A3F; + --color-plum: #361E5B; + +} + +#site-header { + color: var(--color-storm); + background: #fff; + font-family: 'Rubik', Arial, Helvetica, sans-serif; + font-size: 12px; + line-height: 1.5; + position: relative; + width: 100%; + z-index: 4; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + padding: 20px; +} + +body.tc-header-active #site-header { + z-index: 5; +} + +#site-header .brand { + display: flex; + justify-content: space-between; + gap: 20px; + width: 100%; +} + +#site-header .logo { + display: flex; +} + +#site-header .logo img, +#site-header .logo svg { + height: 30px; + width: auto; + max-width: 100%; +} + +#site-header #mobile-menu-toggle { + background: none; + border: none; + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + color: var(--color-eggplant); + padding: 0; + margin: 0; + font-weight: 500; +} + +body.mobile-menu #site-header #mobile-menu-toggle { + color: var(--color-topaz); +} + +#site-header ul { + list-style: none; + padding: 0; + margin: 0; +} + +#site-header nav { + display: none; +} + +#site-header .menu-item { + display: flex; +} + +#site-header .menu-item button, +#site-header .menu-item a { + min-height: 30px; + display: flex; + gap: 6px; + align-items: center; + border: none; + background: none; + cursor: pointer; + padding: 0; + font-weight: 500; + color: var(--color-eggplant); + text-decoration: none; + font-size: 14px; + transition: color 0.2s ease; + white-space: nowrap; +} + +#site-header .menu-item .badge { + color: white; + font-size: 10px; + padding: 2px 6px; + background-color: #0FD5C6; +text-align: center; + text-decoration: none; + display: inline-block; + border-radius: 6px; + &:hover { + + } +} + +#site-header .menu-item button:hover, +#site-header .menu-item a:hover { + color: var(--color-topaz); +} + +#site-header .menu-item button .icon-external, +#site-header .menu-item a .icon-externa { + margin-left: auto; + opacity: .3; + flex-shrink: 0; +} + +#site-header .menu-item button .icon-caret, +#site-header .menu-item a .icon-caret { + opacity: .3; + height: 8px; +} + +#site-header .menu-item button .icon-slack, +#site-header .menu-item a .icon-slack, +#site-header .menu-item button .icon-github, +#site-header .menu-item a .icon-github { + height: 18px; +} + +#site-header .menu-item .menu-dropdown { + flex-direction: column; +} + +body #site-header .menu-item .menu-dropdown { + display: none; +} + +#site-header .menu-item.has-children.active .menu-dropdown { + display: flex; + z-index: 10; +} + +#site-header .menu-dropdown-item + .menu-dropdown-item { + border-top: 1px solid var(--color-mist); +} + +#site-header .menu-dropdown-item a { + display: flex; + gap: 10px; + align-items: center; + padding: 10px 20px; + font-weight: 500; + color: var(--color-eggplant); + text-decoration: none; + transition: + color 0.2s ease, + background 0.2s ease; +} + +#site-header .menu-dropdown-item a .icon-external { + margin-left: auto; + color: var(--color-fog); + flex-shrink: 0; + opacity: 1; +} + +#site-header .menu-dropdown-item a:hover { + background-color: var(--color-catskill-45); +} + +#site-header .menu-dropdown-item a:hover .icon-external { + color: var(--color-topaz); +} + +#site-header .menu-dropdown-item a img { + height: 24px; +} + +.md-header { + background-color: var(--color-catskill); + color: var(--color-eggplant); +} + +.md-header.md-header--shadow { + box-shadow: none; +} + +.md-header__inner.md-grid { + max-width: 100%; + padding: 1.5px 20px; +} + +[dir=ltr] .md-header__title { + margin: 0; +} + +.md-header__topic:first-child { + font-size: 16px; + font-weight: 500; + font-family: 'Rubik', Arial, Helvetica, sans-serif; +} + +.md-header__title.md-header__title--active .md-header__topic, +.md-header__title[data-md-state=active] .md-header__topic { + opacity: 1; + pointer-events: all; + transform: translateX(0); + transition: none; + z-index: 0; +} + +.md-header__topic a { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + transition: color .2s ease; +} + +.md-header__topic a:hover { + color: var(--color-topaz); +} + +div.md-header__source { + width: auto; +} + +div.md-source__repository { + max-width: 100%; +} + +.md-main { + padding: 0 12px; +} + +@media screen and (min-width: 60em) { + form.md-search__form { + background-color: #FBFBFF; + color: var(--color-storm); + } + + form.md-search__form:hover { + background-color: #fff; + } + + .md-search__input + .md-search__icon { + color: var(--color-plum); + } + + .md-search__input::placeholder { + color: var(--color-smoke); + } +} + +@media (min-width: 500px) { + #site-header { + font-size: 16px; + padding: 20px 40px; + } + #site-header .logo img, + #site-header .logo svg { + height: 48px; + } + + #site-header .menu-item button .icon-caret, + #site-header .menu-item a .icon-caret { + height: 10px; + } + + #site-header .menu-item button .icon-slack, + #site-header .menu-item a .icon-slack, + #site-header .menu-item button .icon-github, + #site-header .menu-item a .icon-github { + height: 24px; + } + + .md-header__inner.md-grid { + padding: 5px 40px; + } + + .md-main { + padding: 0 32px; + } +} + +@media (min-width: 1024px) { + #site-header #mobile-menu-toggle { + display: none; + } + + #site-header nav { + display: block; + } + + #site-header .menu { + display: flex; + justify-content: center; + gap: 30px; + } + + #site-header .menu-item { + align-items: center; + position: relative; + } + + #site-header .menu-item button, + #site-header .menu-item a { + min-height: 48px; + gap: 8px; + font-size: 16px; + } + + #site-header .menu-item .menu-dropdown { + position: absolute; + top: 100%; + right: -8px; + border: 1px solid var(--color-mist); + border-radius: 6px; + background: #fff; + box-shadow: 0px 30px 35px var(--color-smoke-75); + min-width: 200px; + } +} + + +@media (max-width: 1023px) { + #site-header { + flex-direction: column; + } + + body.mobile-tc-header-active #site-header { + z-index: 5; + } + + body.mobile-menu #site-header nav { + display: flex; + } + + #site-header nav { + position: absolute; + top: calc(100% - 5px); + width: calc(100% - 80px); + flex-direction: column; + border: 1px solid var(--color-mist); + border-radius: 6px; + background: #fff; + box-shadow: 0px 30px 35px var(--color-smoke-75); + min-width: 200px; + } + + #site-header .menu-item { + flex-direction: column; + } + #site-header .menu-item + .menu-item { + border-top: 1px solid var(--color-mist); + } + + #site-header .menu-item button, + #site-header .menu-item a { + padding: 10px 20px; + } + + #site-header .menu-item.has-children.active .menu-dropdown { + border-top: 1px solid var(--color-mist); + } + + #site-header .menu-dropdown-item a { + padding: 10px 20px 10px 30px; + } +} + +@media (max-width: 499px) { + #site-header nav { + width: calc(100% - 40px); + } +} \ No newline at end of file diff --git a/docs/site/favicon.ico b/docs/site/favicon.ico new file mode 100644 index 0000000..311a0ac Binary files /dev/null and b/docs/site/favicon.ico differ diff --git a/docs/site/js/tc-header.js b/docs/site/js/tc-header.js new file mode 100644 index 0000000..4186b6c --- /dev/null +++ b/docs/site/js/tc-header.js @@ -0,0 +1,45 @@ +const mobileToggle = document.getElementById("mobile-menu-toggle"); +const mobileSubToggle = document.getElementById("mobile-submenu-toggle"); +function toggleMobileMenu() { + document.body.classList.toggle('mobile-menu'); + document.body.classList.toggle("mobile-tc-header-active"); +} +function toggleMobileSubmenu() { + document.body.classList.toggle('mobile-submenu'); +} +if (mobileToggle) + mobileToggle.addEventListener("click", toggleMobileMenu); +if (mobileSubToggle) + mobileSubToggle.addEventListener("click", toggleMobileSubmenu); + +const allParentMenuItems = document.querySelectorAll("#site-header .menu-item.has-children"); +function clearActiveMenuItem() { + document.body.classList.remove("tc-header-active"); + allParentMenuItems.forEach((item) => { + item.classList.remove("active"); + }); +} +function setActiveMenuItem(e) { + clearActiveMenuItem(); + e.currentTarget.closest(".menu-item").classList.add("active"); + document.body.classList.add("tc-header-active"); +} +allParentMenuItems.forEach((item) => { + const trigger = item.querySelector(":scope > a, :scope > button"); + + trigger.addEventListener("click", (e) => { + if (e.currentTarget.closest(".menu-item").classList.contains("active")) { + clearActiveMenuItem(); + } else { + setActiveMenuItem(e); + } + }); + + trigger.addEventListener("mouseenter", (e) => { + setActiveMenuItem(e); + }); + + item.addEventListener("mouseleave", (e) => { + clearActiveMenuItem(); + }); +}); \ No newline at end of file diff --git a/docs/site/language-logos/clojure.svg b/docs/site/language-logos/clojure.svg new file mode 100644 index 0000000..506db3b --- /dev/null +++ b/docs/site/language-logos/clojure.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/docs/site/language-logos/dotnet.svg b/docs/site/language-logos/dotnet.svg new file mode 100644 index 0000000..2fb163d --- /dev/null +++ b/docs/site/language-logos/dotnet.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/docs/site/language-logos/elixir.svg b/docs/site/language-logos/elixir.svg new file mode 100644 index 0000000..532746a --- /dev/null +++ b/docs/site/language-logos/elixir.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/site/language-logos/go.svg b/docs/site/language-logos/go.svg new file mode 100644 index 0000000..bfcca48 --- /dev/null +++ b/docs/site/language-logos/go.svg @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/docs/site/language-logos/haskell.svg b/docs/site/language-logos/haskell.svg new file mode 100644 index 0000000..eb6de37 --- /dev/null +++ b/docs/site/language-logos/haskell.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/site/language-logos/java.svg b/docs/site/language-logos/java.svg new file mode 100644 index 0000000..590da12 --- /dev/null +++ b/docs/site/language-logos/java.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/docs/site/language-logos/nodejs.svg b/docs/site/language-logos/nodejs.svg new file mode 100644 index 0000000..08c6ea7 --- /dev/null +++ b/docs/site/language-logos/nodejs.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/docs/site/language-logos/php.svg b/docs/site/language-logos/php.svg new file mode 100644 index 0000000..939f1ec --- /dev/null +++ b/docs/site/language-logos/php.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/site/language-logos/python.svg b/docs/site/language-logos/python.svg new file mode 100644 index 0000000..d06a313 --- /dev/null +++ b/docs/site/language-logos/python.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/docs/site/language-logos/ruby.svg b/docs/site/language-logos/ruby.svg new file mode 100644 index 0000000..05537ce --- /dev/null +++ b/docs/site/language-logos/ruby.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/site/language-logos/rust.svg b/docs/site/language-logos/rust.svg new file mode 100644 index 0000000..8903933 --- /dev/null +++ b/docs/site/language-logos/rust.svg @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/docs/site/language-logos/scala.svg b/docs/site/language-logos/scala.svg new file mode 100644 index 0000000..23decc0 --- /dev/null +++ b/docs/site/language-logos/scala.svg @@ -0,0 +1,26 @@ + + + + + Scala + The Scala Logo + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/site/logo.png b/docs/site/logo.png new file mode 100644 index 0000000..88961b3 Binary files /dev/null and b/docs/site/logo.png differ diff --git a/docs/site/logo.svg b/docs/site/logo.svg new file mode 100644 index 0000000..bac0c39 --- /dev/null +++ b/docs/site/logo.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/site/testcontainers-logo.svg b/docs/site/testcontainers-logo.svg new file mode 100644 index 0000000..4b099f3 --- /dev/null +++ b/docs/site/testcontainers-logo.svg @@ -0,0 +1,22 @@ + + + Testcontainers + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/site/theme/main.html b/docs/site/theme/main.html new file mode 100644 index 0000000..f5ced87 --- /dev/null +++ b/docs/site/theme/main.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block analytics %} + +{% endblock %} + +{% block extrahead %} + + +{% endblock %} diff --git a/docs/site/theme/partials/header.html b/docs/site/theme/partials/header.html new file mode 100644 index 0000000..56105c0 --- /dev/null +++ b/docs/site/theme/partials/header.html @@ -0,0 +1,151 @@ + + + +{% set class = "md-header" %} +{% if "navigation.tabs.sticky" in features %} + {% set class = class ~ " md-header--shadow md-header--lifted" %} +{% elif "navigation.tabs" not in features %} + {% set class = class ~ " md-header--shadow" %} +{% endif %} + +{% include "partials/tc-header.html" %} + + +
+ + + + {% if "navigation.tabs.sticky" in features %} + {% if "navigation.tabs" in features %} + {% include "partials/tabs.html" %} + {% endif %} + {% endif %} +
diff --git a/docs/site/theme/partials/nav.html b/docs/site/theme/partials/nav.html new file mode 100644 index 0000000..10a1f23 --- /dev/null +++ b/docs/site/theme/partials/nav.html @@ -0,0 +1,79 @@ + + + +{% import "partials/nav-item.html" as item with context %} +{% set class = "md-nav md-nav--primary" %} +{% if "navigation.tabs" in features %} +{% set class = class ~ " md-nav--lifted" %} +{% endif %} +{% if "toc.integrate" in features %} +{% set class = class ~ " md-nav--integrated" %} +{% endif %} + + + diff --git a/docs/site/theme/partials/tc-header.html b/docs/site/theme/partials/tc-header.html new file mode 100644 index 0000000..53c6458 --- /dev/null +++ b/docs/site/theme/partials/tc-header.html @@ -0,0 +1,157 @@ +{% set header = ({ + "siteUrl": "https://testcontainers.com/", + "menuItems": [ + { + "label": "Desktop NEW", + "url": "https://testcontainers.com/desktop/" + }, + { + "label": "Cloud", + "url": "https://testcontainers.com/cloud/" + }, + { + "label": "Getting Started", + "url": "https://testcontainers.com/getting-started/" + }, + { + "label": "Guides", + "url": "https://testcontainers.com/guides/" + }, + { + "label": "Modules", + "url": "https://testcontainers.com/modules/" + }, + { + "label": "Docs", + "children": [ + { + "label": "Testcontainers for Java", + "url": "https://java.testcontainers.org/", + "image": "/site/language-logos/java.svg", + }, + { + "label": "Testcontainers for Go", + "url": "https://golang.testcontainers.org/", + "image": "/site/language-logos/go.svg", + }, + { + "label": "Testcontainers for .NET", + "url": "https://dotnet.testcontainers.org/", + "image": "/site/language-logos/dotnet.svg", + }, + { + "label": "Testcontainers for Node.js", + "url": "https://node.testcontainers.org/", + "image": "/site/language-logos/nodejs.svg", + }, + { + "label": "Testcontainers for Python", + "url": "https://testcontainers-python.readthedocs.io/en/latest/", + "image": "/site/language-logos/python.svg", + "external": true, + }, + { + "label": "Testcontainers for Rust", + "url": "https://docs.rs/testcontainers/latest/testcontainers/", + "image": "/site/language-logos/rust.svg", + "external": true, + }, + { + "label": "Testcontainers for Haskell", + "url": "https://github.com/testcontainers/testcontainers-hs", + "image": "/site/language-logos/haskell.svg", + "external": true, + }, + { + "label": "Testcontainers for Ruby", + "url": "https://github.com/testcontainers/testcontainers-ruby", + "image": "/site/language-logos/ruby.svg", + "external": true, + }, + ] + }, + { + "label": "Slack", + "url": "https://slack.testcontainers.org/", + "icon": "icon-slack", + }, + { + "label": "GitHub", + "url": "https://github.com/testcontainers", + "icon": "icon-github", + }, + ] +}) %} + + + + + + + + + + + diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..b2cf85c --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,58 @@ +# Troubleshooting + +For runtime endpoint settings, see [configuration](configuration.md). + +## Docker daemon is not reachable + +Symptoms: + +- container start fails immediately +- Docker API errors from runtime client + +Checks: + +- Ensure Docker is running. +- Verify socket/endpoint access: + +```bash +docker version +docker info +``` + +- Set `DOCKER_HOST` when not using the default local socket: + +```bash +export DOCKER_HOST=unix:///var/run/docker.sock +``` + +## Container starts but app is not ready + +`GenericContainer` uses a running-state wait strategy by default. +For real readiness, add an explicit wait strategy (`WaitForLog`, `WaitForExec`, `WaitForHttp`, `WaitForHostPort`, `WaitForHealthCheck`). + +See [wait strategies](features/wait-strategies.md) for examples. + +## Wrong host/port in CI or Docker-in-Docker + +If mapped ports are reachable but host resolution is wrong, set: + +```bash +export TESTCONTAINERS_HOST_OVERRIDE=127.0.0.1 +``` + +Use a value that is reachable from your test process. + +## Private registry pull failures + +If image pull fails for private registries, provide Docker auth config through: + +```bash +export DOCKER_AUTH_CONFIG='{"auths":{"registry.example.com":{"auth":""}}}' +``` + +or configure credentials in Docker config files (`~/.docker/config.json`). + +## Podman-specific networking issues + +When using Podman with Docker-compatible APIs, host/network behavior can differ from Docker. +If startup succeeds but connections fail, check `DOCKER_HOST`, network mode, and host override settings. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..2b66fcc --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,57 @@ +site_name: Testcontainers for PHP +site_url: https://php.testcontainers.org +repo_name: testcontainers-php +repo_url: https://github.com/testcontainers/testcontainers-php +edit_uri: edit/main/docs/ + +theme: + name: "material" + custom_dir: "docs/site/theme" + palette: + scheme: testcontainers + font: + text: Roboto + code: Roboto Mono + logo: "site/logo.svg" + favicon: "site/favicon.ico" + features: + - content.code.copy + +extra_css: + - "site/css/extra.css" + - "site/css/tc-header.css" + +plugins: + - search + - codeinclude + - markdownextradata + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.highlight: + linenums: true + - pymdownx.tabbed: + alternate_style: true + - toc: + permalink: true + +nav: + - Home: index.md + - Quickstart: + - Install: quickstart/install.md + - Usage: quickstart/usage.md + - Features: + - Containers: features/containers.md + - Wait strategies: features/wait-strategies.md + - Networking: features/networking.md + - Modules: + - MariaDB: modules/mariadb.md + - MongoDB: modules/mongodb.md + - MySQL: modules/mysql.md + - OpenSearch: modules/opensearch.md + - PostgreSQL: modules/postgresql.md + - Redis: modules/redis.md + - Configuration: configuration.md + - Troubleshooting: troubleshooting.md diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ca24545 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +mkdocs==1.6.1 +mkdocs-codeinclude-plugin==0.3.1 +mkdocs-markdownextradata-plugin==0.2.6 +mkdocs-material==9.7.5