testcontainers-node icon indicating copy to clipboard operation
testcontainers-node copied to clipboard

Selenium container not using provided network

Open GeezFORCE opened this issue 1 year ago • 12 comments

Encountering this issue when trying expose a host port in compose environment within jest global setup.

Expected Behaviour

Description of the Issue

I am writing a series of tests and as per the previous issue I opened, I was advised to try out jest global setup for creating the environment which can be reused. I moved to jest and was able to migrate most of my test cases.

I am right now trying to integrate some selenium tests into the mix. For this I was utilizing the Selenium Testcontainer module. For using the selenium container, I tried to expose the host ports as shown below and it fails. The port must be exposed and the compose environment must be up, but as soon as the port gets exposed the whole environment exits.

Alternatives Tried I tried using top-level awaits, waiting for the port to get exposed and then starting the environment to no avail. I also tried to share the docker compose network to selenium and that too does not seem to work.

Actual Behaviour Test containers exits just after exposing the port

Testcontainer Logs

testcontainers [DEBUG] Acquiring lock file "/var/folders/1z/mppcm56x179_dq32fmbv89_m0000gn/T/testcontainers-node-sshd.lock"... +0ms
  testcontainers [DEBUG] Acquired lock file "/var/folders/1z/mppcm56x179_dq32fmbv89_m0000gn/T/testcontainers-node-sshd.lock" +2ms
  testcontainers [DEBUG] Checking container runtime strategy "TestcontainersHostStrategy"... +0ms
  testcontainers [DEBUG] Loading ".testcontainers.properties" file... +0ms
  testcontainers [DEBUG] Loaded ".testcontainers.properties" file +1ms
  testcontainers [DEBUG] Found custom configuration: tcHost: "tcp://127.0.0.1:49421", dockerHost: "tcp://127.0.0.1:49421" +1ms
  testcontainers [TRACE] Fetching Docker info... +0ms
  testcontainers [DEBUG] Container runtime strategy "TestcontainersHostStrategy" does not work: "Error: connect ECONNREFUSED 127.0.0.1:49421" +6ms
  testcontainers [DEBUG] Error: connect ECONNREFUSED 127.0.0.1:49421
  testcontainers     at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1611:16) +1ms
  testcontainers [DEBUG] Checking container runtime strategy "ConfigurationStrategy"... +0ms
  testcontainers [TRACE] Fetching Docker info... +0ms
  testcontainers [DEBUG] Container runtime strategy "ConfigurationStrategy" does not work: "Error: connect ECONNREFUSED 127.0.0.1:49421" +1ms
  testcontainers [DEBUG] Error: connect ECONNREFUSED 127.0.0.1:49421
  testcontainers     at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1611:16) +0ms
  testcontainers [DEBUG] Checking container runtime strategy "UnixSocketStrategy"... +0ms
  testcontainers [DEBUG] Container runtime strategy "UnixSocketStrategy" is not applicable +0ms
  testcontainers [DEBUG] Checking container runtime strategy "RootlessUnixSocketStrategy"... +0ms
  testcontainers [TRACE] Fetching Docker info... +1ms
  testcontainers [TRACE] Fetching remote container runtime socket path... +20ms
  testcontainers [TRACE] Resolving host... +0ms
  testcontainers [TRACE] Fetching Compose info... +0ms
  testcontainers [TRACE] Looking up host IPs... +656ms
  testcontainers [TRACE] Initialising clients... +7ms
  testcontainers [TRACE] Container runtime info:
  testcontainers {
  testcontainers   "node": {
  testcontainers     "version": "v22.9.0",
  testcontainers     "architecture": "arm64",
  testcontainers     "platform": "darwin"
  testcontainers   },
  testcontainers   "containerRuntime": {
  testcontainers     "host": "localhost",
  testcontainers     "hostIps": [
  testcontainers       {
  testcontainers         "address": "::1",
  testcontainers         "family": 6
  testcontainers       },
  testcontainers       {
  testcontainers         "address": "127.0.0.1",
  testcontainers         "family": 4
  testcontainers       }
  testcontainers     ],
  testcontainers     "remoteSocketPath": "/var/run/docker.sock",
  testcontainers     "indexServerAddress": "https://index.docker.io/v1/",
  testcontainers     "serverVersion": "27.2.0",
  testcontainers     "operatingSystem": "Docker Desktop",
  testcontainers     "operatingSystemType": "linux",
  testcontainers     "architecture": "aarch64",
  testcontainers     "cpus": 2,
  testcontainers     "memory": 4112158720,
  testcontainers     "runtimes": [
  testcontainers       "io.containerd.runc.v2",
  testcontainers       "runc"
  testcontainers     ],
  testcontainers     "labels": [
  testcontainers       "com.docker.desktop.address=unix:///Users/user/Library/Containers/com.docker.docker/Data/docker-cli.sock"
  testcontainers     ]
  testcontainers   },
  testcontainers   "compose": {
  testcontainers     "version": "2.29.2-desktop.2",
  testcontainers     "compatability": "v2"
  testcontainers   }
  testcontainers } +0ms
  testcontainers [DEBUG] Container runtime strategy "RootlessUnixSocketStrategy" works +0ms
  testcontainers [DEBUG] Acquiring lock file "/var/folders/1z/mppcm56x179_dq32fmbv89_m0000gn/T/testcontainers-node.lock"... +1ms
  testcontainers [DEBUG] Acquired lock file "/var/folders/1z/mppcm56x179_dq32fmbv89_m0000gn/T/testcontainers-node.lock" +2ms
  testcontainers [DEBUG] Listing containers... +0ms
  testcontainers [DEBUG] Listed containers +6ms
  testcontainers [DEBUG] Creating new Reaper for session "1911d6792f15" with socket path "/var/run/docker.sock"... +1ms
  testcontainers [DEBUG] Checking if image exists "testcontainers/ryuk:0.5.1"... +1ms
  testcontainers [DEBUG] Checked if image exists "testcontainers/ryuk:0.5.1" +4ms
  testcontainers [DEBUG] Image "testcontainers/ryuk:0.5.1" already exists +0ms
  testcontainers [DEBUG] Creating container for image "testcontainers/ryuk:0.5.1"... +1ms
  testcontainers [DEBUG] [8e4f1e007da9] Created container for image "testcontainers/ryuk:0.5.1" +27ms
  testcontainers [INFO] [8e4f1e007da9] Starting container for image "testcontainers/ryuk:0.5.1"... +0ms
  testcontainers [DEBUG] [8e4f1e007da9] Starting container... +0ms
  testcontainers [DEBUG] [8e4f1e007da9] Started container +191ms
  testcontainers [INFO] [8e4f1e007da9] Started container for image "testcontainers/ryuk:0.5.1" +0ms
  testcontainers [DEBUG] [8e4f1e007da9] Inspecting container... +0ms
  testcontainers [DEBUG] [8e4f1e007da9] Inspected container +4ms
  testcontainers [DEBUG] [8e4f1e007da9] Fetching container logs... +0ms
  testcontainers [DEBUG] [8e4f1e007da9] Demuxing stream... +3ms
  testcontainers [DEBUG] [8e4f1e007da9] Demuxed stream +0ms
  testcontainers [DEBUG] [8e4f1e007da9] Fetched container logs +0ms
  testcontainers [DEBUG] [8e4f1e007da9] Waiting for container to be ready... +0ms
  testcontainers [DEBUG] [8e4f1e007da9] Waiting for log message "/.+ Started!/"... +0ms
  testcontainers [DEBUG] [8e4f1e007da9] Fetching container logs... +0ms
  testcontainers:containers [8e4f1e007da9] 2024/10/14 14:34:03 Pinging Docker... +0ms
  testcontainers:containers [8e4f1e007da9] 2024/10/14 14:34:03 Docker daemon is available! +0ms
  testcontainers:containers [8e4f1e007da9] 2024/10/14 14:34:03 Starting on port 8080... +0ms
  testcontainers:containers [8e4f1e007da9] 2024/10/14 14:34:03 Started! +0ms
  testcontainers [DEBUG] [8e4f1e007da9] Demuxing stream... +3ms
  testcontainers [DEBUG] [8e4f1e007da9] Demuxed stream +0ms
  testcontainers [DEBUG] [8e4f1e007da9] Fetched container logs +0ms
  testcontainers [DEBUG] [8e4f1e007da9] Log wait strategy complete +1ms
  testcontainers [INFO] [8e4f1e007da9] Container is ready +1ms
  testcontainers [DEBUG] [8e4f1e007da9] Connecting to Reaper (attempt 1) on "localhost:55223"... +0ms
  testcontainers [DEBUG] [8e4f1e007da9] Connected to Reaper +1ms
  testcontainers [DEBUG] Releasing lock file "/var/folders/1z/mppcm56x179_dq32fmbv89_m0000gn/T/testcontainers-node.lock"... +0ms
  testcontainers [DEBUG] Released lock file "/var/folders/1z/mppcm56x179_dq32fmbv89_m0000gn/T/testcontainers-node.lock" +0ms
  testcontainers [DEBUG] Listing containers... +0ms
  testcontainers:containers [8e4f1e007da9] 2024/10/14 14:34:03 Adding {"label":{"org.testcontainers.session-id=1911d6792f15":true}} +8ms
  testcontainers:containers [8e4f1e007da9] 2024/10/14 14:34:03 New client connected: 192.168.65.1:42759 +0ms
  testcontainers [DEBUG] Listed containers +7ms
  testcontainers [DEBUG] Creating new Port Forwarder... +1ms
  testcontainers [DEBUG] Checking if image exists "testcontainers/sshd:1.2.0"... +0ms
  testcontainers [DEBUG] Checked if image exists "testcontainers/sshd:1.2.0" +2ms
  testcontainers [DEBUG] Image "testcontainers/sshd:1.2.0" already exists +0ms
  testcontainers [DEBUG] Creating container for image "testcontainers/sshd:1.2.0"... +0ms
  testcontainers [DEBUG] [c825d68800a4] Created container for image "testcontainers/sshd:1.2.0" +27ms
  testcontainers [INFO] [c825d68800a4] Starting container for image "testcontainers/sshd:1.2.0"... +0ms
  testcontainers [DEBUG] [c825d68800a4] Starting container... +0ms
  testcontainers [DEBUG] [c825d68800a4] Started container +75ms
  testcontainers [INFO] [c825d68800a4] Started container for image "testcontainers/sshd:1.2.0" +0ms
  testcontainers [DEBUG] [c825d68800a4] Inspecting container... +0ms
  testcontainers [DEBUG] [c825d68800a4] Inspected container +2ms
  testcontainers [DEBUG] [c825d68800a4] Fetching container logs... +0ms
  testcontainers [DEBUG] [c825d68800a4] Demuxing stream... +1ms
  testcontainers [DEBUG] [c825d68800a4] Demuxed stream +1ms
  testcontainers [DEBUG] [c825d68800a4] Fetched container logs +0ms
  testcontainers [DEBUG] [c825d68800a4] Waiting for container to be ready... +0ms
  testcontainers [DEBUG] [c825d68800a4] Waiting for host port 55224... +0ms
  testcontainers [DEBUG] [c825d68800a4] Waiting for internal port 22... +0ms
  testcontainers [DEBUG] [c825d68800a4] Host port 55224 ready +1ms
  testcontainers [DEBUG] [c825d68800a4] Host port wait strategy complete +0ms
  testcontainers:containers [c825d68800a4] chpasswd: password for 'root' changed +115ms
  testcontainers [DEBUG] [c825d68800a4] Internal port 22 ready +33ms
  testcontainers [INFO] [c825d68800a4] Container is ready +0ms
  testcontainers [DEBUG] Connecting to Port Forwarder on "localhost:55224"... +0ms
  testcontainers [DEBUG] Connected to Port Forwarder on "localhost:55224" +34ms
  testcontainers [DEBUG] Releasing lock file "/var/folders/1z/mppcm56x179_dq32fmbv89_m0000gn/T/testcontainers-node-sshd.lock"... +0ms
  testcontainers [DEBUG] Released lock file "/var/folders/1z/mppcm56x179_dq32fmbv89_m0000gn/T/testcontainers-node-sshd.lock" +0ms
  testcontainers [INFO] Exposing host port 80... +0ms

