Networking and communicating with containers¶
Exposing container ports to the host¶
It is common to want to connect to a container from your test process, running on the test 'host' machine. For example, you may be testing some code that needs to connect to a backend or data store container.
Generally, each required port needs to be explicitly exposed. For example, we can specify one or more ports as follows:
req := ContainerRequest{
Image: nginxAlpineImage,
ExposedPorts: []string{nginxDefaultPort},
WaitingFor: wait.ForListeningPort(nginxDefaultPort),
}
Note that this exposed port number is from the perspective of the container.
From the host's perspective Testcontainers actually exposes this on a random free port. This is by design, to avoid port collisions that may arise with locally running software or in between parallel test runs.
Because there is this layer of indirection, it is necessary to ask Testcontainers for the actual mapped port at runtime.
This can be done using the MappedPort
function, which takes the original (container) port as an argument:
port, err := container.MappedPort(ctx, nginxDefaultPort)
Warning
Because the randomised port mapping happens during container startup, the container must be running at the time MappedPort
is called.
You may need to ensure that the startup order of components in your tests caters for this.
Getting the container host¶
When running with a local Docker daemon, exposed ports will usually be reachable on localhost
.
However, in some CI environments they may instead be reachable on a different host.
As such, Testcontainers provides a convenience function to obtain an address on which the container should be reachable from the host machine.
ip, _ := nginxC.Host(ctx)
It is normally advisable to use Host
and MappedPort
together when constructing addresses - for example:
ip, _ := nginxC.Host(ctx)
port, _ := nginxC.MappedPort(ctx, "80")
_, _ = http.Get(fmt.Sprintf("http://%s:%s", ip, port.Port()))
Info
Setting the TESTCONTAINERS_HOST_OVERRIDE
environment variable overrides the host of the docker daemon where the container port is exposed. For example, TESTCONTAINERS_HOST_OVERRIDE=172.17.0.1
.
Exposing host ports to the container¶
- Since testcontainers-go v0.31.0
In some cases it is necessary to make a network connection from a container to a socket that is listening on the host machine. Natively, Docker has limited support for this model across platforms. Testcontainers, however, makes this possible, allowing your code to access services running on the host machine.
In this example, assume that freePorts
is an slice of ports on our test host machine where different servers (e.g. a web application) are running.
We can simply create a container and expose these ports to the container using the ContainerRequest
struct:
ContainerRequest: testcontainers.ContainerRequest{
Image: "alpine:3.17",
HostAccessPorts: freePorts,
Cmd: []string{"top"},
},
Warning
Note that the server/s listening on those ports on the host must have been started before the container is created.
From a container's perspective, the hostname will be host.testcontainers.internal
and the port will be the same value as any in the freePorts
slice. Testcontainers for Go exposes the host internal name as the testcontainers.HostInternal
constant, so you can use it to build the address to connect to the host on the exposed port.
code, reader, err := c.Exec(
context.Background(),
[]string{"wget", "-q", "-O", "-", fmt.Sprintf("http://%s:%d", testcontainers.HostInternal, port)},
tcexec.Multiplexed(),
)
In the above example we are executing an HTTP request from the command line inside the given container to the host machine.
How it works¶
When you expose a host port to a container, Testcontainers for Go creates an SSHD server companion container, which will be used to forward the traffic from the container to the host machine. This is done by creating a tunnel between the container and the host machine through the SSHD server container.
You can find more information about this SSHD server container on its Github repository: https://github.com/testcontainers/sshd-docker.
sshdImage string = "testcontainers/sshd:1.2.0"
Important
At this moment, each container request will use a new SSHD server container. This means that if you create multiple containers with exposed host ports, each one will have its own SSHD server container.
Docker's host networking mode¶
From Docker documentation:
If you use the host network mode for a container, that container’s network stack is not isolated from the Docker host (the container shares the host’s networking namespace), and the container does not get its own IP-address allocated. For instance, if you run a container which binds to port 80 and you use host networking, the container’s application is available on port 80 on the host’s IP address.
But according to those docs, it's supported only for Linux hosts:
The host networking driver only works on Linux hosts, and is not supported on Docker Desktop for Mac, Docker Desktop for Windows, or Docker EE for Windows Server.
In the case you need to skip a test on non-Linux hosts, you can use the SkipIfDockerDesktop
function:
ctx := context.Background()
SkipIfDockerDesktop(t, ctx)
It will try to get a Docker client and obtain its Info. In the case the Operation System is "Docker Desktop", it will skip the test.
Advanced networking¶
Docker provides the ability for you to create custom networks and place containers on one or more networks. Then, communication can occur between networked containers without the need of exposing ports through the host. With Testcontainers, you can do this as well.
Tip
Note that Testcontainers for Go allows a container to be on multiple networks including network aliases.
For more information about how to create networks using Testcontainers for Go, please refer to the How to create a network section.
func TestContainerAttachedToNewNetwork(t *testing.T) {
ctx := context.Background()
newNetwork, err := network.New(ctx)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
require.NoError(t, newNetwork.Remove(ctx))
})
networkName := newNetwork.Name
aliases := []string{"alias1", "alias2", "alias3"}
gcr := testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: nginxAlpineImage,
ExposedPorts: []string{
nginxDefaultPort,
},
Networks: []string{
networkName,
},
NetworkAliases: map[string][]string{
networkName: aliases,
},
},
Started: true,
}
nginx, err := testcontainers.GenericContainer(ctx, gcr)
require.NoError(t, err)
defer func() {
require.NoError(t, nginx.Terminate(ctx))
}()
networks, err := nginx.Networks(ctx)
if err != nil {
t.Fatal(err)
}
if len(networks) != 1 {
t.Errorf("Expected networks 1. Got '%d'.", len(networks))
}
nw := networks[0]
if nw != networkName {
t.Errorf("Expected network name '%s'. Got '%s'.", networkName, nw)
}
networkAliases, err := nginx.NetworkAliases(ctx)
if err != nil {
t.Fatal(err)
}
if len(networkAliases) != 1 {
t.Errorf("Expected network aliases for 1 network. Got '%d'.", len(networkAliases))
}
networkAlias := networkAliases[networkName]
require.NotEmpty(t, networkAlias)
for _, alias := range aliases {
require.Contains(t, networkAlias, alias)
}
networkIP, err := nginx.ContainerIP(ctx)
if err != nil {
t.Fatal(err)
}
if len(networkIP) == 0 {
t.Errorf("Expected an IP address, got %v", networkIP)
}
}