Why you should never use fixed ports in your Testcontainers tests
I admit it - people are used to static ports.
Web server? port 80
! Redis? 6379
! Java app? 8080
!
But let’s be honest, the following also looks painfully familiar:
Bind for 0.0.0.0:80 failed: port is already allocated.
But there is more - localhost
is the king!
There was some disturbance back in the days of docker-machine
(magical 192.168.99.100
, anyone?
Heck, I even wrote a tool that auto-forwards ports to localhost
),
but people really think everything they start on from their machine should be on localhost
.
However, the technologies are growing, we now develop microservices where each service requires his own set of databases to start, and the likehood of the port conflicts is growing.
Luckily, the tooling is growing too, and thanks to projects like Testcontainers, we can start databases and other components without having to worry about the host and ports, because they take care of randomization and provide programmatic access to the actual values.
But… Old habbits sometimes make people not want changes, and we constantly receive questions like
“How do I run a container on fixed port?”.
And, while you can, we strongly advise against it.
The twist is that if you think that the conflicts is the only issue you will have, you’re missing an important detail!
OSS maintainers are stupid
Let’s say you have some code you want to test, and it requires a PostgreSQL database to run.
In “before Docker” era, you would have something like this:
$ cat src/test/resources/application-test.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/databasename
It expects that PostgreSQL will be available on localhost:5432
.
Note, however, that it does not say which version of PostgreSQL, 9? 10? 11? Anyways, let’s skip this question for now.
Now you try Testcontainers, because you’ve read some article saying that it can help you starting Docker containers from your tests.
But the docs say:
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.
Obviously, our tests will fail, since there won’t be anything started by Testcontainers on port 5432
.
What we do? Of course we read the docs go to GitHub/Slack/StackOverflow and ask “How to use fixed ports?”.
Eventually you find the answer, and, despite the inconvenient API, it even works (on your machine)!
What could go wrong?
if something can go wrong it will
You push your tests, and 10 minutes after your colleague sends you an IM comes to your desk and reports:
> If you could just fix it, that’d be great.”
Apparently, he needed to start some fancy database, and, when it was asking for port,
he just… rolled his finger over 5, 4, 3, 2 on his keyboard!
(pretty much the same did the developers of PostgreSQL, I assume 😅)
That’s his computer and he is free to do whatever he wants, right?
You explain him the problem, and now it works on his machine too.
Problem solved? Nope!
There is no place like 127.0.0.1
… unless there is.
Let’s say you’ve learned how to ignore the nervous tick every time someone gets a port conflict, fine.
But something is still not working. And this something is… your CI environment.
Since everyone is uses Docker nowadays, the CI system is not an exception.
And, to run your tests, it will start a container.
Let’s simulate a similar environment. Here I am running everything On My Machine™:
$ docker run -d -p 80:80 nginx
cb74d81a4d2d72619f92bc003bcd44cc03ea1f523f41821bef0adf7954ceaa2e
$ wget -q -O- localhost:80 | grep title
<title>Welcome to nginx!</title>
As we can see, it works perfectly fine and I can hardcode localhost:80
.
But now, let’s simulate our CI system and start our “test” inside a container:
$ docker run -it --rm alpine
$ wget -q -O- localhost:80
wget: can't connect to remote host (127.0.0.1): Connection refused
Since we’re running inside a container, our localhost
is not host’s localhost
.
We can access the Docker host:
$ wget -q -O- 172.17.0.1:80 | grep title
<title>Welcome to nginx!</title>
But we cannot hardcode 172.17.0.1
since this depends on many factors.
So, as you can see, if you manage the fix the port, you won’t be able to guarantee that it will always be on localhost
.
This is why in Testcontainers we have Container#getContainerIpAddress()
:
https://www.testcontainers.org/features/networking/#getting-the-container-ip-address
It will auto-detect the host and return either localhost
, 172.17.0.1
-ish IP,
or something like 54.68.123.73
(don’t forget about the remote Docker daemons!).
There is no excuse for using fixed ports
So, as you can see, fixed ports are dangerous and only work in some environments but not all.
Always obtain the actual host and port and never hardcode anything, including the host.
Having troubles providing the actual values to your Java app? Most of the frameworks support configuration with System properties. Just set them before starting your service.
Having troubles debugging containers? You have two great options with Testcontainers:
- Reusable containers - start container once, reuse between the tests. Pretty similar to Docker Compose, but automated and controlled by your tests.
- “How to locally debug containers started by Testcontainers”.
Still want to use fixed ports? 😜