How do Docker containers communicate with each other?

Containers

You’ve gone through the quickstart and you’ve created your first Docker container. But now you’re struggling with how to run more than one container at the same time. If Docker containers are isolated, how do they communicate with each other? And how do you run multiple containers together in production?

In reality, running just one container isn’t quite enough for most applications. A modern application typically consists of a few components, like a database, a web server, some microservices, and maybe even more.

So if you want to run all of your components in containers, how do the applications talk to each other? How do containers communicate with each other, if they’re supposed to be isolated?

In this article we’ll look at communication between Docker containers on the same host (single-host networking).

The ways that containers can communicate

First let’s have a look at the main ways that Docker containers can communicate with each other:

  • Networking: Containers are designed to be isolated. But they can send and receive requests to other applications, using networking.

    For example, a web server container might be opened up so that it can receive requests, on port 80. Or an application container might make connect to a database container.

  • Files: Some applications communicate by reading and writing files. These kinds of applications can communicate by writing their files into a volume, which can also be shared with other containers.

    For example, a data processing application might write a file to a shared volume which contains customer data, which is then read by another application.

For the rest of this article, we’ll focus on applications that use networking as the means of communication. Specifically we’ll talk about how to set up a network, which allows Docker containers on the same host to communicate with other.

Networking with containers

A lot of container-based applications communicate with each other using networking. This means that an application running in one container will use a network connection to talk to an application running in another container. For example, an application might call a REST or GraphQL API, or make a call to a database.

Containers are suited to applications or processes which expose some sort of network service. The most common examples of applications that expose network services are:

  • Web servers - e.g. Nginx, Apache

  • Databases and data stores - e.g. MongoDB, PostgreSQL

  • Applications and application servers - e.g. JBoss, Wildfly, Spring Boot

There are more examples, but these are probably the most common types.

Exposing and publishing ports

To make a process such as a web server or database available outside a container, an label on the container image tells Docker that it exposes a port. This is done by using the EXPOSE directive when writing a Dockerfile.

Exposing a port doesn’t actually make the port available, but is more like applying a label or piece of metadata to the image, so that people who use the image know what it does.

When running an application in a container, it’s helpful to know which ports are exposed on the container. This helps you to publish the ports, which the term which describes making the ports available outside the container.

If you don’t publish any ports, your containerised application won’t be able to receive any requests.

How to find out which ports are exposed by an image

A Docker image contains information about the ports that should be exposed. Exposed ports are like little pieces of documentation about the image: they tell you something about what’s inside. For example, a web server image might choose to expose port 80.

If you want to see a list of exposed ports in a Docker image, you can inspect the image using docker image inspect. For example:

docker image inspect nginx:latest

In the output, look for the section “ExposedPorts”. This is the output from inspecting the nginx Docker image, and it shows that the image is configured to expose port 80:

