Skip to main content


Introdução ao Docker


O Docker é uma plataforma de código aberto que simplifica o processo de desenvolvimento, implantação e execução de aplicativos usando contêineres. Esses contêineres permitem empacotar um aplicativo com todas as suas dependências em uma unidade padronizada para desenvolvimento.


Ao contrário das máquinas virtuais tradicionais, que exigem um sistema operacional completo para cada aplicativo, os contêineres compartilham o kernel do host hospedeiro. Isso os torna ideais para a construção de microsserviços, onde aplicativos são divididos em componentes menores e independentes, cada um executado em seu próprio contêiner.


Além disso, o Docker oferece uma série de ferramentas para gerenciar e orquestrar contêineres em larga escala, como o Docker Compose para definir e executar aplicativos multi-contêineres, o Docker Swarm e o Kubernetes para automatizar a implantação, o dimensionamento e a operação de aplicativos em um cluster de hosts.


A existência do que conhecemos como container só foi possível graças à possibilidade de isolar processos e virtualização, essas funcionalidades estão presentes no Kernel do Linux.

O processo de isolamento de recursos do sistema Linux é uma funcionalidade do Kernel e é conhecido como Namespaces.


O Docker opera usando um conceito de camadas (Union File Systems - UnionFS), onde todas as camadas, exceto a camada superior, são somente leitura (read-only). A camada superior é a única que permite gravação. Isso é facilitado pela técnica chamada Copy-On-Write (COW). Quando um contêiner Docker precisa modificar dados em uma camada somente leitura, o Docker utiliza o Copy-On-Write. Isso significa que, em vez de modificar diretamente os dados na camada original, uma cópia desses dados é feita e enviada para a camada superior, a camada na qual é possível escrever. A modificação é então realizada nessa cópia na camada superior.


Essa abordagem é eficiente em termos de armazenamento, pois não requer a duplicação completa de todas as camadas a cada vez que um contêiner é modificado. Em vez disso, apenas as alterações feitas pelo contêiner são armazenadas separadamente na camada superior. Isso permite que múltiplos contêineres compartilhem as mesmas camadas base, enquanto mantêm suas próprias alterações isoladas na camada superior.


Essencialmente, o Copy-On-Write permite que o Docker otimize o armazenamento e o desempenho, mantendo a eficiência e a consistência dos dados em seus contêineres.



Configuração do Docker


O Docker armazena informações relacionadas a imagens, containers, volumes e outros metadados em um diretório de dados chamado /var/lib/docker no sistema host, por padrão. Esse diretório contém toda a estrutura necessária para o funcionamento do Docker.

  • /var/lib/docker/containers/
    Armazena os metadados e logs de cada container em execução ou parado. Cada container tem seu próprio diretório identificado pelo ID do container. Os logs (stdout e stderr) de um container ficam aqui.

  • /var/lib/docker/image/
    Contém as informações das imagens baixadas ou criadas localmente. Estrutura organizada de acordo com o storage driver (ex.: overlay2, aufs, etc.).

  • /var/lib/docker/volumes/
    Armazena os dados persistentes de volumes criados com o Docker. Cada volume é um diretório independente identificado pelo nome ou ID do volume.

  • /var/lib/docker/network/
    Informações sobre as redes Docker criadas. Inclui detalhes de configuração e mapeamento de sub-redes.

  • /var/lib/docker/plugins/
    Armazena plugins de terceiros instalados para o Docker.

  • /var/lib/docker/swarm/
    Contém informações sobre configurações de Swarm, caso esteja configurado.

  • /var/lib/docker/tmp/
    Diretório temporário para operações em andamento, como downloads de imagens ou criação de containers.


Além do diretório de dados principal, o Docker armazena informações em outros locais específicos, como:

  • Configuração do Docker
    O arquivo principal de configuração do Docker fica localizado em /etc/docker/daemon.json. Essas são configurações relacionadas ao daemon do Docker, como redes e armazenamento.

  • Certificados TLS (para comunicação segura entre cliente e daemon)
    É armazenado em /etc/docker/certs.d/.


Para alterar o local padrão do armazenamento (por exemplo, para um disco diferente), podemos modificar o arquivo de configuração do Docker.

/etc/docker/daemon.json
{
"data-root": "/caminho/para/novo/local"
}

Agora reinicie o serviço Docker:

Terminal
╼ $ sudo systemctl restart docker


Live Restore


A configuração live-restore: true no arquivo daemon.json ativa o recurso de Live Restore, esse recurso permite que os contêineres continuem em execução mesmo que o serviço principal do Docker (o daemon dockerd) seja interrompido ou reiniciado.


Quando o recurso está habilitado, os contêineres que já estão em execução não são afetados se o daemon Docker for:

  • Parado (systemctl stop docker)
  • Reiniciado (systemctl restart docker)
  • Encerrado inesperadamente

Porém, as operações que dependem diretamente do daemon, como a criação de novos contêineres ou a execução de comandos docker, não estarão disponíveis até que o daemon seja reiniciado. Para ativar o Live Restore, adicione o seguinte no arquivo de configuração do Docker (/etc/docker/daemon.json):

{
"live-restore": true
}

Depois, reinicie o serviço Docker para aplicar as configurações:

sudo systemctl restart docker

bug no live-restore

Normalmente, ao iniciar, o Docker cria a cadeia DOCKER-USER e a insere no topo da cadeia FORWARD. Isso permite que os usuários adicionem suas próprias regras de firewall que serão aplicadas antes das regras padrão do Docker.


No entanto, foi observado que, se a cadeia DOCKER-USER já existir antes do início do Docker, ela não é reinserida no topo da cadeia FORWARD como esperado. Consequentemente, as regras personalizadas definidas pelo usuário na cadeia DOCKER-USER não são aplicadas, pois a referência a essa cadeia está ausente na cadeia FORWARD.


Esse comportamento é problemático, especialmente em cenários onde o Docker é reiniciado com a opção live-restore ativada. Nesses casos, o Docker não recria a rede bridge padrão nem reinsere a cadeia DOCKER-USER na cadeia FORWARD, resultando na não aplicação das regras de firewall definidas pelo usuário.


Fonte: https://github.com/moby/moby/issues/48560



Docker Internals


"Docker Internals" se refere aos componentes internos e à arquitetura do Docker. Isso inclui entender como o Docker funciona por baixo dos panos, como os contêineres são isolados, como o armazenamento é gerenciado, como a rede é configurada, entre outros aspectos técnicos.


Selecao_026



Namespaces


Os Namespaces é uma recursod o kernel que é responsáveis por isolar recursos do sistema, como processos, pontos de montagem, rede, e outros, quando utilizamos o Docker. Graças a isso, cada container pode ter sua própria árvore de processos, pontos de montagem, hostname, interfaces de rede, e outros recursos, sem influenciar no sistema hospedeiro ou em outros containers.


