도커로 프로젝트를 진행하다보면 컨테이너끼리 통신이 필요하거나 컨테이너와 호스트 머신의 통신이 필요하게 된다. 이번 글에서는 이러한 문제를 해결할 수 있는 도커 네트워크에 대해 정리해보려한다.
컨테이너와 컨테이너를 연결해보는 테스트를 위해 node.js 서버를 도커 컨테이너로 띄우고 레디스 이미지를 받아 도커 컨테이너를 만든 후 이 두 컨테이너를 연결해볼 것이다.
컨테이너와 컨테이너를 연결하는 방법에는 2가지 방법이 있다.
첫번째는 컨테이너의 아이피 주소를 사용해 연결하는 방식이고, 두번째는 도커 네트워크를 이용하는 방법이다.
첫번째 방법은 가능은 하지만 실제로 사용을 하지는 않는 방식이다. 먼저 첫번째 방식을 적용해본 후 이 방식을 왜 사용하지 않는지 살펴보자.
먼저 redis 이미지를 통해 컨테이너를 만들어준다.
dong@ubuntu:~$ docker run --name redis-container -d redis
f1be3ac60962e647a9a7ca13e44d715f4e93613a0dec77253a4b2bf85db1a855
그 다음 Dockerfile을 이용해 노드 서버를 띄운다.
FROM node
WORKDIR /app
COPY package.json /app
RUN npm install
COPY . /app
EXPOSE 80
CMD ["node", "server.js"]
{
"name": "docker-network-test",
"version": "1.0.0",
"description": "",
"main": "server.js",
"author": "dong",
"license": "MIT",
"dependencies": {
"express": "^4.17.3",
"body-parser": "^1.19.1",
"redis": "^4.0.4"
}
}
dong@ubuntu:~/docker-complete/DOCKER-NETWORK$ docker build . -t network-test-server
dong@ubuntu:~/docker-complete/DOCKER-NETWORK$ docker run -d -p 5000:80 -v "$(pwd)"/server.js:/app/server.js network-test-server
29b427768a4005e5055f8423184944153ac27593db5f7310e42ea79523df8e41
server.js 파일은 테스트를 하며 수정하기 위해 bindmount시켰다.
docker container inspect {컨테이너명}
을 사용하면 컨테이너에 대한 정보들을 확인할 수 있다.
dong@ubuntu:~/docker-complete/DOCKER-NETWORK$ docker container inspect redis-container
[
{
"Id": "f1be3ac60962e647a9a7ca13e44d715f4e93613a0dec77253a4b2bf85db1a855",
"Created": "2022-03-13T17:20:01.08298854Z",
"Path": "docker-entrypoint.sh",
"Args": [
"redis-server"
],
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 9537,
"ExitCode": 0,
"Error": "",
"StartedAt": "2022-03-13T18:20:33.386639484Z",
"FinishedAt": "2022-03-13T11:06:01.446890949-07:00"
},
"Image": "sha256:0e403e3816e890f6edc35de653396a5f379084e5ee6673a7608def32caec6c90",
"ResolvConfPath": "/var/lib/docker/containers/f1be3ac60962e647a9a7ca13e44d715f4e93613a0dec77253a4b2bf85db1a855/resolv.conf",
"HostnamePath": "/var/lib/docker/containers/f1be3ac60962e647a9a7ca13e44d715f4e93613a0dec77253a4b2bf85db1a855/hostname",
"HostsPath": "/var/lib/docker/containers/f1be3ac60962e647a9a7ca13e44d715f4e93613a0dec77253a4b2bf85db1a855/hosts",
"LogPath": "/var/lib/docker/containers/f1be3ac60962e647a9a7ca13e44d715f4e93613a0dec77253a4b2bf85db1a855/f1be3ac60962e647a9a7ca13e44d715f4e93613a0dec77253a4b2bf85db1a855-json.log",
"Name": "/redis-container",
"RestartCount": 0,
"Driver": "overlay2",
"Platform": "linux",
"MountLabel": "",
"ProcessLabel": "",
"AppArmorProfile": "docker-default",
"ExecIDs": null,
"HostConfig": {
"Binds": null,
"ContainerIDFile": "",
"LogConfig": {
"Type": "json-file",
"Config": {}
},
"NetworkMode": "default",
"PortBindings": {},
"RestartPolicy": {
"Name": "no",
"MaximumRetryCount": 0
},
"AutoRemove": false,
"VolumeDriver": "",
"VolumesFrom": null,
"CapAdd": null,
"CapDrop": null,
"CgroupnsMode": "host",
"Dns": [],
"DnsOptions": [],
"DnsSearch": [],
"ExtraHosts": null,
"GroupAdd": null,
"IpcMode": "private",
"Cgroup": "",
"Links": null,
"OomScoreAdj": 0,
"PidMode": "",
"Privileged": false,
"PublishAllPorts": false,
"ReadonlyRootfs": false,
"SecurityOpt": null,
"UTSMode": "",
"UsernsMode": "",
"ShmSize": 67108864,
"Runtime": "runc",
"ConsoleSize": [
0,
0
],
"Isolation": "",
"CpuShares": 0,
"Memory": 0,
"NanoCpus": 0,
"CgroupParent": "",
"BlkioWeight": 0,
"BlkioWeightDevice": [],
"BlkioDeviceReadBps": null,
"BlkioDeviceWriteBps": null,
"BlkioDeviceReadIOps": null,
"BlkioDeviceWriteIOps": null,
"CpuPeriod": 0,
"CpuQuota": 0,
"CpuRealtimePeriod": 0,
"CpuRealtimeRuntime": 0,
"CpusetCpus": "",
"CpusetMems": "",
"Devices": [],
"DeviceCgroupRules": null,
"DeviceRequests": null,
"KernelMemory": 0,
"KernelMemoryTCP": 0,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": null,
"OomKillDisable": false,
"PidsLimit": null,
"Ulimits": null,
"CpuCount": 0,
"CpuPercent": 0,
"IOMaximumIOps": 0,
"IOMaximumBandwidth": 0,
"MaskedPaths": [
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware"
],
"ReadonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
},
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/b2cb7c79ecd4732d45cc99330eb307be6aadc676b3172f362f0acc0d5570b7b4-init/diff:/var/lib/docker/overlay2/422fbddccc1421ff26e9493181e3a35610dbc297cf18a0f0a43d89380bc6ab04/diff:/var/lib/docker/overlay2/3197b0054d7f0fc61cd151104c844b76429a0799a711889f963e257a6c7ef393/diff:/var/lib/docker/overlay2/c2704b37ba2a2ccca54524c124d4c0691de9375c296b8612cc1458793cf68078/diff:/var/lib/docker/overlay2/90fc266e8ed171e5652db0798cccd23cde4ae351c36989c213724b544fcd3418/diff:/var/lib/docker/overlay2/2716ec58affdb467db15467e76a3b251d7571760c66cc1221cfbda10760512b1/diff:/var/lib/docker/overlay2/ce282d510bf3679c3f7453d85226c8cdeebc815562a5345701323cf5dedf0d55/diff",
"MergedDir": "/var/lib/docker/overlay2/b2cb7c79ecd4732d45cc99330eb307be6aadc676b3172f362f0acc0d5570b7b4/merged",
"UpperDir": "/var/lib/docker/overlay2/b2cb7c79ecd4732d45cc99330eb307be6aadc676b3172f362f0acc0d5570b7b4/diff",
"WorkDir": "/var/lib/docker/overlay2/b2cb7c79ecd4732d45cc99330eb307be6aadc676b3172f362f0acc0d5570b7b4/work"
},
"Name": "overlay2"
},
"Mounts": [
{
"Type": "volume",
"Name": "16a5a7e47d184c9d712a49704810cb4a0a9d2f745c49a070df353b07d86cfe08",
"Source": "/var/lib/docker/volumes/16a5a7e47d184c9d712a49704810cb4a0a9d2f745c49a070df353b07d86cfe08/_data",
"Destination": "/data",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
"Config": {
"Hostname": "f1be3ac60962",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"6379/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"GOSU_VERSION=1.14",
"REDIS_VERSION=6.2.6",
"REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-6.2.6.tar.gz",
"REDIS_DOWNLOAD_SHA=5b2b8b7a50111ef395bf1c1d5be11e6e167ac018125055daa8b5c2317ae131ab"
],
"Cmd": [
"redis-server"
],
"Image": "redis",
"Volumes": {
"/data": {}
},
"WorkingDir": "/data",
"Entrypoint": [
"docker-entrypoint.sh"
],
"OnBuild": null,
"Labels": {}
},
"NetworkSettings": {
"Bridge": "",
"SandboxID": "c8f56c09c89f3b3be084a9ade541b941d5c2a1b1ccb8972e378b98763898cbda",
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {
"6379/tcp": null
},
"SandboxKey": "/var/run/docker/netns/c8f56c09c89f",
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "a6f8dafde78b83c5fd53ab128eb22b7ee71df106dc84573254cb95e9224e504f",
"Gateway": "172.17.0.1",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "172.17.0.3",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"MacAddress": "02:42:ac:11:00:03",
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "d0cee92cbfa0b81b614c3107757739bb2e1d6007f22e4e68c6e7274b8230b593",
"EndpointID": "a6f8dafde78b83c5fd53ab128eb22b7ee71df106dc84573254cb95e9224e504f",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.3",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:03",
"DriverOpts": null
}
}
}
}
]
이중 IPAddress를 확인할 수 있는데 이 IPAdress를 사용하면 컨테이너끼리의 통신이 가능하다.
아래 server.js의 코드를 통해 redis를 inspect해서 본 IPAdress인 172.17.0.3
로 연결한 것을 볼 수 있다.
const express = require('express');
const bodyParser = require('body-parser');
const redis = require('redis');
const app = express();
const client = redis.createClient({
url: 'redis://172.17.0.3:6379'
});
client.connect();
client.on('connect', () => console.log('Redis Client Connected'));
client.on('error', (err) => console.log('Redis Client Connection Error', err));
let userGoal = 'Enter Your Goal';
app.use(
bodyParser.urlencoded({
extended: false,
})
);
app.use(express.static('public'));
app.get('/', (req, res) => {
res.send(`
<html>
<head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<section>
<h2>My Course Goal</h2>
<h3>${userGoal}</h3>
</section>
<form action="/store-goal" method="POST">
<div class="form-control">
<label>Course Goal</label>
<input type="text" name="goal">
</div>
<button>Set Course Goal</button>
</form>
</body>
</html>
`);
});
app.post('/store-goal', async (req, res) => {
const enteredGoal = req.body.goal;
await client.set("userGoal", enteredGoal);
userGoal = await client.get("userGoal");
res.redirect('/');
});
app.listen(80);
레디스로부터 제대로 값을 받아온 것을 확인할 수 있다.
위의 방법은 한가지 큰 문제점이 있는데 이는 만약 레디스 컨테이너가 종료되었다가 다시 시작하면 아이피 주소가 바뀔 가능성이 있다는 점이다. 만약 이런 이유로 아이피 주소가 바뀌면 다시 레디스 컨테이너를 inspect한 후 아이피를 확인하고 node.js 코드를 수정해야 하는데 이는 절대 우리가 원하는 방식이 아닐 것이다.
따라서 우리가 실제로 사용할 방법은 도커 네트워크를 이용하는 방식이다.
먼저 docker network create
명령어를 사용해 도커 네트워크를 생성할 수 있다.
dong@ubuntu:~/docker-complete/DOCKER-NETWORK$ docker network create test
953b4b4c3cf864443e7b20b89979356e6618e6e3dab4649bf8043677ba777be8
생성된 네트워크는 docker network ls
명령어를 사용하면 확인할 수 있다.
dong@ubuntu:~/docker-complete/DOCKER-NETWORK$ docker network ls
NETWORK ID NAME DRIVER SCOPE
d0cee92cbfa0 bridge bridge local
139bd915554d host host local
f23aec4c0496 none null local
953b4b4c3cf8 test bridge local
test
네트워크가 birdge 디폴트 드라이버인 bridege로 생성되었는데 driver에 대해서는 뒤에서 다시 정리하겠다.
docker network connect
커맨드를 사용하면 네트워크에 컨테이너를 연결할 수 있다.
# test 네트워크에 tender_murdock 컨테이너를 추가
dong@ubuntu:~/docker-complete/DOCKER-NETWORK$ docker network connect test tender_murdock
docker network inspect
명령어를 사용하면 네트워크에 컨테이너가 추가된 것을 확인할 수 있다.
dong@ubuntu:~/docker-complete/DOCKER-NETWORK$ docker network inspect test
[
{
"Name": "test",
"Id": "953b4b4c3cf864443e7b20b89979356e6618e6e3dab4649bf8043677ba777be8",
"Created": "2022-03-13T12:17:26.854970604-07:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"7c23de714d1949464b78d3a9e828095af1e5f5fc003c70490fbf2f63dc4f95c2": {
"Name": "tender_murdock",
"EndpointID": "4bfd68b665c36d61e11af4171b27442f65149cb31ac24bdb7a44b00d4c808c17",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]
redis가 실행중인 redis-container 또한 test네트워크에 추가시켜준다.
dong@ubuntu:~/docker-complete/DOCKER-NETWORK$ docker network connect test redis-container
dong@ubuntu:~/docker-complete/DOCKER-NETWORK$ docker network inspect test
[
{
"Name": "test",
"Id": "953b4b4c3cf864443e7b20b89979356e6618e6e3dab4649bf8043677ba777be8",
"Created": "2022-03-13T12:17:26.854970604-07:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"7c23de714d1949464b78d3a9e828095af1e5f5fc003c70490fbf2f63dc4f95c2": {
"Name": "tender_murdock",
"EndpointID": "4bfd68b665c36d61e11af4171b27442f65149cb31ac24bdb7a44b00d4c808c17",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
},
"f1be3ac60962e647a9a7ca13e44d715f4e93613a0dec77253a4b2bf85db1a855": {
"Name": "redis-container",
"EndpointID": "bedeabbf269d728bb9814726adb956892a43dfaca1cbc7579365a6242c255379",
"MacAddress": "02:42:ac:12:00:03",
"IPv4Address": "172.18.0.3/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]
test 네트워크에 2개의 컨테이너가 전부 포함된 것을 확인할 수 있다.
--network
옵션을docker run
에서 사용하면 컨테이너를 실행하면서 바로 네트워크를 지정해 줄 수 있다.
또한 docker-compose를 사용하는 것도 당연히 가능하다.
ex)dong@ubuntu:~/docker-complete/DOCKER-NETWORK$ docker run -p 5000:80 -v "$(pwd)"/server.js:/app/server.js --network test network-test-server
두 개의 컨테이너가 같은 네트워크 안에 있다면 호스트가 들어갈 자리에 컨테이너명만 넣으면 연결할 수 있다.
const express = require('express');
const bodyParser = require('body-parser');
const redis = require('redis');
const app = express();
const client = redis.createClient({
url: 'redis://redis-container:6379'
});
client.connect();
client.on('connect', () => console.log('Redis Client Connected'));
client.on('error', (err) => console.log('Redis Client Connection Error', err));
let userGoal = 'Enter Your Goal';
app.use(
bodyParser.urlencoded({
extended: false,
})
);
app.use(express.static('public'));
app.get('/', (req, res) => {
res.send(`
<html>
<head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<section>
<h2>My Course Goal</h2>
<h3>${userGoal}</h3>
</section>
<form action="/store-goal" method="POST">
<div class="form-control">
<label>Course Goal</label>
<input type="text" name="goal">
</div>
<button>Set Course Goal</button>
</form>
</body>
</html>
`);
});
app.post('/store-goal', async (req, res) => {
const enteredGoal = req.body.goal;
await client.set("userGoal", enteredGoal);
userGoal = await client.get("userGoal");
res.redirect('/');
});
app.listen(80);
도커 커스텀 네트워크의 기본값은 birdge 지만 이 외에 host, overlay 등의 다양한 종료가 있다. 자세한 내용은 아래 링크를 참고하자.
https://docs.docker.com/network/#network-drivers
윈도우와 맥의 경우, host.docker.internal
을 사용해 호스트 머신과 통신할 수 있다. 자세한 내용은 해당 문서 참조
하지만 리눅스의 경우에는 host.docker.ionternal
을 바로 사용할 수 없다. --add-host=host.docker.internal:host-gateway
옵션을 추가해 주어야 host.docker.internal
로 호스트 머신과 통신이 가능하다. --add-host=hostnetwork:host-gateway
와 같이 바꾸면 host.docker.internal
이 아니라 hostnetwork
를 사용해서 통신할 수도 있다.