brandLogo

Tech Notes.

By Danish

Docker Networking

Before understanding we docker networking let's understand networking in general.

General Setup

Network : A collection of interconnected devices or servers or apps, where they are able to communicate with each other and share data.

When we run an application and we access it on localhost. Its because the application is running in our laptop within its private network.
So if you try to access localhost on some other device that will point to that system private network.
While running multiple applications on our system and they are able to communicate with each other is because they are all connected to the same network on our system.

If we use the IPv4 address we can access the apps in our local network. That is all the devices connected with the same router.

Check out this example how multiple endpoints are communicating with each other.

  • run the index.js file with node index.js
  • go to http://localhost:3003/fetchKey
  • result would be

    Result of Service B data fetch : Encrypted_Service_Key_In_AppOne

  • The point to be noticed is that ServiceB is able to connect with ServiceA and fetch data (because they are in the same network)

Let's test the same in docker

Use the dockerfile in the app to create images and container.

  • In ubuntu terminal, navigate to /mnt/<path to docker file location>
  • Use docker build command to create the image
  • I'm using docker build -t multi-port-app-test:v1.0 .
  • Now let's create the image with docker run --name multi-port-app -p 8080:3003 multi-port-app-test:v1.0
  • We have logs to ensure services are up
  • Let's try to access our endpoint at http://localhost:8080/fetchKey
  • Logs will reflect the key being returned and we'll also get the same result

    Result of Service B data fetch : Encrypted_Service_Key_In_ServiceA

Services can communicate because they are in the same network stack
  • Hit ctrl+c to exit the container, wait few seconds and we are out.
  • Let's create another container from the same image but map it to different host port
  • docker run --name multi-port-app-2 -p 8181:3003 multi-port-app-test:v1.0
  • Logs again reflect the app to be running on the port 3003
  • Let's check the result http://localhost:8181/fetchKey
  • We get the same result, we are fine

So far so good

Now let's get back to our single app and try running it with node index.js. It starts to run on port 3000 and logs reflect the same.
Let's open a new terminal and run the app again. But this time we get an error

info - 2024-10-16 09:36:10.523 - Failed to start server !! Error : {"code":"EADDRINUSE","errno":-4091,"syscall":"listen","address":"::","port":3000}

Our app could not run because the port 3000 was already in use. Well that was expected.
But how come the second container above was able to run on the same port 3003 and both services are still accessible. Both 8080 and 8181 address get's us the result.

Let's sit with this confusion for some time.

Now let's use the same approach but with a minor tweak and split the services in two different project and each service gets it own dockerfile so they can have a container of their own.

Checkout the code here

Let dive in with our ubuntu terminal and create images and container

  • navigate to app-one and create image: docker build -t multi-network-app:app-one .
  • navigate to app-two and create image: docker build -t multi-network-app:app-two .
  • create container for app-one and expose it on 8080 in detached mode: docker run -d --name app-one -p 8080:3002 multi-network-app:app-one
  • create container for app-two and expose it on 8181: docker run --name app-two -p 8181:3003 multi-network-app:app-two
  • as per logs our app has started
    info - 2024-10-17 03:44:13.920 - registering serviceA routes
    info - 2024-10-17 03:44:13.922 - greet route registered !!
    info - 2024-10-17 03:44:13.923 - serviceKey route registered !!
    info - 2024-10-17 03:44:13.926 - App is running on http://*:3003
    
  • Let's access http://localhost:8181/fetchKey to check our app
  • We get response as We got an error : ECONNREFUSED, which means the connection could not be established to the server
  • But app-one is running already
  • Check http://localhost:8080/serviceKey to ensure app is running fine

Remember what i said earlier ?? Services can communicate because they are in the same network

As long as they were in the same container, they were able to communicate but as soon as they entered different containers, they can no longer communicate.
This is because they are in different networks now.

Isolation of containers

Each docker container is completely isolated and has its own separate network namespace. So each container gets its own network and port numbers.

Referring back to our confusion the reason we were able to run our app on same port because they were in different network namespace. Each container has its own set of ports.

Docker Bridges

Docker bridges are like virtual LAN. When containers are connected with a bridge, they can communicate with each other, just like devices in physical LAN.
Docker by default creates a bridge called 'bridge'. To check the list of all available bridges, we can run docker network ls .

Default networks

NETWORK ID     NAME      DRIVER    SCOPE
ee69a25fedbb   bridge    bridge    local
24145443ea38   host      host      local
de96e11186d1   none      null      local

By default, all the containers are connected with default bridge 'bridge' unless specified otherwise.

We can check more details about the bridge including the attached containers with docker network inspect <network_name>. Let's check the default bridge and we should have our containers attached to it docker inspect bridge.