O Docker utiliza diversos tipos de namespaces, como PID (Identificador de Processo), NET (Rede), MNT (Montagem), UTS (Sistema Unix Timesharing), IPC (Comunicação entre Processos) e USER (Usuário). Ao aproveitar esses namespaces, o Docker é capaz de criar containers leves, portáteis e seguros, que funcionam de forma consistente em diferentes ambientes.


NamespaceDescrição
PID namespacePermite que cada contêiner tenha seus próprios identificadores de processos.
Net namespacePermite que cada contêiner tenha sua própria interface de rede e portas.
Mnt namespacePermite que cada contêiner tenha seu próprio ponto de montagem e sistema de arquivos isolados.
IPC namespaceFornece um ambiente isolado para a comunicação entre processos (SystemV IPC e fila de mensagens POSIX).
UTS namespaceFornece isolamento de hostname, nome de domínio e versão do sistema operacional para cada contêiner.
User namespaceMantém um mapa de identificação de usuário para cada contêiner, permitindo a execução de processos com privilégios reduzidos.


Cgroups


O cgroup ou Control Groups é um outro recurso do Kernel (assim como namespaces) que é usado para permitem a alocação, limitação e monitoramento de recursos de sistema. No contexto do Docker e de outros sistemas de contêineres,os cgroups são usados para impor restrições de recursos nos containers, garantindo um comportamento consistente e previsível. Em outras palavras, é usado para limitar a utilização de recursos do containers, impedindo que eles usem todos os recursos da máquina host.


O cgroup permite gerenciar recursos, como: CPU, memória, largura de banda de rede e I/O. No cgroups v2, todos os controladores são unificados em uma única hierarquia, enquanto no cgroups v1 os controladores são separados. Abaixo estão os principais recursos que os cgroups podem controlar:

  • CPU
    Define a quantidade de tempo de CPU que os processos podem consumir. Exemplos de Configurações:

    • cpu.weight: Prioridade relativa de acesso à CPU entre diferentes cgroups.
    • cpu.max: Limita a quantidade máxima de tempo de CPU que pode ser usado (em microssegundos).
  • Memória Limita a quantidade de memória física e swap que pode ser usada. Exemplos de Configurações:

    • memory.max: Limite máximo de memória física.
    • memory.swap.max: Limite de swap usado pelo grupo.
    • memory.current: Monitora o uso de memória atual.
  • Bloco de E/S (I/O)
    Garante acesso controlado a dispositivos de armazenamento. Exemplos de Configurações:

    • io.max: Define limites máximos de leitura e escrita para dispositivos.
    • io.weight: Prioriza o acesso ao disco entre diferentes cgroups.
  • Rede (Network)
    Regula o uso de largura de banda de rede e restrições de acesso. Exemplos de Configurações:

    • No cgroups v1: O controlador net_cls pode classificar pacotes de rede.
    • No cgroups v2: O controlador network ainda não possui suporte amplamente implementado, mas pode ser gerenciado via ferramentas como tc (traffic control).
  • Dispositivos (Devices)
    Restringe ou permite acesso a dispositivos específicos (por exemplo, discos ou portas USB). Exemplos de Configurações:

    • devices.allow: Permite acesso a dispositivos específicos.
    • devices.deny: Bloqueia acesso a dispositivos.
  • Congelamento de Processos

  • Controle: Permite congelar ou descongelar processos em um cgroup. Exemplos de Configurações:

    • freezer.state: Define o estado dos processos (FROZEN ou THAWED).
  • PID
    Limita o número máximo de processos que podem ser criados. Exemplos de Configurações:

    • pids.max: Define o limite máximo de processos para o grupo.
    • pids.current: Mostra o número atual de processos no grupo.
  • Habilidade de Inicialização (Devices e Runtime)
    Controla permissões no nível de dispositivos e pode ser útil para segurança.



Virtual Machine x Container


A Máquina Virtual é um sistema Operacional completo sendo emulado por um software, esse software é chamado de hipervisor. O hipervisor é responsável por gerenciar os recursos do hardware físico que podem ser usados por máquinas virtuais individuais. Essas máquinas virtuais são chamadas de convidadosdo hipervisor.


Uma máquina virtual tem muitos aspectos de um computador físico emulado em software, como BIOS do sistema e controladores de disco rígido. Uma máquina virtual geralmente usará imagens de disco rígido que são armazenadas como arquivos individuais e terá acesso à RAM e CPU da máquina host por meio do software hipervisor.


O hipervisor separa o acesso aos recursos de hardware do sistema host entre os convidados, permitindo assim que vários sistemas operacionais sejam executados em um único sistema host.


Enquanto que Container usa o Sistema Operacional hospedeiro e cria suas próprias bibliotecas compartilhada e estáticas. Containers usam uma imagem de container estática, geralmente não será muito modificada mas pode ser, é mais como um snapshot do ambiente, permitindo a restauração mais rápida usando o container.

virt



O que é um Container?


Um container Docker é uma série de processos que rodam no sistema operacional do host hospedeiro, mesmo assim, esses processos se comportam diferente de outros processos na camada de rede (por estar em um "container"). Vamos fazer um exercício de imaginação para tentar entender de forma simplificada a estrutura que o Docker usa. Imagine que cada container é um host que esta dentro de uma rede virtual privada e o host hospedeiro possui acesso a essa rede, por isso é possível se comunicar com os containers.


Quando queremos que o container seja acessível ao mundo externo (internet ou rede lan) o host hospedeiro vai atuar como uma ponte (bridge) que permite a comunicação de dispositivos fora da rede virtual privada com os containers. Isso deve ser configurado na criação do container.


O Docker aloca uma sub-rede privada não utilizada, normalmente 172.16.0.0/16 e cria uma interface virtual chamada docker0 para o host hospedeiro. Isso também torna possível que containers se comuniquem entre si por estarem na mesma rede.


Os containers do Docker são criados em sistemas de arquivos em camadas, cada uma é identificada por um hash. Cada conjunto de mudança é colocada no topo da pilha de mudança anterior. Isso é perfeito porque quando temos um novo build apenas precisamos recompilar as camadas afetadas e compilar a mudança a ser implantada.


As imagens do Docker contém tudo que a aplicação precisa para rodar, se apenas uma linha de código for modificada não precisamos recompilar tudo novamente, com Docker, apenas as camadas modificadas serão recompiladas.


resumindo

Um container é um pacote de software leve, autônomo e executável que inclui todas as dependências (bibliotecas, binários e arquivos de configuração) necessárias para executar um aplicativo. Os contêineres isolam os aplicativos de seu ambiente, garantindo que eles funcionem consistentemente em diferentes sistemas.



Arquitetura