[
    {
        "Id": "f35646e83998b844c3f067e5a.....",
        ...
        "Config": {
          "ExposedPorts": {
              "80/tcp": {}
          },

You can also get this information more cleanly from docker inspect by using the --format option. This will show you just the exposed ports information:

docker inspect --format='{{.Config.ExposedPorts}}' nginx:latest

Note: This only shows you which exposed ports are labelled in the image. To actually make these ports available, you need to publish the ports when you run a container, which we’ll look at next.

How to communicate with a container from your host machine

To access a container from your host machine, you need to publish the ports on the container.

Publishing a port makes it available on your localhost address. This allows you to send requests to the container by addressing localhost + the published port number. For example, if you were running a web server in a container, publishing a port would allow you to make a request to the web server from your host machine.

Illustration of a user making a request to localhost, and the request being forwarded on to an nginx container
Publishing a port in docker allows you to access it from localhost

When you publish ports on a container, you can choose to either publish all exposed ports on the container, or publish specific ports:

  • To publish all exposed ports, run the container with the -P switch. Your Docker image must specify some exposed ports, so that Docker knows which ports it should publish.

    This command will run the nginx image and publish all exposed ports:

    docker run -P nginx:latest
    

    Docker publishes each exposed port to a random port number, to make sure there are no port conflicts. If you want to find out the port of your nginx container, you could use the docker port command, with your container’s ID. The nginx image exposes one port (80), so there’s one line in the output of this command:

    $ docker port <container_id>
    80/tcp -> 0.0.0.0:34659
    

    I could now access my web server using http://localhost:34659 because it forwards on to port 80 in the container.

  • To publish specific ports you can use the -p switch. This allows you to expose any port you like:

    docker run -p 8080:80 nginx:latest
    

    This exposes port 80 inside the container, and makes it accessible at localhost port 8080.

Example: to start an nginx web server container and publish port 8080, which is the default:

docker run -p 8080:80 nginx

Publishing ports is very useful to be able to access ports on your container from your host.

But how do containers communicate with each other? In the next section we’ll look at how to create a network to allow containers to address each other.

How does a container make a request to another container

When you are running multiple containers, they can communicate with each other by both attaching to the same network. Each container is assigned an IP address on the network, and optionally a hostname.

Docker provides a few different ways to network between containers. We’ll cover two of the easiest options:

  • The default Bridge network which allows simple container-to-container communication by IP address, and is set up by default.

  • A user-defined Bridge network, which you create yourself, and enables containers to communicate with each other by using their container name as a hostname.

The default Bridge network: container-to-container communication by IP address

The simplest network in Docker is the bridge network, which is also Docker’s default networking driver. A bridge network allows simple communication between containers on the same host.

Two people sharing a parcel across a bridge
A bridge network allows containers to communicate with each other

When Docker starts up, it creates a default bridge network called bridge. The bridge network starts automatically without any configuration required. Thereafter, all newly-started containers are automatically added into to the bridge network, unless you specify otherwise. In the network, each container is assigned its own IP address. This allows containers to communicate with each other by their IP address.

Bridge networks are a good choice for non-production situations, when you are running several applications in standalone containers which need to communicate with each other. It’s best suited to non-production environments, because containers can only address each other by their IP address. This makes things brittle, because the containers’ IP addresses might change.

How to set up single host networking with the default bridge network

To use a single-host networking setup, to allow two Docker containers on the same host to communicate with each other:

  1. Check that the bridge network is running: You can check this by typing docker network ls, which will show the bridge network in the list.

  2. Start containers and publish their ports: When you start a container, tell Docker to publish ports on your containers by using the -p argument with docker run.

    When you start a container, it will be automatically connected to the bridge network, or you can specify this manually by adding --net=bridge to the docker run command.

  3. Address another container by its IP address: When two containers are joined to the same bridge network, one container is able to address another by using its IP address. To find out a container’s IP address, see the section below.

How do you find out the IP address of a Docker container?

To find the IP addresses of a container, look at the output from the docker inspect command, and find the IPAddress field:

docker inspect <container_id> | grep IPAddress

This will show you the IP address of the Docker container.

A nametag bearing the name 'Database'
It’s better to address containers by name instead of IP address

The user-defined bridge network: container-to-container communication by hostname

If you only use the default bridge network, then containers can only address each other by their IP address. Obviously, this isn’t practical for running containers in production, because the IP addresses might frequently change.

So how do you reach Docker containers by a meaningful hostname, instead of by IP address?

To allow Docker containers to communicate with each other by name, you can create a user-defined bridge network. In a user-defined bridge network, containers can be addressed by their name or alias.

Docker gives each container a unique name, but you can choose your own name or alias for a Docker container when it starts. This gives the container a fixed name (e.g. my-nginx, or myapp-dev), which makes it easier to run multiple containers that communicate with each other.

You might run several containers on a single server in production. If you give each container a fixed name, then they can address each other by this name, and you will not have to worry about keeping track of containers’ IP addresses, which can frequently change.

Example: if you run a database in a container, and give it the name mydatabase, then your app in a container can address the database using the hostname mydatabase. If you’re using MongoDB, the connection string might look like this: mongodb://mydatabase:27017

How to assign a name to a Docker container

Containers are normally assigned a random name, like beefy_tea. But you can give a container a fixed name, using the --name parameter when calling docker run.

For example, this command will create and start a mongodb container, and give it the name my-database:

docker run --name my-database mongodb:latest

When creating a container with an explicit name, be aware that the container still hang around, even when the container stops. This means that if you try to recreate the same container with the same name, you will get an error, because a container already exists with the given name. The most common solution to this is to add --rm to docker run, which tells Docker to remove the container when it stops.

How to create a user-defined bridge network

To allow two Docker containers on the same host to communicate with each other by name:

  1. Create a user-defined bridge network: Create your own custom bridge network first using docker network create. Under the hood, Docker configures your underlying system to set up the relevant networking tables. For example:

    docker network create my-network
    
  2. Start a container and connect it to the bridge: Start your container as normal, and connect it to your user-defined bridge network using --net, e.g. --net=my-network.

    The container will start and be connected to your user-defined bridge network.

  3. Address another container, by using its name as the hostname: When two containers are joined to the same user-defined bridge network, one container is able to address another by using its name (as the hostname).

How to connect an existing container to a user-defined bridge network

If you already have a container running, you might want to connect it to your new user-defined bridge network without having to restart it. To do this, you can use the docker network connect command. For example:

docker network connect my-network mongodb

The command above will connect the container named mongodb to the network my-network.

A real world example

Let’s see how to put this together in the real world. A very common scenario is to have a application running in a Docker container, which depends on a database, which is also running in a Docker container. So we have two containers:

  • Our application container - could be NodeJS, Java, or any language you like.

  • Our database container - could be Mongo, MySQL, or perhaps even Oracle.

Illustration of app and mongo database containers connected to a user-defined network, with the app port published
Running an application and a database in Docker, and accessing the app

If we want to run both of these containers, so that the application can talk to the database, we need to make sure that the containers can see each other.

We’ll create a user-defined bridge network and add the two containers to it. We’ll start the containers, attaching them to the user-defined bridge network, and tell the application how to address the database, by setting an environment variable:

  1. Create the user-defined bridge network:

    docker network create my-custom-network
    
  2. Start a mongodb container in the background (detached) and attach it to my-custom-network:

    docker run --detach --name fun-mongo \
        --net my-custom-network mongo:latest
    
  3. Start the application. In this case, we’re using “Mongo Express”, which is a web front-end to Mongo, to allow you to browse the datastore. Run the container. We call it mongo-frontend, join it to the same network, and publish its port so we can access it from localhost. We also use an environment variable to configure Mongo Express, which provides the hostname of the database, which is the container name, fun-mongo:

    docker run --detach --name mongo-frontend -P \
        --net my-custom-network \
        -e ME_CONFIG_MONGODB_SERVER=fun-mongo \
        mongo-express:latest
    
  4. Now use docker port to get the random published port of the frontend application:

    docker port mongo-frontend
    # Returns something like:
    # 8081/tcp -> 0.0.0.0:44955
    
  5. We can now access the web application from our desktop using the random port, which in our case is 44955. The application is talking to the Mongo database running in a container:

    Screenshot of the Mongo Express app accessed from localhost on a random port
    Accessing the front end app using the random published port