Welcome back, intrepid developer! You’ve mastered spinning up powerful, ephemeral environments with Testcontainers. But what happens when things don’t go as planned? When your containerized application doesn’t start, or your test fails in unexpected ways? That’s where debugging comes in!
In this chapter, we’re going to transform you into a debugging detective for your Testcontainers-powered tests. We’ll explore why debugging containers can be a unique challenge and equip you with the essential tools and techniques to peer inside your test environment, understand what’s happening, and fix problems. From poring over container logs to directly interacting with running containers and even performing remote debugging of your application within a Testcontainer, you’ll gain the confidence to troubleshoot any issue.
Before we dive in, make sure you’re comfortable with the basics of using Testcontainers in your chosen language (Java, JavaScript/TypeScript, or Python) from our previous chapters. Let’s get started on becoming master debuggers!
The Art of Debugging Containerized Tests
Debugging traditional applications can be tricky enough, but when you introduce containers into the mix, there’s an extra layer of abstraction and isolation. Your application code is running inside a separate, ephemeral environment, managed by Docker, and orchestrated by Testcontainers. This isolation, while fantastic for reproducible tests, can initially make debugging feel like trying to solve a puzzle blindfolded.
The key is to remember that at their heart, Testcontainers are just Docker containers. This means all the powerful Docker debugging tools are still at your disposal. Testcontainers just gives you a programmatic way to control them.
Why Debugging Containers is Different (and Fun!)
- Isolation: Containers are designed to be isolated from your host system. This means your application logs and processes aren’t directly visible on your host unless you explicitly expose them.
- Ephemerality: By default, Testcontainers are throwaway. They’re created for a test, and once the test (or test suite) finishes, they’re cleaned up. This is great for resource management but means you can’t easily inspect a container after a test failure without a little trickery.
- Black Box Tendencies: If your application fails to start inside a container, or a dependency isn’t behaving, it can feel like a black box. You need ways to “look inside.”
Let’s map out a general strategy for debugging:
Essential Debugging Tools and Concepts
- Container Logs: The most fundamental tool. Your application, databases, and services inside containers will output logs. Testcontainers provides ways to capture these, and Docker itself keeps them.
- Docker CLI Tools (
docker ps,docker logs,docker exec,docker inspect): These are your best friends. They let you see what containers are running, view their logs directly, execute commands inside a running container, and get detailed configuration information. - Testcontainers’
RyukProcess: Testcontainers uses a small, internal helper container calledRyukto ensure all containers started by Testcontainers are cleaned up after your tests finish. This is normally excellent, but for debugging failed tests, it can remove the evidence too quickly! We’ll learn how to temporarily disable it. - Remote Debugging: For complex issues, you might need to connect your IDE’s debugger directly to your application process running inside the Testcontainer. This allows you to set breakpoints, step through code, and inspect variables.
Let’s look at how to apply these in practice across our languages.
Step-by-Step Debugging Techniques
We’ll cover three primary techniques: capturing logs, keeping containers alive for inspection, and remote debugging.
1. Capturing and Analyzing Container Logs
Your application’s logs are often the first place to look for clues. Testcontainers makes it easy to grab these logs programmatically.
Java Example
For Java with Testcontainers, you can easily access logs via the getLogs() method or use a Slf4jLogConsumer.
First, ensure you have a basic Testcontainers setup. Let’s use a PostgreSQL container.
pom.xml (relevant dependencies, as of 2026-02-14)
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.4</version> <!-- Current stable as of Feb 2026, check Testcontainers.org for latest -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.4</version> <!-- Matches core Testcontainers version -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.1</version> <!-- JUnit 5 latest stable -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>
PostgreSqlLogTest.java
package com.example.testcontainers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.slf4j.LoggerFactory; // For Slf4jLogConsumer if you prefer
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@Testcontainers
class PostgreSqlLogTest {
// Using an explicit DockerImageName with version for clarity and stability
@Container
private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(DockerImageName.parse("postgres:16.2"))
.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpass");
@BeforeEach
void setup() {
System.out.println("PostgreSQL container started at: " + postgres.getJdbcUrl());
}
@Test
void testDatabaseConnectionAndBadQuery() {
try (Connection connection = DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword());
Statement statement = connection.createStatement()) {
// This query should pass
statement.execute("CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100));");
statement.execute("INSERT INTO users (name) VALUES ('Alice');");
// This query is intentionally malformed to cause an error
// Note: Postgres will log an error for this.
statement.execute("SELECT * FROM non_existent_table;"); // This will throw an SQLException
ResultSet resultSet = statement.executeQuery("SELECT name FROM users WHERE name = 'Alice';");
assertTrue(resultSet.next(), "Should find Alice");
} catch (Exception e) {
System.err.println("Test failed with exception: " + e.getMessage());
// It's crucial here to get the logs of the container if the test fails
System.out.println("--- PostgreSQL Container Logs ---");
System.out.println(postgres.getLogs()); // Print all container logs
System.out.println("-------------------------------");
fail("Database operation failed: " + e.getMessage());
}
}
}
Explanation:
- We intentionally added a bad SQL query (
SELECT * FROM non_existent_table;) to simulate a failure within the database operation. - In the
catchblock, after anExceptionis caught, we usepostgres.getLogs()to retrieve all the logs emitted by the PostgreSQL container up to that point. This output will include the database’s own error messages, which are invaluable for debugging. - Pro-tip: For real-time logging, especially during startup, Testcontainers supports
Slf4jLogConsumer. You can add.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(PostgreSqlLogTest.class)))to your container definition to pipe container logs directly to your test runner’s console.
JavaScript/TypeScript Example
For Node.js with testcontainers-node, the logs() method provides a stream of output.
First, ensure testcontainers is installed:
npm install --save-dev testcontainers @types/testcontainers # For TypeScript
# or
yarn add --dev testcontainers
postgresql-log.test.ts
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { GenericContainer, StartedTestContainer } from 'testcontainers';
import { Client } from 'pg'; // PostgreSQL client for Node.js
describe('PostgreSQL Log Test', () => {
let postgresContainer: StartedTestContainer;
let client: Client;
// IMPORTANT: For debugging, it's often useful to keep containers alive after test failure.
// We'll discuss this in the next section. For now, rely on logs.
beforeAll(async () => {
postgresContainer = await new PostgreSqlContainer('postgres:16.2')
.withDatabase('testdb')
.withUsername('testuser')
.withPassword('testpass')
.start();
// You can also capture logs in real-time if needed, but for simple failures,
// getting them at the end is often sufficient.
postgresContainer.on(GenericContainer.ON_CONTAINER_START, async () => {
console.log(`PostgreSQL container started: ${postgresContainer.getHost()}:${postgresContainer.getMappedPort(5432)}`);
});
client = new Client({
host: postgresContainer.getHost(),
port: postgresContainer.getMappedPort(5432),
user: 'testuser',
password: 'testpass',
database: 'testdb',
});
await client.connect();
}, 30000); // 30 second timeout for container startup
afterAll(async () => {
await client.end();
await postgresContainer.stop();
});
it('should connect to PostgreSQL and log errors for bad query', async () => {
try {
await client.query("CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100));");
await client.query("INSERT INTO users (name) VALUES ('Alice');");
// Intentionally malformed query
await client.query("SELECT * FROM non_existent_table;");
const res = await client.query("SELECT name FROM users WHERE name = 'Alice';");
expect(res.rows.length).toBe(1);
expect(res.rows[0].name).toBe('Alice');
} catch (error: any) {
console.error("Test failed with exception:", error.message);
const logs = await postgresContainer.logs();
console.log('--- PostgreSQL Container Logs ---');
logs.on('data', line => console.log(`[POSTGRES_LOG] ${line}`));
// We need to wait for the logs stream to finish or collect it
// For short tests, sometimes collecting them after the fact is more reliable
// Or simply let the stream print until the test process exits
await new Promise(resolve => setTimeout(resolve, 1000)); // Give some time for logs to print
console.log('-------------------------------');
fail(`Database operation failed: ${error.message}`);
}
}, 10000); // 10 second timeout for test execution
});
Explanation:
- Similar to Java, we introduce a bad query.
- In the
catchblock,postgresContainer.logs()returns a Node.js stream. We attach adatalistener to print each line. Since streams are asynchronous, a smallsetTimeoutis added to give the logs a chance to print before the test potentially exits. For more robust log capture, you might collect stream data into a buffer. - Pro-tip: For direct debugging of the
testcontainers-nodelibrary itself, you can set theDEBUG=testcontainersenvironment variable when running your tests to get verbose output.
Python Example
For Python, pytest-docker or testcontainers-python provide mechanisms to get logs. We’ll use testcontainers-python here.
First, install dependencies:
pip install testcontainers-python[postgresql] psycopg2-binary
# testcontainers-python v4.14.1 (Jan 2026)
test_postgresql_logs.py
import pytest
from testcontainers.postgres import PostgresContainer
import psycopg2
@pytest.fixture(scope="module")
def postgres_container():
# Use an explicit image and version
with PostgresContainer("postgres:16.2", dbname="testdb", user="testuser", password="testpass") as postgres:
postgres.start()
print(f"PostgreSQL container started at: {postgres.get_connection_url()}")
yield postgres
@pytest.fixture(scope="function")
def db_connection(postgres_container):
conn = None
try:
conn = psycopg2.connect(postgres_container.get_connection_url())
yield conn
finally:
if conn:
conn.close()
def test_database_connection_and_bad_query(postgres_container, db_connection):
try:
cursor = db_connection.cursor()
cursor.execute("CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100));")
cursor.execute("INSERT INTO users (name) VALUES ('Alice');")
db_connection.commit()
# Intentionally malformed query
cursor.execute("SELECT * FROM non_existent_table;")
db_connection.commit() # This commit will likely fail or cause earlier error to surface
cursor.execute("SELECT name FROM users WHERE name = 'Alice';")
result = cursor.fetchone()
assert result is not None and result[0] == 'Alice'
except psycopg2.Error as e:
print(f"Test failed with psycopg2.Error: {e}")
# Get logs from the container
print("--- PostgreSQL Container Logs ---")
# In testcontainers-python, get_logs() returns a single string of all logs
print(postgres_container.get_logs())
print("-------------------------------")
pytest.fail(f"Database operation failed: {e}")
except Exception as e:
print(f"Test failed with unexpected exception: {e}")
print("--- PostgreSQL Container Logs ---")
print(postgres_container.get_logs())
print("-------------------------------")
pytest.fail(f"Unexpected error: {e}")
Explanation:
- Similar pattern: a malformed query to trigger an error.
- In the
exceptblock,postgres_container.get_logs()is called to retrieve all accumulated logs from the container, which are then printed to the console. pytest.fail()is used to explicitly mark the test as failed and provide the error message.
2. Keeping Containers Alive for Inspection (TESTCONTAINERS_RYUK_DISABLED)
When a test fails, Testcontainers, by default, cleans up the containers immediately. This is usually desirable, but for debugging, you might want to “pause” the cleanup process to inspect the container manually using Docker CLI tools.
You can achieve this by setting the environment variable TESTCONTAINERS_RYUK_DISABLED=true before running your tests.
How it works: Ryuk is a small container that Testcontainers launches to track and clean up other containers. By setting TESTCONTAINERS_RYUK_DISABLED=true, you tell Testcontainers not to start Ryuk. This means any containers launched by Testcontainers will remain running after your test JVM/process exits, allowing you to examine them.
Important Considerations:
- Do NOT use this in CI/CD or production environments. This is purely for local debugging. If you forget to clean up manually, you’ll accumulate stale containers.
- Manual Cleanup Required: When
Ryukis disabled, you are responsible for stopping and removing the containers manually usingdocker rm -f <container_id>ordocker stop <container_id>.
Steps to use TESTCONTAINERS_RYUK_DISABLED:
- Open your terminal.
- Set the environment variable:
- Linux/macOS:
export TESTCONTAINERS_RYUK_DISABLED=true - Windows (Command Prompt):
set TESTCONTAINERS_RYUK_DISABLED=true - Windows (PowerShell):
$env:TESTCONTAINERS_RYUK_DISABLED="true"
- Linux/macOS:
- Run your tests as usual.
- After the tests finish (and fail), open a new terminal window (or ensure the variable is still set in your current one if you don’t launch a new shell).
- Use Docker commands to inspect:
docker ps -a: Lists all containers (running and exited). Look for the name of your container (e.g.,testcontainers/postgresql).docker logs <container_id_or_name>: View the logs again.docker exec -it <container_id_or_name> bash: (orsh,psql, etc.) Execute a shell or a specific command inside the running container. This is incredibly powerful for checking file systems, running database queries directly, or inspecting application state.docker inspect <container_id_or_name>: Get detailed low-level information about the container’s configuration, network, volumes, etc.
- Once you’re done, remember to clean up:
docker rm -f <container_id_or_name>.
3. Remote Debugging an Application Inside a Testcontainer
This is the “heavy artillery” of debugging. If logs and inspection aren’t enough, you can connect your IDE’s debugger directly to your application process running inside a Testcontainer. This lets you set breakpoints, step through code, and inspect variables just as you would with a locally running application.
The general principle involves:
- Configuring your application inside the container to start in debug mode and listen on a specific port.
- Mapping this debug port from the container to a port on your host machine.
- Configuring your IDE to connect to this host port for remote debugging.
Java (Spring Boot Example)
Let’s imagine you have a Spring Boot application that connects to a database. We’ll run this application inside a Testcontainer and debug it.
Dockerfile for a simple Spring Boot app
FROM openjdk:17-jdk-slim
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
# Expose default Spring Boot port and debug port
EXPOSE 8080
EXPOSE 5005
ENTRYPOINT ["java", "-jar", "app.jar"]
MySpringBootDebugTest.java
package com.example.testcontainers;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Testcontainers
class MySpringBootDebugTest {
private static Network network = Network.newNetwork();
@Container
private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(DockerImageName.parse("postgres:16.2"))
.withDatabaseName("mydb")
.withUsername("myuser")
.withPassword("mypass")
.withNetwork(network)
.withNetworkAliases("postgres-db"); // Alias for app to connect to
@Container
private static GenericContainer<?> myApp = new GenericContainer<>(DockerImageName.parse("my-spring-boot-app:latest")) // Replace with your app's image
.withNetwork(network)
.withEnv("SPRING_DATASOURCE_URL", "jdbc:postgresql://postgres-db:5432/mydb")
.withEnv("SPRING_DATASOURCE_USERNAME", "myuser")
.withEnv("SPRING_DATASOURCE_PASSWORD", "mypass")
// Crucial for remote debugging: enable debug agent and expose port
// Use JAVA_TOOL_OPTIONS for flexibility, common in containerized apps
.withEnv("JAVA_TOOL_OPTIONS", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005")
.withExposedPorts(8080, 5005) // Expose app port and debug port
.waitingFor(Wait.forHttp("/actuator/health").forPort(8080).withStartupTimeout(Duration.ofSeconds(60)));
@Test
void testAppWithRemoteDebuggingCapability() throws IOException, InterruptedException {
// Log the debug port for easy IDE connection
System.out.println("Spring Boot App Debug Port (Host): " + myApp.getMappedPort(5005));
System.out.println("Spring Boot App HTTP Port (Host): " + myApp.getMappedPort(8080));
// Now, manually attach your IDE's remote debugger to localhost:<mapped_debug_port> (e.g., 5005)
// Set a breakpoint in your Spring Boot application's code.
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://" + myApp.getHost() + ":" + myApp.getMappedPort(8080) + "/hello")) // Replace with your actual endpoint
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode());
// Assert on response content if needed
System.out.println("App response: " + response.body());
// Keep the test running briefly to allow debugger interaction if needed
// Thread.sleep(5000);
}
}
Explanation:
my-spring-boot-app:latest: You’d replace this with the actual Docker image of your Spring Boot application. You’d build this image locally first (docker build -t my-spring-boot-app:latest .).JAVA_TOOL_OPTIONS: This environment variable is a common way to pass JVM options, including debugging options, to a Java application within a container.agentlib:jdwp: Activates the Java Debug Wire Protocol agent.transport=dt_socket: Specifies socket transport.server=y: Makes the JVM listen for a debugger connection.suspend=n: Allows the application to start normally and wait for a debugger to attach. Ifsuspend=y, the application would pause immediately upon startup until a debugger connects.address=*:5005: Tells the debugger to listen on port 5005 on all interfaces.
.withExposedPorts(8080, 5005): Testcontainers exposes both your application’s HTTP port (8080) and the debug port (5005).getMappedPort(5005): This provides the dynamically assigned host port that maps to the container’s internal debug port. You’ll use this host port in your IDE.
How to connect with IntelliJ IDEA:
- Go to
Run->Edit Configurations.... - Add a new
Remote JVM Debugconfiguration. - Set
HosttolocalhostandPortto the mapped debug port printed by your test (e.g.,5005). - Run your Testcontainers test. Once the application container starts, quickly switch to IntelliJ and “Debug” your newly created Remote JVM Debug configuration. Your IDE should connect, and you can now set breakpoints in your Spring Boot application code.
JavaScript/TypeScript (Node.js Example)
For Node.js, we use the --inspect flag.
Dockerfile for a simple Node.js app
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
EXPOSE 9229 # Default Node.js debug port
CMD ["node", "src/index.js"]
my-node-app.test.ts
import { GenericContainer, StartedTestContainer } from 'testcontainers';
import { Wait } from 'testcontainers';
import fetch from 'node-fetch'; // npm install node-fetch@2 for older CommonJS, or use global fetch in Node 18+
describe('My Node.js App Debug Test', () => {
let appContainer: StartedTestContainer;
beforeAll(async () => {
appContainer = await new GenericContainer('my-node-app:latest') // Replace with your app's image
.withExposedPorts(3000, 9229) // Expose app port and debug port
// Enable Node.js inspector
.withCommand(['node', '--inspect=0.0.0.0:9229', 'src/index.js']) // Listen on all interfaces
.waitingFor(Wait.forHttp("/health").forPort(3000).withStartupTimeout(120000))
.start();
console.log("Node.js App Debug Port (Host):", appContainer.getMappedPort(9229));
console.log("Node.js App HTTP Port (Host):", appContainer.getMappedPort(3000));
// Wait a moment for the debugger to be ready if suspend=n,
// or quickly attach your debugger if suspend=y
await new Promise(resolve => setTimeout(resolve, 2000));
}, 180000); // 3 minute timeout for container startup
afterAll(async () => {
await appContainer.stop();
});
it('should respond to an HTTP request', async () => {
const response = await fetch(`http://${appContainer.getHost()}:${appContainer.getMappedPort(3000)}/`);
const text = await response.text();
expect(response.status).toBe(200);
expect(text).toContain('Hello from Node.js!');
}, 10000);
});
Explanation:
my-node-app:latest: Replace with your actual Node.js app image.--inspect=0.0.0.0:9229: This command-line argument tells Node.js to start its inspector (debugger) on port 9229, listening on all network interfaces inside the container..withExposedPorts(3000, 9229): Exposes both the app’s port (3000) and the debugger port (9229).getMappedPort(9229): Get the host-mapped port for the debugger.
How to connect with VS Code:
- In VS Code, go to
Run and Debugtab. - Click
create a launch.json fileif you don’t have one. - Add a new configuration of type
Node.js: Attach to Process. Modify it to look like this:{ "name": "Attach to Testcontainers Node.js App", "type": "node", "request": "attach", "address": "localhost", "port": 9229, // Use the mapped port from getMappedPort(9229) "localRoot": "${workspaceFolder}", "remoteRoot": "/app" // This is the WORKDIR in your Dockerfile } - Run your Testcontainers test. Once the
myAppcontainer starts and prints the mapped debug port, quickly start the “Attach to Testcontainers Node.js App” debugger in VS Code.
Python (Flask/Django with debugpy Example)
Python applications often use a dedicated debugging library like debugpy.
Dockerfile for a simple Flask app
FROM python:3.10-slim-buster
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000 # Flask default port
EXPOSE 5678 # debugpy default port
CMD ["python", "app.py"]
app.py (simple Flask app with debugpy integration)
import debugpy
from flask import Flask
# Check for debugpy environment variable to enable debugging
if debugpy.is_client_connected():
print("Debugger is attached. Ready to debug.")
else:
# This block allows you to start the debugger server before your app logic,
# and wait for a client to attach.
# It's better to configure this via environment variables or CLI flags
# rather than hardcoding in prod, but for an example, this works.
try:
debugpy.listen(("0.0.0.0", 5678)) # Listen on all interfaces on port 5678
print("Debugpy server listening on 0.0.0.0:5678")
# debugpy.wait_for_client() # Uncomment if you want the app to *suspend* until debugger attaches
# print("Debugger client connected.")
except Exception as e:
print(f"Error starting debugpy: {e}")
app = Flask(__name__)
@app.route('/')
def hello():
# Set a breakpoint here in your IDE once connected
message = "Hello from Flask in Testcontainers!"
return message
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
test_flask_debug.py
import pytest
from testcontainers.core.generic import GenericContainer
from testcontainers.core.waiting_utils import wait_for_http_status
import requests
@pytest.fixture(scope="module")
def flask_app_container():
# Ensure your Flask Docker image is built locally first:
# docker build -t my-flask-app:latest .
container = GenericContainer("my-flask-app:latest") \
.with_exposed_ports(5000, 5678) \
.with_env("PYTHONUNBUFFERED", "1") # Important for real-time logs in Python
# No need to explicitly add debugpy command here if it's in app.py
# or enabled by env vars in app.py.
# If using CLI flag, it would be .with_command(["python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "--wait-for-client", "app.py"])
with container as c:
c.start()
# Wait for HTTP endpoint to be ready
wait_for_http_status(f"http://{c.get_host()}:{c.get_mapped_port(5000)}/", 200, timeout=60)
print(f"Flask App Debug Port (Host): {c.get_mapped_port(5678)}")
print(f"Flask App HTTP Port (Host): {c.get_mapped_port(5000)}")
yield c
def test_flask_app_endpoint(flask_app_container):
# Connect your VS Code debugger now to localhost:mapped_debug_port
# and set a breakpoint in app.py
host = flask_app_container.get_host()
port = flask_app_container.get_mapped_port(5000)
response = requests.get(f"http://{host}:{port}/")
assert response.status_code == 200
assert "Hello from Flask" in response.text
Explanation:
my-flask-app:latest: Replace with your Flask app image.debugpy.listen(("0.0.0.0", 5678)): Inapp.py, this line tellsdebugpyto start a debugging server, listening on port 5678 on all interfaces..with_exposed_ports(5000, 5678): Exposes both the app’s port (5000) and the debug port (5678).get_mapped_port(5678): Gets the host-mapped port for the debugger.
How to connect with VS Code:
- As with Node.js, create or modify your
launch.jsonfile. - Add a new configuration of type
Python: Remote Attach. Modify it:{ "name": "Python: Remote Attach (Testcontainers)", "type": "python", "request": "attach", "connect": { "host": "localhost", "port": 5678 // Use the mapped port from get_mapped_port(5678) }, "pathMappings": [ { "localRoot": "${workspaceFolder}", "remoteRoot": "/app" // This is the WORKDIR in your Dockerfile } ], "justMyCode": false // Set to false to debug library code if needed } - Run your Testcontainers test. Once the
flask_app_containerstarts and prints the mapped debug port, quickly start the “Python: Remote Attach (Testcontainers)” debugger in VS Code.
Mini-Challenge: Debug a Failing Kafka Test
Let’s put your debugging skills to the test!
Scenario: You have a Testcontainers-based test that uses Kafka. It’s failing, and you suspect an issue with message production or consumption, or perhaps the Kafka broker itself.
Challenge:
- Create a simple Testcontainers test with a Kafka container (use
testcontainers-java/kafka,testcontainers-node/kafka, ortestcontainers-python/kafka). - Intentionally introduce a subtle error in your Kafka client code (e.g., publish to a topic that the consumer isn’t subscribed to, or send a message that’s too large if you configured Kafka to reject it).
- Run the test. It should fail.
- Now, re-run the test with
TESTCONTAINERS_RYUK_DISABLED=true(or the equivalent for your environment). - After the test fails and completes, use
docker ps -ato find your Kafka container. - Use
docker logs <kafka_container_id>to inspect the Kafka broker’s logs. Look for error messages related to topics, producers, or consumers. - (Bonus): Use
docker exec -it <kafka_container_id> bash(orsh) to get a shell inside the Kafka container. See if you can use Kafka’s command-line tools (likekafka-topics.shorkafka-console-consumer.sh) to inspect the topic and messages directly.
Hint: Focus on the error messages in both your test code’s output and the Kafka container’s logs. The combination often reveals the root cause. Remember to clean up your containers manually afterwards!
Common Pitfalls & Troubleshooting
Debugging is largely about pattern recognition and methodical investigation. Here are some common issues you might encounter with Testcontainers and how to approach them:
Container Fails to Start (or takes too long):
- Symptoms:
ContainerStartupException,TimeoutException, tests just hang. - Troubleshooting:
- Check Docker: Is your Docker daemon running? Can you run
docker pssuccessfully from your terminal? - Image Availability: Is the Docker image specified available? Check for typos in the image name or tag (e.g.,
postgres:latestmight pull a huge image;postgres:16.2is better). Try pulling it manually:docker pull <image_name>. - Waiting Strategies: Are your
Wait.for...conditions correct? Is the port exposed that you’re waiting for? Is the health check endpoint actually available? Sometimes the service inside the container takes longer to fully initialize than the basic port opening. - Container Logs (immediately): Use the programmatic log capture (e.g.,
getLogs()) or theSlf4jLogConsumerto see what the container is printing during startup. This is often the quickest way to spot configuration errors within the container’s entrypoint. - Resources: Is your Docker engine allocated enough memory/CPU? Large containers or many containers can exhaust resources.
- Check Docker: Is your Docker daemon running? Can you run
- Symptoms:
Application Inside Container Fails (after container starts):
- Symptoms: Your application fails with an internal error, but the Testcontainer itself seemed to start.
- Troubleshooting:
- Application Logs: The number one place to look. Capture them using
getLogs()orlogs(), or pipe them to your console. - Environment Variables/Configuration: Did you pass all necessary environment variables (e.g.,
SPRING_DATASOURCE_URL,KAFKA_BOOTSTRAP_SERVERS) correctly? Are there typos? Are the host/port values inside the container correct (e.g., using network aliases likepostgres-dbinstead oflocalhostor127.0.0.1)? - Dependencies: Can your application reach its dependencies (e.g., database, message queue) inside the Testcontainers network? If using
Network.newNetwork(), ensure all relevant containers are added to it and use network aliases. - Remote Debugging: If logs aren’t sufficient, remote debugging (as covered above) is your next step to step through your application code.
- Application Logs: The number one place to look. Capture them using
Networking Issues:
- Symptoms:
Connection refused,Host unreachable,Timeout. - Troubleshooting:
- Exposed Ports: Did you expose the correct ports with
.withExposedPorts()? - Mapped Ports: Are you connecting to the
getMappedPort()on the host, not the internal container port? - Firewall: Is your host machine’s firewall blocking connections to the mapped ports?
- Internal Network: If containers need to talk to each other, are they on the same
Network(e.g.,Network.newNetwork())? Are you using the correct network aliases/hostnames for inter-container communication? - Host OS Differences: Windows/WSL2 and macOS sometimes have slightly different networking behaviors or firewall rules for Docker.
- Exposed Ports: Did you expose the correct ports with
- Symptoms:
Resource Exhaustion / Stale Containers:
- Symptoms: Docker slows down, “No space left on device” errors, containers piling up after tests.
- Troubleshooting:
- Ryuk Disabled? If you used
TESTCONTAINERS_RYUK_DISABLED=true, ensure you manually clean up your containers. - Testcontainers Cleanup: If
Ryukis enabled, check your Testcontainers library version. Older versions or specific configurations might have issues. Usually,Ryukis very reliable. - Docker System Prune: Periodically run
docker system prune -fto clean up unused images, containers, volumes, and networks. Be careful, this is aggressive! - Memory/CPU: Monitor Docker Desktop’s resource usage. Increase allocated resources if needed.
- Ryuk Disabled? If you used
Debugging Testcontainers tests is an iterative process. Start with the simplest tools (logs), and escalate to more powerful ones (inspection, remote debugging) if the initial clues aren’t enough. With practice, you’ll become incredibly efficient at diagnosing and resolving issues in your containerized test environments!
Summary
You’ve now added powerful debugging skills to your Testcontainers toolkit! Here’s a quick recap of the key takeaways:
- Logs are Your First Line of Defense: Always start by checking the container’s logs for immediate clues about startup failures or application errors. Testcontainers provides programmatic access (
getLogs(),logs()) for this. - Temporarily Disable Ryuk for Inspection: Use
TESTCONTAINERS_RYUK_DISABLED=trueto keep containers alive after test execution, allowing you to usedocker ps,docker logs,docker exec, anddocker inspectfor manual, in-depth analysis. Remember to clean up manually! - Remote Debugging is for Deep Dives: When logs aren’t enough, configure your application inside the container to expose a debug port (e.g.,
JAVA_TOOL_OPTIONS,--inspect,debugpy). Map this port to your host and attach your IDE’s debugger for full step-through capabilities. - Common Pitfalls: Be aware of issues like container startup failures, incorrect environment configuration, networking problems, and resource exhaustion.
- Iterative Process: Debugging is a journey. Start simple, gather information, form hypotheses, and escalate your tools as needed.
With these techniques, you’re well-equipped to tackle the challenges of containerized test environments and ensure your Testcontainers-powered integration tests are robust and reliable.
Next up, we’ll shift our focus to integrating Testcontainers into your CI/CD pipelines, making sure your debugging efforts translate into smooth, automated testing workflows!
References
- Testcontainers Official Documentation: https://testcontainers.com/
- Testcontainers Java GitHub Releases (for latest version info): https://github.com/testcontainers/testcontainers-java/releases
- Testcontainers Node.js GitHub Releases (for latest version info): https://github.com/testcontainers/testcontainers-node/releases
- Testcontainers Python GitHub Releases (for latest version info): https://github.com/testcontainers/testcontainers-python/releases
- Docker CLI Reference (for
logs,exec,inspect): https://docs.docker.com/engine/reference/commandline/cli/ - Node.js Debugger Documentation: https://nodejs.org/docs/latest/api/debugger.html
debugpy(Python Debugger) Documentation: https://github.com/microsoft/debugpy
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.