O Docker usa a arquitetura de cliente/servidor, mas diferente de outras aplicações que possuem binários distintos para cada função, o docker possui apenas um único binário que atua como cliente ou servidor dependendo de como você invoca os comandos do docker. Além do cliente/servidor existe um terceiro componente chamado registry que tem como função armazenar as imagens do docker e os metadados das imagens.


O servidor tem a função de efetuar o trabalho continuo de executar e gerenciar os containers. já o cliente diz ao servidor o que fazer. o daemon (aplicação que nesse caso roda no servidor) pode ser executado em quantos servidores desejarmos, e um único cliente pode endereçar todos eles sem nenhum problema.


Pela arquitetura o docker tem como alvo aplicações que são stateless ou onde o estado é externalizado para um sistema de armazenamento de dados como um banco de dados ou cache. Por isso tentar colocar uma engine de banco de dado dentro do docker é considerado nadar contra a corrente, não é que não de para fazer ou que não seja recomendado, é que o docker não foi criado pensando nesse tipo de aplicação.


A razão para isso é que os bancos de dados são, por natureza, stateful, o que significa que eles mantêm um estado interno que é crítico para o funcionamento correto da aplicação. Isso pode incluir dados persistentes, caches em memória, configurações específicas do banco de dados e muito mais.


Ao executar um banco de dados dentro de um contêiner Docker, você pode enfrentar desafios relacionados à persistência de dados, gerenciamento de armazenamento, escalabilidade e segurança. Embora seja possível resolver esses desafios usando volumes Docker, redes específicas e outras técnicas, ainda assim pode não ser a melhor prática para aplicações críticas ou de produção.


O caso de uso do docker se destina a aplicações front-end, APIs de backend e tarefas de curta duração. Servidores Web também podem entrar aqui. Boa parte das aplicações são stateful, isso significa que elas mantém dados importantes em memoria, arquivos ou banco de dados. Caso o sistema seja reiniciado pode perder dados que não foram escritos num desses tipos de memoria.


É útil deixar claro que, embora o Docker tenha sido inicialmente concebido para aplicações stateless, ele ainda pode ser usado para aplicações stateful, com o uso adequado de volumes e persistência de dados.



Portas de Redes e Sockets Unix


O cliente e o daemon do servidor se comunicam usando sockets. Os sockets podem escutar portas TCP ou serem arquivos no unix. Normalmente, a comunicação entre o cliente Docker e o daemon Docker (servidor) que estão no mesmo host é realizada através arquivos sockets no sistema de arquivos.


Porém, em ambientes onde o cliente Docker e o daemon Docker estão em máquinas diferentes, ou quando a comunicação precisa passar por uma ou mais redes, é possível configurar o Docker para se comunicar através de uma porta TCP específica.


A porta sem criptografia usada e registrada pelo docker é a 2375 e a com criptografia é a 2376.



Armazenamento de Estado


Aplicações que necessitam armazenar arquivos no sistema de arquivos do contêiner enfrentam diversos desafios. Armazenar dados diretamente no sistema de arquivos do contêiner pode apresentar limitações significativas, incluindo espaço limitado disponível e falta de preservação do estado dos arquivos ao longo do ciclo de vida do contêiner.


Esses desafios podem resultar em perda de dados, dificuldade de escalabilidade e falta de flexibilidade para lidar com operações de leitura/gravação intensivas ou necessidades de armazenamento de dados persistentes.


Como alternativa, é possível armazenar o estado em um bind mount, essa prática não é amplamente recomendada, pois introduz dependências entre o host e o container, e só poderá ser implantado em um sistema que possuam o mesmo filesystem.


Além disso, o uso de serviços de armazenamento externo, como sistemas de arquivos compartilhados, bancos de dados ou serviços de armazenamento em nuvem, também pode ser considerado para atender às necessidades específicas de armazenamento de dados das aplicações, proporcionando uma solução mais robusta e confiável para o armazenamento de estado.



Containerd e LXC


Antes da versão 0.9, o Docker usava o LXC (Linux Containers) como sua tecnologia subjacente para criar e gerenciar contêineres. Naquela época, o Docker era basicamente uma camada de gerenciamento e automação em torno do LXC, facilitando a criação, distribuição e execução de contêineres.


O LXC é uma tecnologia que oferece virtualização de nível de sistema operacional para Linux, permitindo a execução de múltiplos sistemas Linux isolados em um único host Linux. O Docker inicialmente aproveitou as capacidades do LXC para criar ambientes de contêineres isolados e portáteis.


No entanto, após o Docker 0.9, o Docker começou a migrar para sua própria tecnologia de execução de contêineres, que se tornou conhecida como "containerd". O containerd foi desenvolvido como uma camada de tempo de execução de contêineres mais genérica, fornecendo funcionalidades semelhantes ao LXC mas com maior flexibilidade e abstração. A partir do Docker 1.11, o Docker Engine começou a usar o containerd como sua interface de execução de contêineres padrão, e o uso direto do LXC foi descontinuado.


O Docker funciona como uma interface amigável que simplifica e abstrai as operações de contêiner. No entanto, ele não consegue criar ou gerenciar contêineres sozinho porque depende de componentes subjacentes para executar essas tarefas. Nesse cenário, o containerd é o "motor" que faz o trabalho pesado.


O Docker fornece ferramentas para criar, gerenciar e distribuir contêineres de forma intuitiva, seja via CLI ou API. Já o containerd é o responsável por executar e gerenciar os contêineres de fato, lidando diretamente com o kernel do sistema operacional.


Falando de forma mais técnica, o Docker usa o containerd como um runtime backend, ou seja, ele delega ao containerd todas as operações de execução de contêineres, como:

  • Gerenciar namespaces e cgroups (isolar processos e recursos).
  • Baixar e montar as imagens.
  • Executar contêineres através do runc (um executável que implementa o padrão OCI).

Sem o containerd, o Docker não consegue criar ou rodar contêineres e gerenciar diretamente a interação com o kernel. Portanto, sem o containerd, o Docker perde sua capacidade de executar contêineres, tornando-se apenas uma interface que não tem como agir. Por causa disso, o Kubernetes não precisa ter o Docker instalado para conseguir operar, necessitando apenas do containerd.


Antigamente, o Kubernetes utilizava o Docker como runtime backend, mas a partir da versão 1.20, começou a descontinuar o suporte ao Docker como runtime nativo. Essa mudança ocorreu porque o Docker é uma solução mais pesada, contendo componentes que o Kubernetes não utiliza, como a CLI e ferramentas para build de imagens.


Além disso, o uso do Docker adicionava uma camada intermediária chamada dockershim, responsável por traduzir as interações entre o Kubernetes e o container runtime real, o containerd. Essa camada extra tornava o gerenciamento mais complexo e menos eficiente. Na versão 1.24, o suporte ao dockershim foi completamente removido, permitindo que o Kubernetes interaja diretamente com runtimes como o containerd, eliminando a dependência do Docker e tornando o sistema mais leve e eficiente.