Process finished with exit code 0

Steps to Reproduce

  1. Start a compose environment along with exposing port as part of jest's global setup.
// Jest Global Setup File
import {DockerComposeEnvironment, log, TestContainers, Wait} from "testcontainers";

module.exports = async () => {
    try {
        await TestContainers.exposeHostPorts(80)
        global.environment = await new DockerComposeEnvironment(composeFilePath, composeFile)
                    .withWaitStrategy("testcontainer1", Wait.forHealthCheck())
                    .withWaitStrategy("testcontainer2", Wait.forLogMessage(/Server started at/))
                    .withNoRecreate()
                    .up();
    } catch (e) {
        log.error(e);
    }
};

Environment Information

  • Operating System: MacOS v 15.0.1 (M1)
  • Docker Version: 27.2.0, build 3ab4256
  • Node version: 22.9.0
  • Testcontainers version: 10.13.2

GeezFORCE avatar Oct 14 '24 14:10 GeezFORCE

An update

I was able to overcome the issue mentioned indirectly. I added the Selenium Container to the same network as the docker compose environment created using the withNetworkMode (from testcontainers-java/#915). Now, the selenium container can access the containers of the compose environment using the container names directly.

With this workaround my tests are working. But this is not ideal. I am also recording the Selenium tests to see that it is working as expected, especially during writing the tests. With this workaround, I am not able to record the tests. From the code, seems like if recording is enabled a new network is created and shared across the selenium containers.

So, I request your help for the following

  1. Are there any debugging tips you want me to try out for the primary issue?
  2. Can an enhancement be done so that the selenium recording container uses the network specified by withNetworkMode (and may be withNetwork) when they are used?

Thanks for such a great project!

GeezFORCE avatar Oct 22 '24 11:10 GeezFORCE

Hi @GeezFORCE, I am not a JavaScript or Node developer, so likely I am going into a wrong direction here, but I don't think Testcontainers will exit here because of using exposeHostPorts(), but it looks more like an issue with the async closure, not executing the second line with the DockerComposeEnvironment (I honestly don't know enough about await behavior in JavaScript do know if this direction is right, but the logs indicate this to me).

