Course – LS – All

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

When we run a Docker container, it connects with a virtual network using an IP address. For this reason, we expect services to get a configuration dynamically. However, we might want to use a static IP instead of an automatic IP allocation.

In this tutorial, we'll see the difference between the built-in configuration and assigning a manual IP to a container. Finally, we'll add some Docker Compose examples with tests.

2. DHCP and DNS

Let's see the Docker built-in IPs assignment to containers using DHCP and DNS to resolve hosts' names.

2.1. How Docker Assigns an IP

Docker first assigns an IP to each container, acting as a DHCP server. Furthermore, there are multiple DNS servers.

Containers then process DNS requests with a server inside dockerd, which recognizes the names of other containers on the same internal network. This way, containers can communicate without knowing their internal IP addresses. Although each time the internal IP addresses might differ when the application starts, containers can still easily connect with a human-readable name thanks to the internal DNS server inside dockerd.

Then, dockerd sends name lookups to CoreDNS (from the CNCF). Finally, requests move to the host depending on the domain name.

There's a side case for the domain docker.internal. It includes the DNS name host.docker.internal that resolves to a valid IP address for the current host. It allows containers to contact those host services without worrying about hardcoding IP addresses. Although not recommended, it can be handy for development purposes.

2.2. Network Example

As an example, we can run a container for a MySQL service. Let's check out the Docker Compose YAML definition:

services:
  db:
    image: mysql:latest
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_ROOT_HOST=localhost
    ports:
      - 3306:3306
    volumes:
      - db:/var/lib/mysql
    networks:
      - network

volumes:
  db:
    driver: local

networks:
  network:
    driver: bridge

As usual, we run our container:

docker-compose up -d

Let's inspect the network from a container perspective with the format syntax using jq to get a JSON output:

docker inspect --format='{{json .NetworkSettings.Networks}}' 2d3f4c69a213 | jq .

Docker Compose assigns the network name based on the current directory. We can see a similar output if, for example, we are in the project directory:

{
  "project_network": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": [
      "project-db-1",
      "db",
      "2d3f4c69a213"
    ],
    "NetworkID": "39ffbd8155d11ba03d0b548307f549f06790fe045e121a6d862b070d4fb67fa7",
    "EndpointID": "0eba235239b06f7e0cb5065b7f2ebd83e7d227f8cfad4df8de73260472737500",
    "Gateway": "172.19.0.1",
    "IPAddress": "172.19.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:13:00:02",
    "DriverOpts": null
  }
}

The container gets a private 172.19.0.2 IP address from the subnet created by the network.

Most importantly, we can see info about IPAMConfig, which is the IP address management. It will be relevant when we'll statically assign the IP.

Now, we can inspect the network:

docker inspect network project_network

This time, we have a better insight into the network:

[
    {
        "Name": "project_network",
        "Id": "39ffbd8155d11ba03d0b548307f549f06790fe045e121a6d862b070d4fb67fa7",
        "Created": "2022-09-09T16:19:26.27396468+02:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.19.0.0/16",
                    "Gateway": "172.19.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "2d3f4c69a2139dea9089a6d42907fdc085282c5df176b39bf7c20f5d0780179d": {
                "Name": "project-db-1",
                "EndpointID": "7447fe2550afb3f980f36449673724e9ed6dd16f41a085cc20ada3074a0d8e54",
                "MacAddress": "02:42:ac:13:00:02",
                "IPv4Address": "172.19.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {
            "com.docker.compose.network": "network",
            "com.docker.compose.project": "project",
            "com.docker.compose.version": "2.10.2"
        }
    }
]

It's worth noticing the Docker Compose network has been available since version 2.

3. Static IP

Knowing a bit more about automatic IP assignment, we'll now create our subnet of a network. We can then assign to our service the IP we prefer.

3.1. Assign a Static IP

If we are using Docker CLI, we would achieve this result by first creating the subnet:

docker network create --subnet=10.5.0.0/16 mynet

And then, we run the container with a static IP, again with a MySQL service:

