Each of your services is given a unique private IP address and they all run on the same network. The port numbers are the same as configured in the Docker image.
The recommended way to help services communicate with each other internally is either through DNS (it's always updated live), or using an API.
We use Consul for service discovery and each service is registered there. To access another service, you can query consul DNS names using servicename.service.consul. You can also use the consul HTTP API on consul.service.consul:8500 (only available from inside the infrastructure, from your application for example).
For example, if your service is named "myproduct/backend", other services will be able to access it using myproduct-backend.service.consul : this record has a TTL set to 0 and is always up to date with the number of instances of the service.
Here's a record example for an internal service named "myproduct/backend" that has 5 instances (hence 5 IP addresses):
$ dig +short myproduct-backend.service.consul
Any other service wanting to access the backend service will do it through the service discovery DNS name myproduct-backend.service.consul, and one of the instances will answer, load balancing results through DNS.
Another simple solution to share information between services is through project-level Environment Variables.
Let's say 2 services need to access the same information, like another service location. We could add a project-level environment variable named BACKEND_SERVICE_ADDR with the predictable value of "backend.service.consul".