Besides, I think your workaround is a good one, if your Selenium container is not supposed to hit the host, but rather another container, putting the Selenium container into the same network is the better approach.

Can an enhancement be done so that the selenium recording container uses the network specified by withNetworkMode (and may be withNetwork) when they are used?

I think that should be possible.

kiview avatar Oct 22 '24 12:10 kiview

Hi @kiview , Thank You for the reply.

I am also pretty new to the JavaScript/ Node Ecosystem and coming from a Java background I am still wrapping my brain around how this all works. I also share your same view that the issue might be something going wrong with the code I implemented rather than with testcontainers. But, I am not able to find the correct way to debug this. I will wait for Cristian's comments on this.

For the second recommendation, I think this might be a good enhancement as this is what a dev might expect when withNetworkMode is used. I will file an enhancement request once Cristian replies.

GeezFORCE avatar Oct 22 '24 14:10 GeezFORCE

Hello @cristianrgreco

Can an enhancement be done so that the selenium recording container uses the network specified by withNetworkMode (and may be withNetwork) when they are used?

Is this feasible?

GeezFORCE avatar Nov 06 '24 11:11 GeezFORCE

Hello @cristianrgreco

Can an enhancement be done so that the selenium recording container uses the network specified by withNetworkMode (and may be withNetwork) when they are used?