Storage drivers


Os Drivers de armazenamento (Storage Drivers) é um mecanismo usado pela engine do Docker para ditar como ele vai manipular os dados no filesystem do container, por exemplo, como ele vai armazenar os dados.



AUFS (Another Union File System)


Este foi o primeiro filesystem disponível para o Docker, ele funciona em nível de arquivo (não em bloco), tendo múltiplos diretórios que é apresentado ao S.O como apenas um único ponto de montagem.


Escrever em arquivos grandes causa lentidão na performance porque deve ser copiado o arquivo para a camada superior de escrita, sua busca é feito através do PATH, o problema é que, para cada camada, ele vai buscar os diretórios dentro do PATH, ou seja, um PATH com 2 diretórios serão pesquisados estes 2 diretórios em todas as camadas, até achar o que procura ou concluir que não existe.


Quando um arquivo é deletado, ele não é realmente deletado, ele é apenas renomeado para .wh.<nome> e fica indisponível para o container, já que não da para apagar por ser read-only.h.



Device Mapper


Há uma semelhança muito grande entre AUFS e Device Mapper, o DM (Device Mapper) foi criado pela Red Hat e permite técnicas como RAID e LVM graças ao mapeamento de blocos físicos para lógicos. Ele corrige alguns problemas do AUFS, como o problema de copiar arquivos grandes quando formos alterar, a cópia é feita no nível de bloco, em teoria, o problema de performance não ocorre mais.



OverlayFS e OverlayFS2


O OverlayFS é uma versão melhorada do AUFS, sua segunda versão é recomendada pela equipe do Docker. Ele veio com multilayer e page caching sharing, ou seja, múltiplos containers acessando o mesmo arquivo e dividem a mesma entrada no arquivo de paginação, economizando mais memoria.


Como ele é a nível de arquivo, ainda temos o problema de copiar para camada superior, porém, após copiado, ele permanece lá, desta forma edições futuras serão mais rápidos.



BRTFS


Trabalha em nível de bloco, suporte inúmeras tecnologias avançadas de storage e suporte copy-on-write snapshots. Só é suportado na versão CE para Debian-like e EE para Suse Linux Enterprise Server.



ZFS


O ZFS é um sistema de arquivos de próxima geração que oferece suporte a muitas tecnologias avançadas de armazenamento, como gerenciamento de volumes, snapshots, checksumming, compressão, deduplicação, replicação e muito mais.


O ZFS é um sistema de arquivos que opera em um nível mais alto do que o tradicional sistema de arquivos baseado em blocos. Ele gerencia tanto os blocos de dados quanto os metadados, permitindo uma abordagem mais integrada e eficiente para armazenamento e gerenciamento de dados.


Em decorrência das incompatibilidades de licença entre a CDDL e a GPL, o ZFS não pode ser distribuído como parte do kernel Linux principal. No entanto, o projeto ZFS On Linux (ZoL) fornece um módulo de kernel e ferramentas de espaço de usuário que podem ser instalados separadamente.


A porta do ZFS para Linux (ZoL) está saudável e amadurecendo. No entanto, neste momento, não é recomendado usar o driver de armazenamento zfs do Docker para uso em produção, a menos que você tenha experiência substancial com o ZFS no Linux.



Docker Hub


O Docker Hub é uma plataforma de registro de imagens Docker. Ele funciona como um repositório central onde usuários podem armazenar, compartilhar e gerenciar imagens Docker. Antes de continuar, crie uma conta no docker hub. Após a criação da conta, vamos logar com nossa conta via CLI:

Terminal
╼ $ docker login
Log in with your Docker ID or email address to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com/ to create one.
You can log in with your password or a Personal Access Token (PAT). Using a limited-scope PAT grants better security and is required for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/

Username: fulano
Password: ***************************
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

Não se esqueça de configurar uma credencial para não deixar a senha armazenada em texto puro, como informado na mensagem.



Orquestração de Container


Orquestrar um contêiner refere-se ao processo de gerenciar e coordenar a implantação, operação e escalabilidade de múltiplos contêineres em um ambiente distribuído. Ou seja, orquestrar um contêiner é organizar e controlar como vários contêineres trabalham juntos. Isso inclui garantir que eles sejam iniciados corretamente, mantidos funcionando, escalados quando necessário (adicionando ou removendo contêineres) e que se comuniquem entre si de forma eficiente, mesmo em diferentes servidores.


Na orquestração de contêineres usando Docker, várias ferramentas são comumente usadas para gerenciar, implantar e escalar aplicativos em um ambiente de contêiner. Algumas das ferramentas mais populares incluem:

  • Docker Swarm: Uma ferramenta nativa do Docker para orquestração de contêineres. Ela permite que você agrupe hosts Docker em um cluster e gerencie os contêineres em escala.

  • Kubernetes (k8s): Desenvolvido pelo Google e mantido pela Cloud Native Computing Foundation (CNCF), o Kubernetes é uma plataforma de código aberto para automatizar a implantação, o dimensionamento e a operação de aplicativos em contêineres. Ele oferece recursos avançados de orquestração, como balanceamento de carga, autoescalonamento, implantação de canários e muito mais.

  • Docker Compose: Embora não seja uma ferramenta de orquestração completa, o Docker Compose permite definir e executar aplicativos Docker com vários contêineres. É útil para ambientes de desenvolvimento e teste.

  • Mesosphere (DC/OS): Uma plataforma de código aberto que pode orquestrar contêineres Docker, juntamente com outros tipos de cargas de trabalho, como aplicativos tradicionais e big data.

  • OpenShift: Desenvolvido pela Red Hat, o OpenShift é uma plataforma de contêiner corporativa baseada em Kubernetes. Ele fornece recursos adicionais, como integração contínua, entrega contínua (CI/CD) e segurança aprimorada.

  • Nomad: Desenvolvido pela HashiCorp, o Nomad é uma plataforma de orquestração de contêineres e trabalho que permite implantar e gerenciar aplicativos em grande escala.

  • Rancher: Uma plataforma de gerenciamento de contêineres que oferece suporte a várias orquestradores, incluindo Kubernetes, Docker Swarm e outros.

  • Amazon ECS (Elastic Container Service): Um serviço gerenciado pela AWS para executar, parar e gerenciar contêineres Docker em um cluster.



Open Container Initiative - OCI


A Open Container Initiative (OCI) é uma organização aberta, criada em 2015 pela Docker e outras empresas do setor de tecnologia, com o objetivo de padronizar formatos e ferramentas relacionados a contêineres. Hoje ela é gerenciada pela Linux Foundation e busca promover interoperabilidade e consistência entre diferentes plataformas de contêineres.


A OCI criar especificações abertas para imagens de contêineres e tempos de execução (runtimes), permitindo que diferentes ferramentas sejam compatíveis entre si, ou seja, uma imagem que está nesse padrão, funcionará no Docker, no CRI-O, containerd, Podman, entre outras.


