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.