Is this feasible?

Hi @GeezFORCE, yep that makes sense. The code in question is here:

https://github.com/testcontainers/testcontainers-node/blob/92c6f68039a5e05a6b9a236e67381b6f0d29a900/packages/modules/selenium/src/selenium-container.ts#L81-L82

It needs to use the provided network if provided, else create its own. Something like if (this.networkMode) ... PR welcome 👍

cristianrgreco avatar Nov 06 '24 12:11 cristianrgreco

Hello @cristianrgreco, I am trying to implement the changes for this issue. I need help with the following.

https://github.com/testcontainers/testcontainers-node/blob/e4fa915685b4fa8eddffe493594be883981fa333/packages/modules/selenium/src/selenium-container.ts#L93

Here, a network is essential to start the recording container. How can I get the instance of the network using the network mode?

Thanks in advance for the help

GeezFORCE avatar Apr 17 '25 14:04 GeezFORCE

Hi @GeezFORCE, so at the moment we always create a new network:

https://github.com/testcontainers/testcontainers-node/blob/e4fa915685b4fa8eddffe493594be883981fa333/packages/modules/selenium/src/selenium-container.ts#L81-L83

Should be a case of doing something like this:

if (!this.networkMode) {
  const network = await new Network().start();
  this.withNetwork(network);
}

cristianrgreco avatar Apr 17 '25 14:04 cristianrgreco

Hi @cristianrgreco , Thanks for the reply.

The network created is used further down in the code to create the recording container. https://github.com/testcontainers/testcontainers-node/blob/e4fa915685b4fa8eddffe493594be883981fa333/packages/modules/selenium/src/selenium-container.ts#L88

It is also used in the constructor of StartedSeleniumRecordingContainer.