A OCI mantém duas especificações principais:

  1. Runtime Specification (runc)
    Define como um contêiner deve ser iniciado e gerenciado em um sistema operacional. O runc é o runtime padrão de referência para contêineres baseado nessa especificação.

  2. Image Specification
    Estabelece o formato de imagens de contêineres, descrevendo como elas devem ser empacotadas, armazenadas e distribuídas. Isso garante que imagens criadas em uma ferramenta (ex.: Docker) possam ser usadas por outra (ex.: Podman).



Criando Container sem usar Docker ou LXC/LXD


Vou mostrar mais ou menos como seria criar containers manualmente, o processo abaixo não é recomendado em ambiente de produção, pelo menos sem algum esforço para tornar ele mais completo e mais seguro. O intuito é demonstrar como tecnologias que trabalham com containers conseguem isolar processos e limitar recursos.


Terminal
# Instale o debootstrap:
╼ $ sudo apt install -y debootstrap

# Obtenha um sistemas básicos Debian:
╼ $ sudo debootstrap stable /mnt/debian http://deb.debian.org/debian

## Para Ubuntu, use o comando abaixo:
╼ $ debootstrap jammy /mnt/jammy http://archive.ubuntu.com/ubuntu

# Veja os arquivos que temos:
╼ $ ls /mnt/debian
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

# Veja qual é o S.O que temos como hospedeiro:
╼ $ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.5 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.5 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

# Agora faça um chroot para alterar o diretório raiz:
╼ $ sudo chroot /mnt/debian/

# Veja qual é o S.O que temos no sistemas convidado:
╼ $ cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

# Tente executar algum comando como o 'ps':
root@ubuntu2204:/# ps
Error, do this: mount -t proc proc /proc

O que é debootstrap?

A função do debootstrap é criar um sistema base Debian (ou Ubuntu) em um diretório, sem a necessidade de um sistema operacional previamente instalado. Essa ferramenta é útil para montar sistemas mínimos ou configurar ambientes isolados, de forma semelhante ao conceito de containers.


O que ele faz, essencialmente, é preparar um conjunto básico de arquivos e pacotes necessários para que o ambiente funcione como um sistema Linux funcional, baseado na estrutura do Debian. Isso inclui:

  • Diretórios essenciais como: /bin, /etc, /lib, etc.
  • Ferramentas básicas para o funcionamento do sistema.
  • Um ambiente mínimo que pode ser expandido conforme necessário.

Dessa forma, o debootstrap permite configurar rapidamente ambientes de teste, sistemas isolados, ou preparar a base para um sistema que será executado em um chroot, em containers ou até em VMs.


Isso acontece porque o sistema Debian que temos, não possui os pontos de montagem como proc, sys e dev. Esses pontos de montagem, são sistemas de arquivos virtuais usados pelo kernel para fornecer informações sobre os processos, dispositivos e outros aspectos do sistema em execução.


Quando você tenta executar o comando ps (ou comandos que dependem de /proc), ele falha porque o /proc não está montado. Sem esse ponto de montagem, o sistema isolado não consegue acessar informações sobre os processos. Vamos montar o /proc, esse processo deve ser realizado no sistema hospedeiro:


Terminal
# Agora faça um chroot novamente:
╼ $ sudo chroot /mnt/debian/

# Crie o ponto de montagem para '/proc':
root@docker:/# mount -t proc proc /proc

# Tente executar algum comando como o 'ps':
root@docker:/# ps
PID TTY TIME CMD
60038 ? 00:00:00 sudo
60039 ? 00:00:00 su
60040 ? 00:00:00 bash
69234 ? 00:00:00 bash
69238 ? 00:00:00 ps

O sistema Debian exibido possui vários processos, mas eles são do host hospedeiro e não do ambiente Debian. Isso ocorre porque não há isolamento real de processos, o chroot apenas altera o diretório raiz do processo, mas ainda compartilha o mesmo namespace de processos do host. Assim, o sistema convidado (Debian) não tem seu próprio namespace de processos isolado.


Para isolar os processos, precisamos usar os namespaces do Linux. Um namespace é um recurso do kernel que isola diferentes aspectos do sistema, como processos, rede, mount, user, cgroups, IPC (comunicação entre processos) e PID (identificadores de processos). Usando namespaces, é possível criar um ambiente isolado onde os processos "acreditam" estar em um sistema separado, já que os namespaces são independentes entre si e não compartilham informações diretamente.


Vamos isolar o sistema Debian. Não iremos isolar a rede (NET namespace) porque isso exigiria configurar uma rede própria e trabalhar com encaminhamento de pacotes. Como o objetivo aqui é apenas demonstrar o funcionamento básico, não vamos nos aprofundar nesse aspecto. Os comandos abaixo devem ser realizados na o sistema hospedeiro:


Atenção

Antes de continuar execute os comandos abaixo, como nós executamos o mount do /proc anteriormente, ele ficará montado e influenciando negativamente no isolamento.


Terminal
# Faça o chroot:
╼ $ sudo chroot /mnt/debian/

# Desmonte o /proc que foi montado anteriormente:
root@docker:/# umount /proc

Agora podemos continuar isolando o sistema Debian:

Terminal
# Isole o namespaces do sistema Debian e já faça 'chroot':
╼ $ sudo unshare --mount --pid --fork --user --map-root-user --uts --ipc chroot /mnt/debian /bin/bash

# Agora vamos montar o '/proc'/
root@docker:/# mount -t proc proc /proc

# Agora liste os processos:
root@docker:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 21:48 ? 00:00:00 /bin/bash
root 7 1 0 21:49 ? 00:00:00 ps -ef

Podemos ver nosso sistema Debian só possui dois processos rodando, com isolamento de processos efetuado com sucesso.


Opções do unshare

Abaixo podemos ver o significado de cada opção utilizada no comando unshare com namespaces no Linux. Vale notar que o comando unshare quando utilizado para criar um novo namespace de PID, vai isolar apenas os processos que são iniciados após a sua criação.

  • --pid
    É usado para isolar os identificadores de processos, criando um novo namespace de PID. No novo namespace, os processos terão uma nova hierarquia de IDs, começando do 1. Um processo dentro do namespace não verá processos fora dele.

  • --net
    Cria um novo namespace de rede (NET). Isolando interfaces de rede, tabelas de roteamento, sockets e outras configurações relacionadas à rede. O sistemas isolado pode ter um endereço IP separado ou uma interface de loopback independente.

  • --user
    Cria um novo namespace de usuário (USER). Permite mapear IDs de usuários e grupos dentro do namespace. Usado para rodar processos como "root" no namespace, mesmo que o processo seja um usuário comum no sistema anfitrião.

  • --map-root-user
    Trabalha em conjunto com --user. Faz com que o usuário "root" dentro do namespace seja mapeado para o ID do usuário que executou o comando no host. O root no namespace tem privilégios limitados fora dele, aumentando a segurança.

  • --fork
    Garante que o processo que entra no namespace seja independente. Após criar o namespace, o comando executa o processo como um novo "filho", para que o PID do processo inicial seja isolado do host.

  • --uts
    Cria um novo namespace de UTS (Unix Timesharing System), isolando o hostname e o nome de domínio do sistema. Você pode definir um hostname personalizado para o namespace sem impactar o sistema anfitrião.

  • --ipc
    Cria um novo namespace de IPC (Inter-Process Communication), isolando recursos de comunicação entre processos, como semáforos, filas de mensagens e segmentos de memória compartilhada. Processos dentro do namespace não podem acessar os mecanismos de IPC criados fora dele.

  • --mount
    Cria uma visão independente das montagens de sistemas de arquivos, permitindo que o processo e seus filhos alterem as montagens (como montar ou desmontar sistemas de arquivos) sem interferir no sistema anfitrião.


