Repository: testcontainers/testcontainers-go (Go)

Issue:
[Bug]: Regression for Kafka
### Testcontainers version

0.33.0

### Using the latest Testcontainers version?

Yes

### Host OS

Mac

### Host arch

ARM

### Go version

1.22

### Docker version

```shell
docker version
Client:
 Version:           25.0.4-rd
 API version:       1.44
 Go version:        go1.21.8
 Git commit:        c4cd0a9
 Built:             Fri Mar  8 09:09:46 2024
 OS/Arch:           darwin/arm64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          26.1.1
  API version:      1.45 (minimum version 1.24)
  Go version:       go1.21.9
  Git commit:       ac2de55
  Built:            Tue Apr 30 11:48:47 2024
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.6.31
  GitCommit:        e377cd56a71523140ca6ae87e30244719194a521
 runc:
  Version:          1.1.12
  GitCommit:        v1.1.12-0-g51d5e94
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```


### Docker info

```shell
docker info
Client: Docker Engine - Community
 Version:    27.1.2
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.16.2
    Path:     /Users/<username>/.docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  2.29.2
    Path:     /Users/<username>/.docker/cli-plugins/docker-compose

Server:
 Containers: 5
  Running: 5
  Paused: 0
  Stopped: 0
 Images: 57
 Server Version: 26.1.1
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Using metacopy: false
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: e377cd56a71523140ca6ae87e30244719194a521
 runc version: v1.1.12-0-g51d5e94
 init version: de40ad0
 Security Options:
  apparmor
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 6.8.0-31-generic
 Operating System: Ubuntu 24.04 LTS
 OSType: linux
 Architecture: aarch64
 CPUs: 2
 Total Memory: 7.738GiB
 Name: colima
 ID: be95d926-6cd1-4ed5-8556-ac01c0037ced
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false
```


### What happened?

In version 0.32.0 integration tests were passing with a KafkaContainer. In 0.33.0 the following lines in the Kafka module at the top of `copyStarterScript` result in the context deadline being exceeded and the test failing. If I remove them my tests pass again.

```
if err := wait.ForListeningPort(publicPort).
		SkipInternalCheck().
		WaitUntilReady(ctx, c); err != nil {
		return fmt.Errorf("wait for exposed port: %w", err)
	}
```

The Kafka container I am creating has the following properties:

```
kafkaContainer, err := kafka.Run(ctx,
		cfg.image,
		kafka.WithClusterID(cfg.clusterId),
		testcontainers.WithLogger(testcontainers.TestLogger(t)),
	)
```

The image for the container is `confluentinc/confluent-local:7.7.0`.

I suspect this may have to do with me using Colima on a Mac and host port mapping, what is confusing to me is that if I comment those lines out and pass the following in my container params then the tests still pass.

```
kafkaContainer, err := kafka.Run(ctx,
		cfg.image,
		kafka.WithClusterID(cfg.clusterId),
		testcontainers.WithLogger(testcontainers.TestLogger(t)),
		testcontainers.WithWaitStrategy(
		 	wait.ForListeningPort("9093/tcp"),
		 	wait.ForLog(".*Transitioning from RECOVERY to RUNNING.*").AsRegexp(),
		),
	)
```

### Relevant log output

```shell
lifecycle.go:62: 🐳 Creating container for image postgres:15.3-alpine
    lifecycle.go:68: ✅ Container created: c6eaaf53bec4
    lifecycle.go:74: 🐳 Starting container: c6eaaf53bec4
    lifecycle.go:80: ✅ Container started: c6eaaf53bec4
    lifecycle.go:271: ⏳ Waiting for container id c6eaaf53bec4 image: postgres:15.3-alpine. Waiting for: &{timeout:<nil> deadline:0x1400059fc68 Strategies:[0x14000345410 0x14000345440]}
    lifecycle.go:86: 🔔 Container is ready: c6eaaf53bec4
    lifecycle.go:62: 🐳 Creating container for image confluentinc/confluent-local:7.7.0
    lifecycle.go:68: ✅ Container created: f49cdd632ddd
    lifecycle.go:74: 🐳 Starting container: f49cdd632ddd
    lifecycle.go:80: ✅ Container started: f49cdd632ddd
    lifecycle.go:341: container logs (copy starter script: wait for exposed port: get state: Get "http://%2FUsers%2F*******%2F.colima%2Fdocker.sock/v1.45/containers/f49cdd632ddd469cbc51a3f2c820a72dd44ffece473c5f5a8e5a38cbd9c04e99/json": context deadline exceeded):
        
2024/08/26 11:42:21 failed to start container: start container: started hook: copy starter script: wait for exposed port: get state: Get "http://%2FUsers%2F******%2F.colima%2Fdocker.sock/v1.45/containers/f49cdd632ddd469cbc51a3f2c820a72dd44ffece473c5f5a8e5a38cbd9c04e99/json": context deadline exceeded.
```


