Skip to content

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:

ctx := context.Background()
req := ContainerRequest{
    Image:        mysqlImage,
    ExposedPorts: []string{"3306/tcp", "33060/tcp"},
    Env: map[string]string{
        "MYSQL_ROOT_PASSWORD": "password",
        "MYSQL_DATABASE":      "database",
    },
    WaitingFor: wait.ForLog("port: 3306  MySQL Community Server - GPL"),
}
mysqlC, err := GenericContainer(ctx, GenericContainerRequest{
    ProviderType:     providerType,
    ContainerRequest: req,
    Started:          true,
})

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:

p, _ := mysqlC.MappedPort(ctx, "3306/tcp")
port := p.Int()

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.

host, _ := mysqlC.Host(ctx)

It is normally advisable to use Host and MappedPort together when constructing addresses - for example:

host, _ := mysqlC.Host(ctx)
p, _ := mysqlC.MappedPort(ctx, "3306/tcp")
port := p.Int()
connectionString := fmt.Sprintf("%s:%[email protected](%s:%d)/%s?tls=skip-verify",
    "root", "password", host, port, "database")

db, err := sql.Open("mysql", connectionString)
if err != nil {
    t.Fatal(err)
}

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.

func TestContainerAttachedToNewNetwork(t *testing.T) {
    aliases := []string{"alias1", "alias2", "alias3"}
    networkName := "new-network"
    ctx := context.Background()
    gcr := GenericContainerRequest{
        ProviderType: providerType,
        ContainerRequest: ContainerRequest{
            Image: nginxAlpineImage,
            ExposedPorts: []string{
                nginxDefaultPort,
            },
            Networks: []string{
                networkName,
            },
            NetworkAliases: map[string][]string{
                networkName: aliases,
            },
        },
        Started: true,
    }

    newNetwork, err := GenericNetwork(ctx, GenericNetworkRequest{
        ProviderType: providerType,
        NetworkRequest: NetworkRequest{
            Name:           networkName,
            CheckDuplicate: true,
        },
    })
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() {
        require.NoError(t, newNetwork.Remove(ctx))
    })

    nginx, err := GenericContainer(ctx, gcr)

    require.NoError(t, err)
    terminateContainerOnEnd(t, ctx, nginx)

    networks, err := nginx.Networks(ctx)
    if err != nil {
        t.Fatal(err)
    }
    if len(networks) != 1 {
        t.Errorf("Expected networks 1. Got '%d'.", len(networks))
    }
    network := networks[0]
    if network != networkName {
        t.Errorf("Expected network name '%s'. Got '%s'.", networkName, network)
    }

    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)
    }
}