docker run --net mynet --ip 10.5.0.1 -p 3306:3306 --mount source=db,target=/var/lib/mysql -e MYSQL_ROOT_PASSWORD=password mysql:latest

We can wrap up with a complete example using Docker Compose:

services:
  db:
    container_name: mysql_db
    image: mysql:latest
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_ROOT_HOST=10.5.0.1
    ports:
      - 3306:3306
    volumes:
      - db:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      network:
        ipv4_address: 10.5.0.5

volumes:
  db:
    driver: local

networks:
  network:
    driver: bridge
    ipam:
      config:
        - subnet: 10.5.0.0/16
          gateway: 10.5.0.1

We have now defined our network‘s subnet by the ipam keyword and assigned an IPv4 address to the service. For a change, we used 10.5.0.5. 172.* and 10.* IP addresses are commonly in use for private networks. We can also use an IPv6 address, which has a 128-bit address length and will replace the IPv4 due to more efficiency.

As recommended, we assign the gateway address to the database host MYSQL_ROOT_HOST.

Finally, we add an SQL script to create a user, a database, and a table:

CREATE DATABASE IF NOT EXISTS test;
CREATE USER 'db_user'@'10.5.0.1' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON *.* TO 'db_user'@'10.5.0.1' WITH GRANT OPTION;
FLUSH PRIVILEGES;

use test;

CREATE TABLE IF NOT EXISTS TEST_TABLE (id int, name varchar(255));

INSERT INTO TEST_TABLE VALUES (1, 'TEST_1');
INSERT INTO TEST_TABLE VALUES (2, 'TEST_2');
INSERT INTO TEST_TABLE VALUES (3, 'TEST_3');

We want to give the user access to the database only at that specific address.

After the container starts, we can have a look at its definition with docker ps:

CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                                                  NAMES
97812e199512   mysql:latest   "docker-entrypoint.s…"   7 minutes ago   Up 7 minutes   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   mysql_db

We can now connect to the database by entering the password. We use the container name or ID as these resolve as an alias for DNS:

mysql --host=mysql_db -u db_user -p

Now, using the status command, we can test that our MySQL host resolves as the container ID:

Connection id:          10
Current database:       test
Current user:           [email protected]
SSL:                    Not in use
Current pager:          stdout
Using outfile:          ''
Using delimiter:        ;
Server:                 MySQL
Server version:         8.0.30 MySQL Community Server - GPL
Protocol version:       10
Connection:             97812e199512 via TCP/IP
Server characterset:    utf8mb4
Db     characterset:    utf8mb4
Client characterset:    utf8mb3
Conn.  characterset:    utf8mb3
TCP port:               3306

3.2. Difference With Built-in Docker IP Management

Let's inspect the container. In the case of a static IP, we can see that the IPAM configuration now has an IPv4 address:

{
  "project_network": {
    "IPAMConfig": {
      "IPv4Address": "10.5.0.5"
    },
    "Links": null,
    "Aliases": [
      "project_db",
      "db",
      "122c0c6bfcf9"
    ],
    "NetworkID": "7ac7a1d9e33dffc65bc867aee4db04b9b8fecaeb3bbb91c74c2f72e4611c6955",
    "EndpointID": "84145191a0327b777b6a31bacb2a0260d9a31e8c22cbfca1923775b3649b1d7e",
    "Gateway": "10.5.0.1",
    "IPAddress": "10.5.0.5",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:0a:05:00:05",
    "DriverOpts": null
  }
}

From a container perspective, that's the main difference.

If we need a static, private IP address, we should consider if we need to use one at all. Most of the time, we want a static IP to talk to one container from another or the host. Docker’s built-in networking can already handle this.

However, we might want to manually specify a private IP address, for example, for accessing containers directly from the host.

It's worth noticing the possibility of custom networking using Docker Swarm.

4. Conclusion

In this article, we've seen how Docker manages IP allocation and how to add a static address to a container. We've also seen examples of Docker Compose configuration running a MySQL service with or without a static IP.

As always, we can find working code examples over on GitHub.

Course – LS – All

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
1 Comment
Oldest
Newest
Inline Feedbacks
View all comments
Comments are closed on this article!