Por fim, vamos usar cgroup para isolar CPU e Memória.


Terminal
# Precisa usar cgroups para limitar cpu/memoria
╼ $ sudo apt install -y cgroup-tools

# Isole o namespaces do sistema Debian e já faça 'chroot':
╼ $ sudo unshare --mount --pid --fork --user --map-root-user --uts --ipc chroot /mnt/debian /bin/bash

# Monte o '/proc':
root@docker:/# mount -t proc proc /proc

## Em outro terminal, isole memory,cpu,blkio,freezer,devices baseado no processo do unshare.

# Obtenha o PID:
╼ $ ps -ef | grep unshare -A1
root 37597 37596 0 19:30 pts/3 00:00:00 unshare --mount --pid --fork --user --map-root-user --uts --ipc chroot /mnt/debian /bin/bash
root 37598 37597 0 19:30 pts/3 00:00:00 /bin/bash

# Crie um grupo (chamarei de 'debiantest') para controle de memória, CPU, dispositivos de E/S, entre outros:
╼ $ sudo cgcreate -g memory,cpu,blkio,freezer,devices:/debiantest

# Se voce estiver executando numa VM, pode receber o erro abaixo:
## Erro: cgcreate: can't create cgroup /debiantest: Cgroup one of the needed subsystems is not mounted

# Associe um processo (PID 37598) ao grupo criado.
╼ $ sudo cgclassify -g memory,cpu,blkio,freezer,devices:debiantest 37598

# Os dados do grupo ficam em:
╼ $ cat /sys/fs/cgroup/cpu/debiantest/tasks
37598

# Limite o uso de memória para 128 MB.
╼ $ sudo cgset -r memory.limit_in_bytes=128M debiantest

# Defina o tempo de execução na CPU permitido para o grupo:
╼ $ sudo cgset -r cpu.cfs_period_us=100000 -r cpu.cfs_quota_us=$((10000*$(grep -c ^processor /proc/cpuinfo))) debiantest

# Usando chroot podemos instalar o 'stress' no chroot para testar o limite de recursos.


Instalando o Docker


Atualmente, o Docker é divido em dois produtos, Community Edition (CE) e Enterprise Edition (EE). Como os nomes sugerem, o Community é gratuito e voltado a comunidade, enquanto o Enterprise é recomendado para uso empresarial.

Vou fazer a instalação do docker usando um script fornecido pelo Docker.



Docker Desktop


Docker Desktop é uma aplicação que oferece uma experiência de desenvolvimento completa para usuários de Windows e macOS. Ela inclui o Docker Engine, a tecnologia que executa contêineres Docker, juntamente com uma interface gráfica de usuário (GUI) e ferramentas para gerenciamento de contêineres, redes e volumes.



Docker Engine


Docker Engine é o componente principal do Docker que executa e gerencia contêineres Docker em um sistema operacional. Ele consiste em vários componentes, incluindo o daemon do Docker (dockerd) e a API do Docker, e é responsável por criar e executar contêineres a partir de imagens Docker, bem como por fornecer funcionalidades como isolamento de recursos, redes e volumes.


docker em servidores

Quando você instala o Docker em um servidor, geralmente você instala o Docker Engine. O Docker Engine é o componente principal do Docker que gerencia os contêineres Docker e executa as operações principais relacionadas aos contêineres.


Então, para a maioria dos casos de uso em um servidor, você estaria instalando o Docker Engine. Isso permitirá que você crie, execute e gerencie contêineres Docker diretamente no servidor.



Instalar o Docker Engine usando script

Uma das formas de instalar o Docker é usando um script fornecido no site do Docker:

Terminal
╼ $ sudo curl -fsSL https://get.docker.com/ | sh

Opções usadas no Curl

-f = Em casos normais, quando um servidor HTTP falha na entrega de um documento, ele retorna um documento HTML informando isso (que geralmente também descreve o porquê e muito mais). O -f impedirá que o erro seja exibido e faça download do arquivo e como resposta, seja retornado o erro 22.


-s = Não mostre medidor de progresso ou mensagens de erro. -S = Quando usado com -s, o curl mostra uma mensagem de erro se falhar.


-L = Se o servidor relatar que a página solicitada foi movida para um local diferente (indicado com um cabeçalho Local: e um código de resposta 3XX), essa opção irá refazer a solicitação no novo local. Se usado junto com -i, --include ou -I, --head, os cabeçalhos de todas as páginas solicitadas serão mostrados. Quando a autenticação é usada, o curl envia apenas suas credenciais para o host inicial. Se um redirecionamento receber curl para outro host, ele não poderá interceptar o usuário + a senha. Veja também --location-trust sobre como mudar isso. Você pode limitar a quantidade de redirecionamentos a seguir usando a opção --max-redirs.


Quando o curl segue um redirecionamento e a solicitação não é um GET simples (por exemplo, POST ou PUT), ele faz a solicitação a seguir com um GET se a resposta HTTP for 301, 302 ou 303. Se o código de resposta for qualquer outro 3xx código, curl reenviará a solicitação a seguir usando o mesmo método não modificado.


Você pode dizer ao curl para não alterar o método de solicitação não GET para GET após uma resposta 30x usando as opções dedicadas para isso: --post301, --post302 e -post303.



Instalar o Docker Engine manualmente

Para ambiente de servidores não é recomendado instalar o Docker Desktop, já que servidores normalmente não possuem interface gráfica. Para instalar manualmente, sem usar o script, usando as versões disponíveis no repositório da distro, use o comando abaixo:

Terminal
╼ $ sudo apt-get -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-ce-rootless-extras docker-buildx-plugin


Instalar o Docker Engine a partir do repositório oficial do Docker

Para instalar a versão mais recente, vamos usar o repositório oficial:

Terminal
# Atualize a versão dos pacotes conhecidos pelo package manager:
╼ $ sudo apt-get update