[
    {
        "Name": "bridge",
        "Id": "ee69a25fedbb0fd795cff17a4ca7f46a0f7aa9e1f087ca9e8fc84275238be3fe",
        "Created": "2024-10-17T06:59:10.624506946+05:30",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
	                // I have masked these for security reasons
                    "Subnet": "***.**.*.*/**",
                    "Gateway": "***.**.*.*"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "c36452a8e3e29efccccb7ca202bf51e9fec27bcbbecd331d997e3628a8501a26": {
                "Name": "app-one",
                "EndpointID": "6ffbbddc447827927816c33cea5f35332911bdf74671dea3012897488e944b99",
                // I have masked these for security reasons
                "MacAddress": "**:**:**:**:**:**",
                "IPv4Address": "***.**.*.*/**",
                "IPv6Address": ""
            },
            "f02bf8dd6cca455ae26a84fccd009fd88c6e0e9b74393c6ed2ff8c846c7c8a1c": {
                "Name": "app-two",
                "EndpointID": "6a965e4dcecf1465a09f299b0fdb66f5793bb976b302638c16e2eee5779f6203",
                // I have masked these for security reasons
                "MacAddress": "**:**:**:**:**:**",
                "IPv4Address": "***.**.*.*/**",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

Only the containers currently running are attached to networks i.e containers that are displayed with docker ps. To check this, start other containers with docker start <container_name/id>, and run the command again to check the updated list

Default and User defined bridges

Default bridge

Containers linked to default bridge:

  • can't communicate with others through container names directly
  • Can communicate only with IP addresses, and need to be setup two way with --link legacy flag.
  • Only share environment variables

User defined bridges

  • Gets DNS resolver of its own, so containers can communicate through container name
  • Containers can be attached and detached from user-defined networks on the fly
  • Provides better isolation as containers has to be explicitly added into the network
  • Can not share environment variables implicitly, need to share via mounted shared directory or volumes

Now let's put ou understanding to test

Let's create a custom network and see if what we learned is actually correct. To create a new network we do docker network create <network_name>. I want to name my network as 'custom-network'. So, I'll run docker network create custom-network.
Mow let's check the list of networks with docker network ls

NETWORK ID     NAME             DRIVER    SCOPE
ee69a25fedbb   bridge           bridge    local
c92d6eec959a   custom-network   bridge    local
24145443ea38   host             host      local
de96e11186d1   none             null      local

Cool !! so now we have our own network.

Let's create new apps to setup on our new network. Its just our multi-network-app node with minor refactoring and updates to make more sense in the example we are going to use.

Checkout this repo : https://github.com/dev-danish-javed/technotes-demo-code/tree/main/docker-demo/node-code/bridge-app

Our notification processor needs to pick keys from vault. So it needs to connect to the other ms.

Let's set our system to communicate with each other.

  • Create images and related containers
    • Navigate to the vault-ms in ubuntu terminal
    • docker build -t vault-ms:v1.0 .
    • run the container docker run --name vault-ms vault-ms:v1.0, notice that we have not mapped ports
    • navigate to security-processor
    • build image by passing the details, url docker build --build-arg vault_ms_url="http://vault-ms:3002" -t security-processor:v1.0 .
    • run the container docker run -p 8282:3003 --name security-processor security-processor:v1.0

Now let's try to access our app on http://localhost:8282/fetchKey The output is We got an error : EAI_AGAIN which is "DNS lookup timed out error". So basically the DNS vault-ms could not be resolved. But our both our apps are running.

Add containers to network

Let's add them to the same network and try again. The command to do that is :
docker network connect <network_name> <container_name_or_id>.

I'll do docker network connect custom-network vault-ms and docker network connect custom-network security-processor. There will be no output for the commands as we don't have errors. If there were errors they would have been displayed.

Let's check the network to make sure they are both connected to our network.
Run docker network inspect custom-network. There we'll have both containers listed in container section.

Now let's hit the endpoint http://localhost:8282/fetchKey again and see if that works and voila !!!
We get the desired response Result of ServiceTwo data fetch : Encrypted_Service_Key_From_Vault

Let's remove it from the network with docker network disconnect custom-network security-processor Now endpoint get us the error We got an error : EAI_AGAIN

Let's connect and try again and we get desired result again 🥳🥳.

Start container in network from the beginning

Instead of creating containers and then adding them to network we can also provide the network name when starting the container. Let's create another container directly with network docker run --network custom-network -p 8383:3003 --name security-processor-2 security-processor:v1.0

Emphasis on --network custom-network, again we get the desired result on 8383 as well.

Ain't this so cool 🤩🤩 ??
Let's get started with docker volumes in next section.