https://github.com/testcontainers/testcontainers-node/blob/e4fa915685b4fa8eddffe493594be883981fa333/packages/modules/selenium/src/selenium-container.ts#L93

If the code suggested is implemented, we may need to also reimplement the code snippets above as well. I assume it is not safe to only specify the network mode and not specify the network?

GeezFORCE avatar Apr 21 '25 18:04 GeezFORCE

I think it's a bad idea for SeleniumRecordingContainer to create a new network.

The only reason network is passed to StartedSeleniumRecordingContainer, by the way, is so that its stop method can stop the network - which I also think is a bad idea, especially if the network can be provided by the user in the future.

https://github.com/testcontainers/testcontainers-node/blob/25d5b56e3c9104a29cf0266398833c986a969869/packages/modules/selenium/src/selenium-container.ts#L109

I also think it's odd that withRecording returns a new container, leaving the constructed SeleniumContainer for garbage?

https://github.com/testcontainers/testcontainers-node/blob/25d5b56e3c9104a29cf0266398833c986a969869/packages/modules/selenium/src/selenium-container.ts#L43-L44

Suggestions:

  • Override withNetwork to retain the Network in an instance variable
  • withRecording should just set a flag
  • Override beforeContainerCreated in order to create the ffmpeg container and give it the right network
  • Emit a warning or fail in beforeContainerCreated if recording is enabled and a network has not been provided

joebowbeer avatar May 13 '25 07:05 joebowbeer

I think it's a bad idea for SeleniumRecordingContainer to create a new network.

Any reason?

The only reason network is passed to StartedSeleniumRecordingContainer, by the way, is so that its stop method can stop the network - which I also think is a bad idea, especially if the network can be provided by the user in the future.

If the network is managed by the user and provided to the SeleniumContainer, then the container should not stop it. Is there any other issue?

I also think it's odd that withRecording returns a new container, leaving the constructed SeleniumContainer for garbage?

Node has a garbage collector, yes. The withRecording method returns a new type, because the recording container has additional behaviours - a different startup method, and when it's stopped, returns an instance of a StoppedRecordingContainer on which a user is able to call saveRecording. Things which are not possible if the container did not start with recording capabilities enabled. Is there an issue in making use of the type system? The alternative would be to let a user call saveRecording on an instance which can't do it, likely throwing a RuntimeException or equivalent.

cristianrgreco avatar May 13 '25 08:05 cristianrgreco

I think it's a bad idea for SeleniumRecordingContainer to create a new network.

Any reason?

It's surprising and can be confusing.

Consider, for example:

new SeleniumContainer().withRecording().withNetwork(startedNetwork)

The user thinks withNetwork() is doing something but it's not.

and then there is the example from this issue:

new SeleniumContainer().withNetwork(startedNetwork).withRecording()

Where the user's network is once again ignored.

Furthermore, there is no method to return the StartedNetwork that was created, so the user is unable to obtain it and reuse it if they need to.

Is there an issue in making use of the type system?

Um, no. But having the user create and configure a new SeleniumContainer and then have that one thrown away and replaced with another one is surprising. Fluent methods (and Builder patterns) don't usually do this.

Consider this surprise:

container = new SeleniumContainer()
if (recording) {
  container.withRecording()
}
container.start()

But I see your point. Unimplemented methods are also surprising.

Perhaps a better way to handle this than my first suggestion is to remove withRecording from SeleniumContainer and have the user either create a new SeleniumRecordingContainer or a new SeleniumContainer.

That way the container they create and configure is not thrown away.

In either case, the user should provide the network if needed.

joebowbeer avatar May 13 '25 09:05 joebowbeer

I agree with pretty much all that you said. Just a few clarifications:

Consider, for example:

new SeleniumContainer().withRecording().withNetwork(startedNetwork) The user thinks withNetwork() is doing something but it's not.

and then there is the example from this issue:

new SeleniumContainer().withNetwork(startedNetwork).withRecording() Where the user's network is once again ignored.

This is what this issue is about, we should not be ignoring it.

Furthermore, there is no method to return the StartedNetwork that was created, so the user is unable to obtain it and reuse it if they need to.

The network (if) created by the SeleniumContainer isn't meant to be exposed/shared - it's an implementation detail where the SeleniumContainer manages its own network to talk to the FfmpegContainer, and is thrown away as soon as it's no longer necessary. If a user wants to use their own network, well that's what this issue's about.

Agreed about the builder pattern, PR is welcome.

cristianrgreco avatar May 13 '25 09:05 cristianrgreco