# Instale o curl e o ca-certificates:
╼ $ sudo apt-get install -y ca-certificates curl

# Crie o diretório abaixo com as permissões 0755:
╼ $ sudo install -m 0755 -d /etc/apt/keyrings

# Baixe a chave gpg do Docker, será necessária para baixar os pacotes vindo do repositório dele:
╼ $ sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc

# Corrija as permissões do arquivo baixado:
╼ $ sudo chmod a+r /etc/apt/keyrings/docker.asc

# O comando abaixo vai criar o mapeamento do repo dele no nosso S.O, com as variáveis corretas:
╼ $ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Atualize a versão dos pacotes conhecidos pelo package manager:
╼ $ sudo apt-get update

# Agora é só instalar:
╼ $ sudo apt-get -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-ce-rootless-extras docker-buildx-plugin

A tabela abaixo mostra os componentes do Docker que a gente precisa instalar:

ComponenteDescrição
containerd.ioDaemon de contêiner de alto desempenho, uma implementação do runtime do Docker.
docker-ceVersão gratuita e de código aberto do Docker Engine, incluindo Docker CLI e Docker daemon.
docker-buildx-pluginPlugin Docker que fornece uma experiência de compilação aprimorada para imagens de contêineres.
docker-ce-rootless-extrasExtras do Docker CE para execução sem privilégios, permitindo a execução do Docker sem privilégios.
docker-compose-pluginUm plugin personalizado para o Docker Compose que estende sua funcionalidade.


Firewall e Filtragem de pacotes


O Docker isola a rede dos containers através do namespace de rede (NET namespace), criando uma pilha de rede isolada para cada container. Isso garante que, por padrão, os containers tenham redes independentes tanto do host quanto de outros containers, a menos que estejam configurados para compartilhar o mesmo NET namespace.


Esse isolamento é útil para segurança e organização, mas, sem configuração adicional, impede a comunicação entre containers e com o host hospedeiro. O Docker resolve esse problema criando uma rede bridge padrão ou redes personalizadas, essa rede bridge permite a comunicação controlada entre containers e acesso ao host hospedeiro ou à internet.

Normalmente a rede bridge do Docker se chama docker0.


O lado negativo é que, por padrão, todos os IPs externos têm permissão para se conectar às portas publicadas nos endereços do host pelo Docker. Isso pode dificultar o bloqueio ou a filtragem de pacotes pelo administrador, pois bloquear diretamente uma porta publicada na chain INPUT do iptables não surte efeito. O tráfego destinado aos containers é roteado pela chain FORWARD, ignorando a INPUT. Sem o devido cuidado, portas podem ficar expostas inadvertidamente.



Fluxo de processamento no Iptables sem Docker


O iptables é dividido em três níveis principais: tabelas, chains e regras/políticas. O fluxo de um pacote no sistema segue o caminho descrito abaixo:

  1. O pacote entra pela interface de rede
    O pacote é recebido pela interface de entrada e imediatamente analisado.

  2. O pacote passa pela Chain PREROUTING da tabela nat
    Esta etapa ocorre antes do roteamento. A Chain PREROUTING é usada para aplicar NAT (como DNAT) em pacotes que ainda não passaram pelo algoritmo de roteamento no kernel, o qual determinará o destino do pacote. Os pacotes processados aqui ainda não foram comparados com as chains INPUT ou FORWARD.

  3. O kernel realiza o roteamento
    Com base no destino do pacote, o kernel decide se ele será:

    • Entregue ao host localmente (Chain INPUT).
    • Encaminhado para outro destino (Chain FORWARD).
  4. Caminhos possíveis após o roteamento

    4.1. Pacotes destinados ao próprio host (local - INPUT)
    Passam pela Chain INPUT na tabela filter (responsável por políticas de aceitação ou bloqueio para o host). Após o processamento na Chain INPUT, o pacote é entregue ao kernel ou ao aplicativo apropriado.

    4.2. Pacotes a serem encaminhados (roteados)
    Passam pela Chain FORWARD na tabela filter. O tráfego roteado pode ser:

    • Redirecionado para outra interface.
    • Bloqueado ou permitido, com base nas regras na Chain FORWARD.
  5. Pacotes de saída originados no próprio host
    Para pacotes que o host envia, eles passam pela Chain OUTPUT na tabela filter. Podem ser processados pela tabela nat na Chain OUTPUT, aplicando SNAT ou outros tipos de NAT de saída.

  6. Pacotes na Chain POSTROUTING da tabela nat
    Todos os pacotes que saem do sistema passam pela Chain POSTROUTING. Aqui são aplicadas transformações de saída, como SNAT (Source NAT) ou mascaramento.


Normalmente, para bloquear a conexão de um IP externo, colocamos as regras de bloqueio na chain INPUT. Mas no Docker, isso não funciona, vamos entender o motivo.



Fluxo de processamento no Iptables com Docker


Para o exemplificar, vamos subir um container com Nginx, rodando na porta 80 e vamos publicar essa porta no host hospedeiro para criar um redirecionamento da porta 80 do host hospedeiro para a porta 80 do container.


Terminal
# Para iniciar o container com nginx:
╼ $ sudo docker run -d -p 80:80 --name webserver nginx

Abaixo segue um fluxo de tráfego resumido para entendermos como funciona o tráfego para um container. No nosso exemplo o IP do container é 172.17.0.2/32 e a porta é a 80.

  1. O pacote entra pela interface de rede
    O pacote é recebido pela interface de entrada e imediatamente analisado.

  2. O pacote passa pela Chain PREROUTING da tabela nat
    O Docker criou uma regra que todo tráfego destinado a um endereço local do host passa por essa regra. Essa regra faz com que o pacote seja encaminhado para a chain DOCKER, que contém as regras de redirecionamento configuradas pelo Docker.

    # Podemos ver a regra abaixo:
    -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
  3. O pacote entra na chain DOCKER na Tabela nat
    Se o tráfego corresponde a uma porta publicada pelo Docker (ex.: porta 80), essa regra aplica DNAT (Destination NAT), redirecionando o tráfego para o container com IP 172.17.0.2 na porta 80. Se houver um match, o pacote será processado pelo NAT e então encaminhado para o container via chain FORWARD.

    # Podemos ver a regra abaixo:
    -A DOCKER ! -i docker0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.17.0.2:80
  4. Se não houver match na chain DOCKER
    Se o tráfego não der match na regra da chain DOCKER, isso significa que o destino é o próprio host (não um container).
    O pacote sai da tabela nat e segue o fluxo normal do sistema, como: chain INPUT da tabela filter, para ser tratado como tráfego destinado ao host.


