1. Overview
When we run a Docker container, it connects with a virtual network using an IP address. Thus, we usually expect services to get a configuration dynamically. However, we might want to use a static IP instead of an automatic IP allocation.
Still, if we think we need a static, private IP address, we should consider that need in the first place. 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. It’s worth considering the possibility of custom networking using Docker Swarm. However, we might want to manually specify a private IP address, for example, for accessing containers directly from the host.
In this tutorial, we’ll see the difference between the built-in configuration and assigning a manual static IP to a container. Finally, we’ll add some Docker Compose examples with tests.
2. Docker DHCP and DNS
To begin with, let’s explore the built-in Docker IP assignment to containers using DHCP and DNS to resolve host names.
Docker first assigns an IP to each container, acting as a DHCP server.
Containers then process DNS requests through 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.
Further, 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 docker.internal domain. It includes the DNS name host.docker.internal that resolves to a valid IP address for the current host. It enables containers to contact those host services without worrying about hardcoding IP addresses. Although not recommended, docker.internal can be handy for development purposes.
3. Docker Network Example
As an example, we can run a container for a MySQL service. Let’s check out the Docker Compose YAML definition:
$ cat docker-compose.yml
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
...
✔ Container compo-db-1 Started
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}}' compo-db-1 | 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, i.e., IP address management. This is relevant when we statically assign the IP address.
Now, we can inspect the network itself:
$ docker inspect 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 noting the Docker Compose network has been available since version 2.
4. Docker Static IP
Armed with a bit more knowledge about automatic IP assignment, we can now create a specific network. After that, we assign the IP address of preference to the containers.
4.1. Assign a Static IP via Docker Directly
When using Docker CLI, we first create a network:
$ docker network create --subnet=10.5.0.0/16 custom_net
At this point, we have the custom_net network with a subnet of 10.5.0.0/16.
Then, we can run a container with a static IP:
$ docker run --detach --net custom_net --ip 10.5.0.5 -p 3306:3306 --mount source=db,target=/var/lib/mysql -e MYSQL_ROOT_PASSWORD=password mysql:latest
Notably, we supply the custom_net network name to the relevant –net option along with the static IP as 10.5.0.5 after –ip.
4.2. Assign a Static IP via Docker Compose
Of course, we can also do the above through Docker Compose:
$ cat docker-compose.yml
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:
custom_net:
ipv4_address: 10.5.0.5
volumes:
db:
driver: local
networks:
custom_net:
driver: bridge
ipam:
config:
- subnet: 10.5.0.0/16
gateway: 10.5.0.1
Thus, we define the network subnet under the ipam keyword and assign an IPv4 address to the service via ipv4_address within the service definition. For coherency, we use the same 10.5.0.5 IP address. Commonly, 172.* and 10.* IP addresses are ones chosen for Docker private networks. Of course, we can also use an IPv6 address, which has a 128-bit address length and gradually replaces IPv4 due to more efficiency.
As recommended, we assign the 10.5.0.1 host and gateway address to MYSQL_ROOT_HOST to allow direct connections.
4.3. Verification
Finally, we can run 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 verify the 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
So, our setup looks good.
4.4. Container Inspection
Let’s inspect the container:
$ docker inspect mysql_db
In the case of a static IP, we can see that the IPAM configuration now has an IPv4 address:
{
"project_custom_net": {
"IPAMConfig": {
"IPv4Address": "10.5.0.5"
},
"Links": null,
"Aliases": [
"mysql_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, this IPv4Address field is the main difference. However, we can also see that the network name we specified in the Docker Compose file has been prefixed with the directory name: project_custom_net.
5. 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.
The example can be found over on GitHub.