Selenium container not using provided network
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
- 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
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
- Are there any debugging tips you want me to try out for the primary issue?
- Can an enhancement be done so that the selenium recording container uses the network specified by
withNetworkMode(and may bewithNetwork) when they are used?
Thanks for such a great project!
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.
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.
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?
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 👍
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
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);
}
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?
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
withNetworkto retain the Network in an instance variable withRecordingshould just set a flag- Override
beforeContainerCreatedin order to create the ffmpeg container and give it the right network - Emit a warning or fail in
beforeContainerCreatedif recording is enabled and a network has not been provided
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.
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.
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.