### Additional information

_No response_


Discussion: Having a same issue(
facing same issue when ran on gitlab ci (ubuntu) , issue is for both kafka and redpanda modules . 
```
Could not start kafka container start container: started hook: copy starter script: wait for exposed port: get state: Get "http://docker:2375/v1.44/containers/69aa9d728f56052c537a2e23a125171ff9a5ac26421b27f1156d73de063cb235/json": context deadline exceeded
```
after reverting to 
```	
github.com/testcontainers/testcontainers-go/modules/kafka v0.32.0
```
issue resolved for me . 

@VRmnv Are you also on Colima, or is this on Docker?
@harsh1231 Reporting it for on Ubuntu suggest the usage of Docker CE (but possibly DinD?), so I don't think this issue is specifically Colima related.
@kiview this is docker 
facing the same issue in v0.35.0. The aforementioned snippet "wait.ForListeningPort(publicPort).SkipInternalCheck().WaitUntilReady(ctx, c) " is causing this issue

Environment:

`
  Server Version: 27.0.3
  API Version: 1.46
  Operating System: Ubuntu 22.04.4 LTS
  Total Memory: 5921 MB
  Testcontainers for Go Version: v0.35.0
  Resolved Docker Host: unix:///tmp/remote-docker-gotest.sock
  Resolved Docker Socket Path: /tmp/remote-docker-gotest.sock
`


Removing  `TESTCONTAINERS_HOST_OVERRIDE=$(colima ls -j | jq -r '.address')` from my rc file worked for me.
Hi,

It seems that this issue is caused by https://github.com/testcontainers/testcontainers-go/pull/2696. I don't get why github.com/testcontainers/testcontainers-go/modules/kafka@v0.37.0 (with github.com/testcontainers/testcontainers-go@v0.37.0) works well on my local environment (VirtualBox VM with Rocky Linux 9.5 and Docker Engine 28.1.1) - it looks like TCP-connection to the mapped port of container (`wait.externalCheck` function from github.com/testcontainers/testcontainers-go@v0.37.0) can be established successfully even if there is no process in container listening respective port (docker `bridge` network specific?) - but it always fails on CI/CD (remote Docker Engine 28.1.1 on RHEL-8-like OS) because of the deadlock introduced in https://github.com/testcontainers/testcontainers-go/pull/2696:

1. `kafka.copyStarterScript` function waits for 9093 port (for respective exposed port) before placing start script in container at /usr/sbin/testcontainers_start.sh (`starterScript` constant):
    > ```go
    > const publicPort = nat.Port("9093/tcp")
    > ...
    > func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*KafkaContainer, error) {
    > 	req := testcontainers.ContainerRequest{
    > 		...
    > 		ExposedPorts: []string{string(publicPort)},
    > 		...
    > 		LifecycleHooks: []testcontainers.ContainerLifecycleHooks{
    > 			{
    > 				PostStarts: []testcontainers.ContainerHook{
    > 					...
    > 					func(ctx context.Context, c testcontainers.Container) error {
    > 						// 1. copy the starter script into the container
    > 						if err := copyStarterScript(ctx, c); err != nil {
    > 							return fmt.Errorf("copy starter script: %w", err)
    > 						}
    > ...
    > func copyStarterScript(ctx context.Context, c testcontainers.Container) error {
    > 	if err := wait.ForListeningPort(publicPort).
    > 		SkipInternalCheck().
    > 		WaitUntilReady(ctx, c); err != nil {
    > 		return fmt.Errorf("wait for exposed port: %w", err)
    > 	}
    > ...
    > ```
1. At the same time container command waits for /usr/sbin/testcontainers_start.sh file to exist in container (so there is no one who can listen 9093 port until /usr/sbin/testcontainers_start.sh file is created):
    > ```go
    > const (
    > 	starterScript = "/usr/sbin/testcontainers_start.sh"
    > ...
    > func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*KafkaContainer, error) {
    > 	req := testcontainers.ContainerRequest{
    > 		...
    > 		Entrypoint: []string{"sh"},
    > 		// this CMD will wait for the starter script to be copied into the container and then execute it
    > 		Cmd: []string{"-c", "while [ ! -f " + starterScript + " ]; do sleep 0.1; done; bash " + starterScript},
    > ...
    > ```

On CI/CD `kafka.copyStarterScript` function fails after 1 minute timeout:
> ```text
> generic container: start container: started hook: copy starter script: wait for exposed port: external check: check target: retries: 578 address: remote-docker-host.local:33632: get state: Get "http://remote-docker-host.local:2375/v1.48/containers/0603dd6f36dfaf5f9e7572d5fd892ff35cb1ad2f3d57f6a39d999462cfe865bc/json": context deadline exceeded
> ```
If I check logs of Kafka container during that timeout, then I see no logs and `docker top` shows the deadlock location:
```text
$ docker top 0603dd6f36df
PID                 USER                TIME                COMMAND
2966199             1000                0:00                sh -c while [ ! -f /usr/sbin/testcontainers_start.sh ]; do sleep 0.1; done; bash /usr/sbin/testcontainers_start.sh
2966686             1000                0:00                {sleep} /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 0.1
```

Downgrade to github.com/testcontainers/testcontainers-go/modules/kafka@v0.32.0 (but still using github.com/testcontainers/testcontainers-go@v0.37.0) has resolved this issue.

Thank you.
Hi,

Here is repro repo for this issue (item 2 from the list in my [comment](https://github.com/testcontainers/testcontainers-go/issues/2670#issuecomment-2877062523) in https://github.com/testcontainers/testcontainers-go/issues/2670): https://github.com/mabrarov/testcontainers-go-kafka-2748.

Please note that issue doesn't happen if github.com/testcontainers/testcontainers-go/modules/kafka@v0.32.0 (i.e. before https://github.com/testcontainers/testcontainers-go/pull/2696) is used ([feat/before_regression](https://github.com/mabrarov/testcontainers-go-kafka-2748/compare/master...feat/before_regression) or [feat/before_regression_with_workaround](https://github.com/mabrarov/testcontainers-go-kafka-2748/compare/master...feat/before_regression_with_workaround) branches of the repro repo).

Thank you.
Thanks for the repo and the logs.

I think I see what your issue might be, if you look at output of your `docker ps` the port that timed out has been exposed via IPv6 instead of IPv4
```shell
ce53b77607ed   confluentinc/confluent-local:7.5.0   "sh -c 'while [ ! -f…"   42 seconds ago   Up 41 seconds   8082/tcp, 9092/tcp, 0.0.0.0:32840->9093/tcp, [::]:32840->9093/tcp   reverent_ritchie
```
I suspect if you disable IPv6 or fix your IPv4 -> IPv6 routing the problem will go away.



Fix (patch):
diff --git a/docs/features/wait/host_port.md b/docs/features/wait/host_port.md
index 10531e5e64..c96b4ce679 100644
--- a/docs/features/wait/host_port.md
+++ b/docs/features/wait/host_port.md
@@ -60,3 +60,42 @@ req := ContainerRequest{
     WaitingFor:   wait.ForExposedPort().SkipInternalCheck(),
 }
 ```
+
+## Skipping the external check
+
+_Testcontainers for Go_ checks if the container is listening to the port externally (outside of container, 
+from the host where _Testcontainers for Go_ is used) before returning the control to the caller.
+
+But there are cases where this external check is not needed.
+In this case, the `wait.ForListeningPort.SkipExternalCheck` can be used to skip the external check.
+
+```golang
+req := ContainerRequest{
+    Image:      "nginx:alpine",
+    // Do not check port 80 externally, check it internally only
+    WaitingFor: wait.ForListeningPort("80/tcp").SkipExternalCheck(),
+}
+```
+
+If there is a need to wait only for completion of container port mapping (which doesn't happen immediately after container is started),
+then both internal and external checks can be skipped:
+
+```golang
+req := ContainerRequest{
+    Image:        "nginx:alpine",
+    ExposedPorts: []string{"80/tcp"},
+    // Wait only for completion of port 80 mapping (from container runtime perspective), do not connect to 80 port
+    WaitingFor:   wait.ForListeningPort("80/tcp").SkipInternalCheck().SkipExternalCheck(),
+}
+```
+
+Alternatively, `wait.ForMappedPort` can be used:
+
+```golang
+req := ContainerRequest{
+    Image:        "nginx:alpine",
+    ExposedPorts: []string{"80/tcp"},
+    // Wait only for completion of port 80 mapping (from container runtime perspective), do not connect to 80 port
+    WaitingFor:   wait.ForMappedPort("80/tcp"),
+}
+```
diff --git a/modules/kafka/kafka.go b/modules/kafka/kafka.go
index 27fb2d55cb..384a1d3898 100644
--- a/modules/kafka/kafka.go
+++ b/modules/kafka/kafka.go
@@ -123,10 +123,9 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
 
 // copyStarterScript copies the starter script into the container.
 func copyStarterScript(ctx context.Context, c testcontainers.Container) error {
-	if err := wait.ForListeningPort(publicPort).
-		SkipInternalCheck().
+	if err := wait.ForMappedPort(publicPort).
 		WaitUntilReady(ctx, c); err != nil {
-		return fmt.Errorf("wait for exposed port: %w", err)
+		return fmt.Errorf("wait for mapped port: %w", err)
 	}
 
 	host, err := c.Host(ctx)
diff --git a/modules/redpanda/go.mod b/modules/redpanda/go.mod
index 74d94e31f3..c17fc8beec 100644
--- a/modules/redpanda/go.mod
+++ b/modules/redpanda/go.mod
@@ -47,7 +47,7 @@ require (
 	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
 	github.com/magiconair/properties v1.8.10 // indirect
-	github.com/mdelapenya/tlscert v0.1.0
+	github.com/mdelapenya/tlscert v0.2.0
 	github.com/moby/patternmatcher v0.6.0 // indirect
 	github.com/moby/sys/sequential v0.6.0 // indirect
 	github.com/moby/sys/user v0.4.0 // indirect
diff --git a/modules/redpanda/go.sum b/modules/redpanda/go.sum
index b227005581..0f43b7c986 100644
--- a/modules/redpanda/go.sum
+++ b/modules/redpanda/go.sum
@@ -59,8 +59,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
 github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
 github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
 github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
-github.com/mdelapenya/tlscert v0.1.0 h1:YTpF579PYUX475eOL+6zyEO3ngLTOUWck78NBuJVXaM=
-github.com/mdelapenya/tlscert v0.1.0/go.mod h1:wrbyM/DwbFCeCeqdPX/8c6hNOqQgbf0rUDErE1uD+64=
+github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
+github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
 github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
 github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
 github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
diff --git a/modules/redpanda/redpanda.go b/modules/redpanda/redpanda.go
index 5620a60666..37f875bc40 100644
--- a/modules/redpanda/redpanda.go
+++ b/modules/redpanda/redpanda.go
@@ -10,7 +10,7 @@ import (
 	"fmt"
 	"math"
 	"net/http"
-	"path/filepath"
+	"path"
 	"strings"
 	"text/template"
 	"time"
@@ -38,7 +38,6 @@ const (
 	defaultKafkaAPIPort       = "9092/tcp"
 	defaultAdminAPIPort       = "9644/tcp"
 	defaultSchemaRegistryPort = "8081/tcp"
-	defaultDockerKafkaAPIPort = "29092"
 
 	redpandaDir         = "/etc/redpanda"
 	entrypointFile      = "/entrypoint-tc.sh"
@@ -87,11 +86,12 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
 				"--memory=1G",
 			},
 			WaitingFor: wait.ForAll(
-				// Wait for the ports to be exposed only as the container needs configuration
-				// before it will bind to the ports and be ready to serve requests.
-				wait.ForListeningPort(defaultKafkaAPIPort).SkipInternalCheck(),
-				wait.ForListeningPort(defaultAdminAPIPort).SkipInternalCheck(),
-				wait.ForListeningPort(defaultSchemaRegistryPort).SkipInternalCheck(),
+				// Wait for the ports to be mapped without accessing them,
+				// because container needs Redpanda configuration before Redpanda is started
+				// and the mapped ports are part of that configuration.
+				wait.ForMappedPort(defaultKafkaAPIPort),
+				wait.ForMappedPort(defaultAdminAPIPort),
+				wait.ForMappedPort(defaultSchemaRegistryPort),
 			),
 		},
 		Started: true,
@@ -158,7 +158,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
 		},
 		testcontainers.ContainerFile{
 			Reader:            bytes.NewReader(bootstrapConfig),
-			ContainerFilePath: filepath.Join(redpandaDir, bootstrapConfigFile),
+			ContainerFilePath: path.Join(redpandaDir, bootstrapConfigFile),
 			FileMode:          600,
 		},
 	)
@@ -168,12 +168,12 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
 		req.Files = append(req.Files,
 			testcontainers.ContainerFile{
 				Reader:            bytes.NewReader(settings.cert),
-				ContainerFilePath: filepath.Join(redpandaDir, certFile),
+				ContainerFilePath: path.Join(redpandaDir, certFile),
 				FileMode:          600,
 			},
 			testcontainers.ContainerFile{
 				Reader:            bytes.NewReader(settings.key),
-				ContainerFilePath: filepath.Join(redpandaDir, keyFile),
+				ContainerFilePath: path.Join(redpandaDir, keyFile),
 				FileMode:          600,
 			},
 		)
@@ -206,7 +206,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
 		return c, err
 	}
 
-	err = ctr.CopyToContainer(ctx, nodeConfig, filepath.Join(redpandaDir, "redpanda.yaml"), 0o600)
+	err = ctr.CopyToContainer(ctx, nodeConfig, path.Join(redpandaDir, "redpanda.yaml"), 0o600)
 	if err != nil {
 		return c, fmt.Errorf("copy to container: %w", err)
 	}
diff --git a/wait/host_port.go b/wait/host_port.go
index 5b9a08d55a..14e1ad97ec 100644
--- a/wait/host_port.go
+++ b/wait/host_port.go
@@ -42,6 +42,11 @@ type HostPortStrategy struct {
 	// a shell is not available in the container or when the container doesn't bind
 	// the port internally until additional conditions are met.
 	skipInternalCheck bool
+
+	// skipExternalCheck is a flag to skip the external check, which, if used with
+	// skipInternalCheck, makes strategy waiting only for port mapping completion
+	// without accessing port.
+	skipExternalCheck bool
 }
 
 // NewHostPortStrategy constructs a default host port strategy that waits for the given
@@ -70,6 +75,12 @@ func ForExposedPort() *HostPortStrategy {
 	return NewHostPortStrategy("")
 }
 
+// ForMappedPort returns a host port strategy that waits for the given port
+// to be mapped without accessing the port itself.
+func ForMappedPort(port nat.Port) *HostPortStrategy {
+	return NewHostPortStrategy(port).SkipInternalCheck().SkipExternalCheck()
+}
+
 // SkipInternalCheck changes the host port strategy to skip the internal check,
 // which is useful when a shell is not available in the container or when the
 // container doesn't bind the port internally until additional conditions are met.
@@ -79,6 +90,15 @@ func (hp *HostPortStrategy) SkipInternalCheck() *HostPortStrategy {
 	return hp
 }
 
+// SkipExternalCheck changes the host port strategy to skip the external check,
+// which, if used with SkipInternalCheck, makes strategy waiting only for port
+// mapping completion without accessing port.
+func (hp *HostPortStrategy) SkipExternalCheck() *HostPortStrategy {
+	hp.skipExternalCheck = true
+
+	return hp
+}
+
 // WithStartupTimeout can be used to change the default startup timeout
 func (hp *HostPortStrategy) WithStartupTimeout(startupTimeout time.Duration) *HostPortStrategy {
 	hp.timeout = &startupTimeout
@@ -124,16 +144,12 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
 	ctx, cancel := context.WithTimeout(ctx, timeout)
 	defer cancel()
 
-	ipAddress, err := target.Host(ctx)
-	if err != nil {
-		return err
-	}
-
 	waitInterval := hp.PollInterval
 
 	internalPort := hp.Port
 	i := 0
 	if internalPort == "" {
+		var err error
 		// Port is not specified, so we need to detect it.
 		internalPort, err = hp.detectInternalPort(ctx, target)
 		if err != nil {
@@ -157,8 +173,7 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
 		}
 	}
 
-	var port nat.Port
-	port, err = target.MappedPort(ctx, internalPort)
+	port, err := target.MappedPort(ctx, internalPort)
 	i = 0
 
 	for port == "" {
@@ -178,8 +193,15 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
 		}
 	}
 
-	if err := externalCheck(ctx, ipAddress, port, target, waitInterval); err != nil {
-		return fmt.Errorf("external check: %w", err)
+	if !hp.skipExternalCheck {
+		ipAddress, err := target.Host(ctx)
+		if err != nil {
+			return fmt.Errorf("host: %w", err)
+		}
+
+		if err := externalCheck(ctx, ipAddress, port, target, waitInterval); err != nil {
+			return fmt.Errorf("external check: %w", err)
+		}
 	}
 
 	if hp.skipInternalCheck {