Somos 6390 pessoas no grupo de Docker do telegram. Se cada um de nós perdeu ao menos 1 dia tentando fazer 2 contêineres conversarem então juntos perdemos mais de 17 anos!
Esse é um guia prático de redes docker que ajudará a entender esse universo.
A missão desse post é trazer os principais conceitos para evitar sofrimento. São conceitos ridiculamente simples, mas que vão poupar horas, dias ou até semanas de cada indivíduo que ler esse post.
Como sei isso?
Bom, eu ajudo a comunidade docker desde 2016, todo dia, várias vezes por dia.
1 ) Verdade 1: Cada container tem seu próprio IP
Se você rodar um simples docker run assim
docker run --rm --name teste -it ubuntu
E inspecionar seu container com
docker inspect teste
Terá como resposta algo assim:
[ { "Id": "2627327edcd3f02ca5678f4160e383b5a95b0a783d1e024c6a522372b801a265", "Created": "2022-06-14T23:12:01.573861823Z", "Path": "bash", "Args": [], "State": { "Status": "running", "Running": true, "Paused": false, "Restarting": false, "OOMKilled": false, "Dead": false, "Pid": 102858, "ExitCode": 0, "Error": "", "StartedAt": "2022-06-14T23:12:01.788208155Z", "FinishedAt": "0001-01-01T00:00:00Z" }, "Image": "sha256:d2e4e1f511320dfb2d0baff2468fcf0526998b73fe10c8890b4684bb7ef8290f", ... "NetworkSettings": { "Bridge": "", "SandboxID": "e90ddbe2f53b0300cf23669aaa13a86fd78614ed825253727ccc6aab8a77e23d", "HairpinMode": false, "LinkLocalIPv6Address": "", "LinkLocalIPv6PrefixLen": 0, "Ports": {}, "SandboxKey": "/var/run/docker/netns/e90ddbe2f53b", "SecondaryIPAddresses": null, "SecondaryIPv6Addresses": null, "EndpointID": "c9e40ff8413ad8e653388d606515e60581ff49d4a6994210a7c731136bf1fd1b", "Gateway": "172.17.0.1", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "IPAddress": "172.17.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "MacAddress": "02:42:ac:11:00:02", "Networks": { "bridge": { "IPAMConfig": null, "Links": null, "Aliases": null, "NetworkID": "dfd8b5810091b3125deb375c68c263a83d945dab888abd61683e1b9cf85c6209", "EndpointID": "c9e40ff8413ad8e653388d606515e60581ff49d4a6994210a7c731136bf1fd1b", "Gateway": "172.17.0.1", "IPAddress": "172.17.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "MacAddress": "02:42:ac:11:00:02", "DriverOpts": null } } } } ]
Exceções:
Quando você cria o container com rede e define ex-pli-ci-ta-men-te redes NONE ou HOST.
2 ) Verdade 2: Todo container é IRRESTRITO para receber e enviar dados pela rede, para a internet ou para outros containers.
Ao criar e rodar um container, por default, não há restrição alguma, não há limite, necessidade alguma de configuração extra para seu container escutar, receber requisições, e enviar requisições para a internet.
Você não precisa abrir porta, expor porta, não precisa de absolutamente nada.
Seu container já nasce podendo receber requisições em qualquer porta, e enviar requisições para qualquer porta de qualquer IP.
Se queremos que um container chamado API converse com um container chamado DB então:
- O container DB não precisa expor porta.
- A porta definida no dockerfile pode ser respeitada ou não.
- No dockerfile pode não haver porta definida no expose.
O container API precisa saber o IP do container DB, para alcançar o banco de dados. O resto são as credenciais.
3 ) Ao invés de referenciar o IP, use redes
Supomos que você tenha criado seu banco e api assim:
docker network create rededb -d bridge docker run --name bancodedados --network rededb -e... mysql docker run --name api --network rededb -e... -p 80:80 minhaimagemdeapi
version: "2" services: api: image: minhaimagemdeapi environment: ... ports: - 80:80 networks: - rededb bancodedados: image: mysql environment: ... networks: - rededb networks: rededb: driver: bridge
Sem expor a porta 3306 no host, ou seja, não tem o parâmetro -p.
Para o container chamado api acessar o banco bancodedados, basta usar o endereço “bancodedados:3306” para alcançar o mysql.
O container com nome bancodedados, nem sequer precisa do -p para estar acessível para o container api.
4) Referenciar IP’s é errado e abominável
Seja você um administrador, ou um container. Fazer referência, estática ao IP de um container é um erro grave.
Exceção:
Aplicações legadas (com mais de 30 anos) cuja infraestrutura de rede das libraries não consegue falar com um DNS.
5) Criar uma rede bridge é ridiculamente simples
docker network create resourcesnetwork -d bridge
6) A rede bridge default (que possui o nome “bridge”) é uma rede especial que não possui resolução de nomes (para service discovery) e é considerada legado
The default bridge
network is considered a legacy detail of Docker and is not recommended for production use. Configuring it is a manual operation, and it has technical shortcomings.
https://docs.docker.com/network/bridge/#use-the-default-bridge-network
VERDADE
Uma rede bridge custom (criada por você) tem resolução de nomes.
MENTIRA
A rede bridge tem resolução de nomes.
7) Se o EXPOSE no dockerfile não influencia em como o container é executado, para que serve?
Serve para documentar e comunicar quais portas o container escuta. Quem usa essa informação é o humano que vai criar esse container, seja via docker run, via docker compose ou yaml do Kubernetes.
docker run -d --name c1 nginx docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d4125f39dd11 nginx "/docker-entrypoint.…" 8 seconds ago Up 8 seconds 80/tcp c1
Acima temos um NGINX rodando sem nenhuma porta mapeada. Pela informação na coluna PORTS, sabemos que no dockerfile há um expose para a porta 80.
Ele acusa que ela existe e isso é útil para nós, que estamos subindo o container.
Da mesma forma que no exemplo abaixo temos a lista de portas:
- 4369/tcp
- 5671-5672/tcp
- 15691-15692/tcp
- 25672/tcp
docker run --rm --name rabbit rabbitmq docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 054aa8df1735 rabbitmq "docker-entrypoint.s…" 34 seconds ago Up 33 seconds 4369/tcp, 5671-5672/tcp, 15691-15692/tcp, 25672/tcp rabbit
Se você conhece o RabbitMQ talvez tenha sentido falta da 15672 (porta do management UI). Ela está presente apenas nas imagens cuja tag “management”.
Ao executarmos criarmos um container a partir da imagem rabbitmq:management, temos a 15672 definida no expose.
docker run --rm --name rabbit rabbitmq:management docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES fc9ed1b736d0 rabbitmq:management "docker-entrypoint.s…" 11 seconds ago Up 10 seconds 4369/tcp, 5671-5672/tcp, 15671-15672/tcp, 15691-15692/tcp, 25672/tcp rabbit
A utilidade é essa, comunicar para outro humano. O docker em si, ignora essa informação.
Exceção:
No docker run existe a possibilidade de usar o -P (P maiúsculo), que é opção que não possui parâmetros. Ela usa todas as portas definidas com EXPOSE no dockerfile para alocar portas aleatórias para cada uma delas. É útil para testes… só!
8) Se expor porta (com docker run -p) é desnecessário, para que serve então?
Seu browser, embora no linux esteja na mesma máquina que o container, não está na mesma rede, e é considerado externo do ponto de vista de rede.
No linux você poderia usar o IP do container, por exemplo.
docker run --rm -d --name c1 nginx d5d12c8439a578180455b80024e61d6b95fc0284f81fb871a242bcac177589d3 docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' c1 172.17.0.2 curl http://172.17.0.2 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> html { color-scheme: light dark; } body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html>
Mas isso estando no mesmo servidor. Os IP’s dos containers são acessíveis apenas dentro do mesmo servidor.
Se eu quero que uma aplicação que está em outra máquina, ou um serviço da internet, ou um usuário da internet, acesse algum container, eu preciso expor a porta no host.
Isso serve para você, inclusive, quando quer acessar o banco de dados com alguma ferramenta.
No Windows por conta da máquina virtual do WSL2 não é tão prático quanto no linux.
9) localhost de um container é o próprio container
Para um container, localhost ou 127.0.0.1 é o próprio container.
Ao executarmos o comando curl http://localhost
em um container qualquer, a requisição será enviada para o próprio container.
E nunca, sob hipótese alguma, essa requisição irá para:
- Seu desktop ou estação de trabalho que o container esteja sendo executado.
- Mesmo se for Linux.
- Mesmo se for Windows.
- O host no qual o container está rodando.
- Mesmo que seja Linux ou Windows.
- Ou qualquer outro novo sistema operacional que possa ser criado durante esse milênio.
Exceções:
Ao invés de usar redes bridge ou overlay, seu container pode estar em uma rede host. A rede host remove a camada de abstração de rede, imposta pelo kernel, e entrega a rede do host como rede do container.
Se seu container estiver na rede host. Então, nesse caso, você removeu a camada de rede do docker: Boa sorte!
10) Para um container acessar um recurso do host é preciso…
Há algumas opções:
Usar o IP público do host
Válido para qualquer tipo de setup docker.
E se você
Usar o nome especial host.docker.internal
Válido exclusivamente para DOCKER DESKTOP.
https://docs.docker.com/desktop/windows/networking/
11) Quero que containers de 2 docker-composes conversem entre si
No item 3 vimos que devemos usar redes docker, essa é a reposta.
Existem 2 formas de fazer isso:
11.1) Criando rede previamente
Use o item 5 desse post para criar uma rede.
#Execute docker network create -d bridge resourcesnetwork
version: "2" services: bancodedados: image: mysql environment: ... networks: - resourcesnetwork networks: resourcesnetwork: external: true
version: "2" services: api: image: minhaimagemdeapi environment: ... ports: - 80:80 networks: - resourcesnetwork networks: resourcesnetwork: external: true
11.2) Criar a rede no primeiro docker compose, e referenciar no segundo
A primeira definição de rede, do primeiro docker-compose.yaml fica como driver:bridge, enquanto no segundo fica a rede deve ser referenciada como <nome_projeto_compose>_<nome_rede> com external:true.
Ao adotar essa abordagem, o segundo docker-compose dependerá da rede que está sendo gerenciada pelo primeiro. Isso dificulta as coisas na hora de rodar docker-compose down do primeiro docker-compose, já que remover a rede não será possível por estar em uso no segundo.
Sugestões?
Comente!
Nunca tinha visto um posto tão detalhado, parebéns.