Como o Docker conecta a interface bridge (como a padrão docker0) ao gateway do host (como eth0, por exemplo) usando NAT, definir regras na chain INPUT para controlar o tráfego destinado aos containers é inútil. Isso ocorre porque o tráfego para os containers não passa pela chain INPUT. Em vez disso, ele é redirecionado pela chain PREROUTING na tabela nat e segue diretamente para a chain FORWARD na tabela filter, sem nunca ser processado pela chain INPUT da tabela Filter.


Mais detalhes

Para mais detalhes de como funciona, incluindo um diagrama, veja esse blog o diagrama exibido no blog pode ser visto aqui.



Como controlar o tráfego para os containers?


O Docker cria chains personalizadas no iptables, como DOCKER, DOCKER-USER e DOCKER-ISOLATION-STAGE-*, para gerenciar o tráfego dos containers. Essas chains são integradas ao fluxo de pacotes, garantindo que o tráfego destinado aos containers ou redes Docker seja processado pelas regras definidas nessas chains.


Normalmente, para bloquear conexões externas, adicionamos regras de bloqueio na chain INPUT. No entanto, no caso do Docker, isso não funciona. Como mostrado no fluxo de pacotes acima, o tráfego destinado aos containers não passa pela chain INPUT, mas segue diretamente para a chain FORWARD.


Com a política padrão da chain FORWARD é definida como DROP, todo o tráfego roteado é bloqueado, a menos que seja explicitamente permitido. A chain DOCKER-USER oferece ao administrador a possibilidade de aplicar políticas personalizadas antes das regras padrão configuradas pelo Docker.


A ordem de verificação depende da tabela e do fluxo do tráfego no iptables. Por exemplo:

  • A chain DOCKER-USER é processada antes das regras padrão do Docker na chain FORWARD.
  • A chain DOCKER é chamada pela chain FORWARD para controlar o roteamento de tráfego para os containers.

Uma forma de bloquear o acesso externo às portas 80 e 443, permitindo acesso apenas a partir do localhost, seria:


Terminal
╼ $ sudo iptables -I DOCKER-USER ! -d 127.0.0.0/8 -m tcp -p tcp -m multiport --dports 80,443 -j DROP

# '! -s 127.0.0.0/8': Bloqueia todo o tráfego que não tenha o localhost como origem.

A regra deve ficar no topo

É importante notar que a regra deve ser colocada no topo da chain DOCKER-USER da tabela filter. Isso ocorrer pelo parâmetro -I DOCKER-USER.


A regra apresentada bloqueia acessos externos às portas 80 e 443 e permite conexões do localhost. No entanto, é importante destacar que essa regra não restringe o acesso entre containers que compartilham a mesma rede Docker, apenas acesso externo. Uma forma de deixar isso persistente seria:


Terminal
# Instale o iptables-persistent:
╼ $ sudo apt install iptables-persistent

Nos arquivos /etc/iptables/rules.v* (exemplo abaixo é para rules.v4), faça o seguinte:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:DOCKER-USER - [0:0]
######################
# outras regras aqui #
######################
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-USER -m conntrack --ctstate INVALID -j DROP
-A DOCKER-USER -p tcp -m tcp -s 192.168.121.138 -m multiport --dports 80,443 -j ACCEPT
-A DOCKER-USER -p tcp -m tcp -j DROP
COMMIT

O importante é criar a chain :DOCKER-USER - [0:0] e adicionar as regras nessa chain, isso não vai impactar na regras do Docker e ainda vai nos possibilitar criar regras de controle de tráfego para os containers.


A regra abaixo aceita pacotes relacionados a conexões existentes, garantindo o tráfego de retorno para os containers.

-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

A regra abaixo bloqueia pacotes com estado inválido.

-A DOCKER-USER -m conntrack --ctstate INVALID -j DROP

A regra abaixo vai permitir tráfego TCP originado do IP 192.168.121.138 para as portas 80 e 443.

-A DOCKER-USER -p tcp -m tcp -s 192.168.121.138 -m multiport --dports 80,443 -j ACCEPT

Já a última regra vai bloqueia qualquer tráfego TCP que não atenda às regras anteriores.

-A DOCKER-USER -p tcp -m tcp -j DROP


Socat como alternativa


O socat pode ser utilizado como uma solução alternativa para controlar o tráfego destinado aos containers, evitando a necessidade de incluir as regras na Chain padrão do Docker. Ele funciona como uma ponte entre o tráfego de rede do host e o container, permitindo que as conexões passem pela cadeia INPUT da tabela filter no iptables. Com isso, as regras configuradas na cadeia INPUT poderão ser aplicadas ao tráfego para os containers, algo que o fluxo padrão do Docker não permite.


Para implementar essa solução, podemos criar um serviço no SystemD que redirecione o tráfego da interface externa do host hospedeiro para o localhost (loopback), onde o container estará ouvindo. Isso envolve publicar a porta do container apenas no localhost, em vez de expor diretamente para 0.0.0.0/0.


Essa abordagem é mais simples de configurar e pode ser mais controlada do que alterar diretamente as regras padrão do Docker. No entanto, ela é similar ao uso de um proxy reverso, já que o socat atua como intermediário para o tráfego entre o cliente e o container.


Crie o arquivo de serviço do systemd:


/etc/systemd/system/socat-docker-webserver.service
[Unit]
Description=Socat/Docker Web Server TCP Forwarding Service
After=network.target

[Service]
ExecStart=/usr/bin/socat -s -d -d TCP4-LISTEN:80,fork,bind=192.168.121.122 TCP4:127.0.0.1:80
Restart=always

[Install]
WantedBy=multi-user.target

explicação do comando socat
  • -s: Mostra mensagens detalhadas de status (útil para debugging).

  • -d -d: Aumenta o nível de detalhes de debug, exibindo informações mais completas no terminal.

  • TCP4-LISTEN:80: Cria um socket TCP IPv4 que escuta na porta 80.

    • fork: Permite múltiplas conexões simultâneas. Cada nova conexão cria um processo filho para gerenciar o tráfego, enquanto o processo pai continua escutando novas conexões.
    • bind=192.168.121.122: Amarra (bind) o listener ao endereço IP 192.168.121.122. Isso significa que o socat estará ouvindo conexões apenas neste endereço, e não em 0.0.0.0 (todos os endereços).
  • TCP4: Cria uma conexão TCP IPv4.

  • 127.0.0.1:80: Define o destino como o localhost (127.0.0.1) na porta 80.


Agora execute:


Terminal
# Recarregue as Units do systemd, para reconhecer o novo serviço:
╼ $ sudo systemctl daemon-reload

# Inicie o serviço:
╼ $ sudo systemctl start socat-docker-webserver.service


Agora vamos iniciar o container, lembre-se que ele deve publicar a porta 80 na interface de localhost para funcionar.


Terminal
# Para iniciar o container com nginx:
╼ $ sudo docker run -d -p 127.0.0.1:80:80 --name webserver nginx

Com tudo isso realizado, podemos aplicar os bloqueios ou liberações na chain INPUT da tabela Filter.