How to locally debug containers started by Testcontainers

One of the best things Docker gave us was the port randomization. You no longer have to care about possible conflicts, especially on CI environments.

But it also created a big confusion for many users who got used to Redis listening on port 6379, or the Java debugger on 5005. Many tools are not container aware and expect things on static ports.

Well, we can’t change the whole world (yet), but maybe there is something we can do for the Testcontainers users?

The beauty of containers-as-code

IMO one of the best things about Testcontainers is that you can “code” your containers in your tests. It changes the game and opens many possibilities.

Let’s start with a simple project with the following dependencies:

testCompile 'org.testcontainers:selenium:1.11.1'
testCompile 'org.seleniumhq.selenium:selenium-server:3.141.59'

Now, we start Chrome inside a container with Testcontainers’ Selenium integration.
We will iterate over all visible same-domain links in the Testcontainers’ documentation and click a random one every N seconds:

var container = new BrowserWebDriverContainer<>()
        .withCapabilities(new ChromeOptions());

container.start();

var driver = container.getWebDriver();
driver.manage().window().maximize();
driver.get("https://testcontainers.org");

while (true) {
    try {
        var links = driver.findElementsByTagName("a").stream()
                .filter(WebElement::isDisplayed)
                .filter(it -> {
                    var href = it.getAttribute("href");
                    return href.startsWith("https://www.testcontainers.org/");
                })
                .collect(Collectors.toList());

        var element = links.get(ThreadLocalRandom.current().nextInt(links.size()));

        System.out.println("Clicking on " + element.getAttribute("href"));
        element.click();

        TimeUnit.SECONDS.sleep(2);
    } catch (WebDriverException e) {
        // ¯\_(ツ)_/¯
    }
}

Some typical code, nothing fancy here.

But isn’t it nice to be able to see what is going on with Chrome inside the container? Luckily, Selenium Docker images have VNC configured, and all we need is to connect using our favorite VNC client. Sadly, every time we start the test, the port will be randomly selected, and our VNC client will not be able to reconnect!

Of course VNC isn’t going something we will be using on a CI environment, but it would be nice to have it locally, on a static port. So, what if instead of statically mapping the port in Docker, we instead start a local proxy on a static port and redirect everything to the random port, while gracefully handling the port conflict instead? Easy as a pie!

Let’s add one more dependency:

testCompile 'com.github.terma:javaniotcpproxy:1.5'

Now, after we start the container, we can try to re-expose the port statically:

var container = new BrowserWebDriverContainer<>()
        .withCapabilities(new ChromeOptions());

container.start();

var config = new StaticTcpProxyConfig(
        5900,
        container.getContainerIpAddress(),
        container.getMappedPort(5900)
);
config.setWorkerCount(1);
var tcpProxy = new TcpProxy(config);
tcpProxy.start();
System.out.println("Running VNC proxy on vnc://localhost:5900, password: 'secret'");

The TcpProxy from https://github.com/terma/java-nio-tcp-proxy will attempt to bind port 5900 and log if it fails, without failing the tests.

Now, if we start this test again and connect with VNC, we will see this:

VNC

But there is more! If we restart the test, it will reconnect because a new proxy will start and listen on the same 5900 port:

VNC reconnect

You may say “using a proxy is generally slow”, but, at least on my machine, I was watching it clicking the URLs without any lag, everything was smooth and I was even able to click things while it is running.

Bonus - attaching a Java debugger

You can use the same technique to debug applications running inside a container:

var container = new GenericContainer<>("tomcat:alpine")
        .withExposedPorts(8080, 5005)
        .waitingFor(Wait.forHttp("/"))
        .withEnv("JAVA_OPTS", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:5005");

container.start();

var config = new StaticTcpProxyConfig(
        5005,
        container.getContainerIpAddress(),
        container.getMappedPort(5005)
);
config.setWorkerCount(1);
var tcpProxy = new TcpProxy(config);
tcpProxy.start();

Conclusion

This super simple, but powerful technique can be very useful when you’re migrating your tests to Testcontainers but want to keep your habbits and tools.

You can even make it better by checking (e.g. with a system property or something) if you’re running locally or on a CI server and not attempt to start a proxy if you know that you will not be using it. Just… code it! :)

comments powered by Disqus