Artigos e tutoriais
   Docker - Conhecimento fundamental
Autor: Patrick Brandão, patrickbrandao@gmail.com
Cópia autorizada somente quando preservar a referência ao autor.




Introdução - O que é Docker

Eu sempre digo aos meus alunos e clientes que a melhor forma de entender uma tecnologia é entender primeiro qual problema ela veio resolver. Não tem como entender uma solução sem entender o problema a ser resolvido!

No final da década de 1990 (1998) houve uma inovação com a introdução da tecnologia de virtualização de sistemas operacionais.
A princípio, uma máquina virtual resolvia um problema: aplicações que deveriam ser isoladas em hardwares diferentes, aumentando o custo da infraestrutura, poderiam agora ser executadas no mesmo hardware (host) sobre uma camada de software responsável por emular um hardware - o hypervisor - criando uma VM (virtual machine - máquina virtual), o sistema operacional executado sobre uma VM tinha uma falsa sensação de estar no hardware real. O problema de custo foi resolvido, o mesmo hardware ocioso com uma aplicação poderia ser utilizado para hospedar vários sistemas operacionais isolados entre si.

Sem hypervisor:



Com hypervisor:



Com novas soluções vem novos problemas, com a proliferação de VMs numa infra temos que criar, manter, atualizar, auditar, fazer backup, recuperar backup, fazer inventário, descobrir porque ela parou de funcionar, monitorar via SNMP ou via protocolo específico de um sistema operacional, gerir usuários root/admin de cada uma, gerir autenticação centralizada de usuários, etc...

Em ambientes de desenvolvimento de software em equipes, qual seria a solução para isolar ambientes de testes dos programadores? Dar um super-hardware para cada um? Criar uma VM para cada desenvolvedor ou criar um usuário para cada um num servidor de teste? Dar ou não privilégios de root/sudo para eles? E se algum dedo gordo danificar o sistema? Todos os desenvolvedores ficam sem programar até o administrador reparar o sistema?

Num datacenter, gerir produtos, serviços, tráfego, monitoramentos, devemos criar uma VM para cada software ou deixar vários softwares numa mesma VM correndo o risco deles se agredirem?

Em provedores de Internet, é muito comum encontrar uma virtualização onde cada serviço tem sua VM: DNS Recursivo 1, DNS Recursivo 2, DNS autoritativo 1, DNS autoritativo 2, software de gestão/Radius, software de monitoramento, software de firewall, software de VPN, software de gestão de produtos específicos, e a lista vai crescendo. Para cada software cria-se uma máquina virtual.

Uma máquina virtual tem um defeito grave: cada sistema operacional tem seu pre-requisito mínimo de CPU, tamanho de disco, memória RAM, rede, CD/DVD.

Visualize: faz sentido alocar 10G de disco, 2G de ram, 2 núcleos para uma VM de DNS que mal consome 1/3 disso na maior parte do tempo?
Se você precisar criar 100 servidores DNS para uma solução de larga escala (1 milhão de requisições), você vai criar 100 VMs alocando esse hardware todo? Quanto vai custar tal servidor com esses recursos?

A solução que precisamos deve:

  • Isolar o ambiente de uma aplicação de outras sobre o mesmo hardware (físico ou virtual);
  • Alocar apenas o espaço em disco para a aplicação e suas dependências;
  • Limitar memória e CPU para o processo em sí;
  • Alocar apenas a porta TCP/UDP necessária para comunicação com o serviço na rede;

Antes de existirem máquinas virtuais havia uma solução simplória chamada CHROOT, feita para sistemas Unix (Linux e afins). A ideia do CHROOT era executar um software isolando-o num sistema de arquivos falso. Vou explicar essa parte com detalhes.

Quando você roda qualquer comando no Linux, antes da executar o software em sí, o Kernel prepara o ambiente para executá-lo, o que inclui herdar variáveis de ambiente (ENV, veremos mais adiante), determinar o PPID (PID do processo pai), determinar o usuário e grupo do processo, estabelecer prioridade de tempo de CPU (+/- NICE), estabelecer espaço de memória de pilha, definir ponteiros para arquivos (STDIN, STDOUT, STDERR), entre outros preparativos. Feito isso o processo começa a ser executado: o kernel analisa cada instrução do software como se fosse uma receita de bolo.

Infelizmente, se um processo é explorado por um atacante (bugs e exploits), tudo que o software tem acesso estará vulnerável. O atacante pode começar lendo o arquivo /etc/passwd para descobrir usuários do sistema, pode ler arquivos comuns em busca de pistas para ganhar mais acessos e privilégios especiais. Controle remoto e vazamento de dados acontecem assim.

A idéia do CHROOT era instruir o kernel a executar o processo dentro de uma jaula de arquivos. Para isso era necessário que você colocasse em uma pasta todos os arquivos que o software precisa para rodar. Tomando a pasta /tmp/jaula01 como base de uma jaula para rodar um software de DNS como o Bind9, você precisaria copiar o binário do Bind9 (/usr/sbin/named) para /tmp/jaula01/usr/sbin/named, e copiar todas as bibliotecas da qual o named precisa para rodar. Observe:

root@labserver:~# ldd /usr/sbin/named
	linux-vdso.so.1 
	liblwres.so.161 => /lib/x86_64-linux-gnu/liblwres.so.161 
	libdns.so.1104 => /lib/x86_64-linux-gnu/libdns.so.1104 
	libgssapi_krb5.so.2 => /lib/x86_64-linux-gnu/libgssapi_krb5.so.2 
	libkrb5.so.3 => /lib/x86_64-linux-gnu/libkrb5.so.3 
	libk5crypto.so.3 => /lib/x86_64-linux-gnu/libk5crypto.so.3 
	libcom_err.so.2 => /lib/x86_64-linux-gnu/libcom_err.so.2 
	libcrypto.so.1.1 => /lib/x86_64-linux-gnu/libcrypto.so.1.1 
	libbind9.so.161 => /lib/x86_64-linux-gnu/libbind9.so.161 
	libisccfg.so.163 => /lib/x86_64-linux-gnu/libisccfg.so.163 
	libisccc.so.161 => /lib/x86_64-linux-gnu/libisccc.so.161 
	libisc.so.1100 => /lib/x86_64-linux-gnu/libisc.so.1100 
	libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 
	libprotobuf-c.so.1 => /lib/x86_64-linux-gnu/libprotobuf-c.so.1 
	libfstrm.so.0 => /lib/x86_64-linux-gnu/libfstrm.so.0 
	libcap.so.2 => /lib/x86_64-linux-gnu/libcap.so.2 
	libjson-c.so.3 => /lib/x86_64-linux-gnu/libjson-c.so.3 
	liblmdb.so.0 => /lib/x86_64-linux-gnu/liblmdb.so.0 
	libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 
	libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 
	libGeoIP.so.1 => /lib/x86_64-linux-gnu/libGeoIP.so.1 
	libxml2.so.2 => /lib/x86_64-linux-gnu/libxml2.so.2 
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 
	libkrb5support.so.0 => /lib/x86_64-linux-gnu/libkrb5support.so.0 
	libkeyutils.so.1 => /lib/x86_64-linux-gnu/libkeyutils.so.1 
	libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2 
	/lib64/ld-linux-x86-64.so.2 
	libicui18n.so.63 => /lib/x86_64-linux-gnu/libicui18n.so.63 
	libicuuc.so.63 => /lib/x86_64-linux-gnu/libicuuc.so.63 
	libicudata.so.63 => /lib/x86_64-linux-gnu/libicudata.so.63 
	libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 
	liblzma.so.5 => /lib/x86_64-linux-gnu/liblzma.so.5 
	libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 
	libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 

Após um longo trabalho de copiar todas essas bibliotecas, criar a pasta /etc/ dentro da jaula com alguns arquivos falsos (ld.so.conf, /etc/passwd, /etc/shadow), criar a pasta /dev/ com alguns ponteiros básicos, copiar os arquivos de configuração (/etc/bind/named.conf entre outros), sua jaula estará pronta.


root@labserver:~# chroot /tmp/jaula01 /usr/sbin/named

Se o atacante tomar o controle do software e solicitar a leitura do arquivo /etc/passwd, ele estará na verdade lendo o arquivo /tmp/jaula01/etc/passwd.

O CHROOT é um bom exemplo de isolamento de sistema de arquivos e era somente isso que ele provia. Ele não era capaz de prover outros níveis de isolamente, o software continuava num ambiente comum de processos, podendo acessar a lista de processos (ps ax), acessar a rede IP onde o servidor se encontrava, enviar sinais para outros processos, acessar uma vasta gama de recursos do kernel que não envolvem o sistema de arquivos.

O que é melhor que uma jaula? Um container.

O kernel Linux foi aperfeiçoado para permitir que os demais recursos providos a um processos pudessem ser isolados. As ferramentas LXD, LXC e LXCFS permitiam a criação de um ambiente onde o processo era executado totalmente isolado graças a um recurso do kernel chamado cgroups.

Antes de rodar um processo, o kernel agora prepara um ambiente tão isolado que se assemelha a uma máquina virtual: uma interface de rede com MAC, IP e gateway, um sistema de arquivos muito mais sofisticado que uma jaula, informações de sistema separados para o container (lscpu, free, lspci, etc...). O melhor disso tudo é que o kernel não precisa criar uma camada de software para simular um hardware.

Um software que está rodando em um container em nada difere do mesmo software rodando numa máquina virtual.

Há um pequeno problema: para rodar um software em um ambiente CGROUPS, você ainda precisa prover os arquivos FHS (/bin, /etc, /lib, /dev, ...). Essa parte chata e cansativa ainda era necessária. Depois de um tempo passando raiva descobrindo os arquivos que faltavam, você acaba fazendo o seu "kit jaula", mas se todos os usuários de containers precisassem passar por isso, pouca gente utilizaria containers hoje.

Se eu gosto de usar Debian, eu posso criar uma pasta contendo tudo que ele precisa para rodar qualquer binário em uma jaula ou container, e a cada software novo eu usaria meu pacote pronto e só acrescentaria um novo binário. Essa seria uma definição primordial de uma IMAGEM de um sistema Debian para jaulas e containers.

Scripts facilitadores e pacotes contendo as imagens de diferentes sistemas fariam toda diferença no dia-a-dia e é nesse ponto que surge o Docker.

Inicialmente o Docker foi criado como um toolkit auxiliar o LXD/LXC e a medida que foi evoluindo ele deixou de depender deles para conter todas as bibliotecas e interfaces com o kernel para criar ambientes CGROUPS.

Exemplo de container com isolamento de arquivos completos:



Alem do isolamento de arquivos, temos o isolamento de rede. Por padrão o docker cria uma rede chamada "docker0" (é uma bridge) na faixa 172.17.0.0/16, o primeiro IP dessa sub-rede - 172.17.0.1 - é o gateway padrão de todo container. No host é criado uma interface VETH (virtual ethernet) e adicionada a bridge, dentro do container essa veth é apresentada como eth0 com um ip 172.17.0.2 em diante.







Instalação Docker

Usaremos como base o Debian 10.5

Esteja logado como ROOT (comando: su -) para executar os comandos abaixo:


    # Instalando ferramentas
    apt-get -y update
    apt-get -y install bridge-utils tcpdump curl mc htop

    # Adicionar repositorio docker
    GPGURL=https://download.docker.com/linux/ubuntu/gpg
    APTURL=https://download.docker.com/linux/debian
    curl -fsSL $GPGURL | apt-key add -
    add-apt-repository "deb [arch=amd64] $APTURL $(lsb_release -cs) stable"

    # Atualizando indice
    apt-get -y update
    apt-get -y upgrade
    apt -y autoremove


    # Instalando docker
    apt-get -y install docker-ce


Caso encontre algum erro durante a instalação via APT, tente instalar via script oficial:


    # Instalando docker via script oficial
    curl -fsSL get.docker.com -o /root/get-docker.sh
    sh /root/get-docker.sh


Execute::

root@labserver:~# docker ps -a
CONTAINER ID     IMAGE     COMMAND     CREATED     STATUS     PORTS     NAMES
root@labserver:~# 

Se o resultado do comando docker ps -a foi como acima, o software está 100% operacional.






Primeiros passos em Docker

Antes de aprofundar nos detalhes do uso de containers em Docker, vou dar alguns exemplos para fixar as regras envolvidas no seu uso.

Regra 1 - Um container é uma cópia de uma imagem em camada transparente (overlay).

Não temos nenhuma imagem em nosso inventário local. Observe:


root@labserver:~# docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
root@labserver:~# 

Para que um container seja criado, o Docker deve pegar uma imagem e descompacta-la em um diretório, para em seguida criar uma jaula CGROUP com ela.
Normalmente esses arquivos ficam em /var/lib/docker/overlay2 (não mexa lá, a menos que seja emergencia).

Para obter uma imagem, você pode:

  • Criar uma imagem do zero, vou ensinar no final do artigo;
  • Usar uma imagem pública do hub.docker.com, método mais comum;
  • Criar uma imagem a partir de outra imagem (vou ensinar mais adiante);
  • Usar uma imagem a partir de um repositório de terceiros (privado ou público).

Cada imagem tem uma identidade baseada na assinatura SHA-256 do arquivo da imagem.

Uma imagem em uso por algum container não pode ser deletada, assim a melhor forma de alterar imagens é criar novas versões. No Docker as versões são conhecidas como TAGs, que podem ser nomes ou números.

Caso você tente obter uma imagem sem informar a TAG, o Docker assume a palavra "latest" (mais recente) como TAG padrão.

Obtendo a imagem do Debian:


root@labserver:~# docker pull debian

Using default tag: latest
latest: Pulling from library/debian
d6ff36c9ec48: Downloading [=============>                                     ]  13.29MB/50.4MB
Digest: sha256:1e74c92df240634a39d050a5e23fb18f45df30846bb222f543414da180b47a5d
Status: Downloaded newer image for debian:latest
docker.io/library/debian:latest

root@labserver:~# docker image ls

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
debian              latest              ee11c54e6bb7        12 days ago         114MB

root@labserver:~# docker pull debian:10.5

10.5: Pulling from library/debian
Digest: sha256:1e74c92df240634a39d050a5e23fb18f45df30846bb222f543414da180b47a5d
Status: Downloaded newer image for debian:10.5
docker.io/library/debian:10.5

root@labserver:~# docker image ls

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
debian              10.5                ee11c54e6bb7        12 days ago         114MB
debian              latest              ee11c54e6bb7        12 days ago         114MB

root@labserver:~# 

No momento em que escrevo esse artigo, a última versão do Debian é a 10.5, portanto, quando eu baixei a imagem "debian" sem informar a tag, a tag latest fazia referencia à última imagem disponível (10.5). Quando tentei obter especificamente a imagem debian tag 10.5 a assinatura era igual, ele adicionou o novo registro no repositório mas não repetiu o download, visto que a assinatura SHA256 da imagem debian:latest é a mesma de debian:10.5.

Ao remover uma imagem que possui duas referencias, ela só é realmente apagada quando você remover a última referencia:


root@labserver:~# docker rmi debian:10.5

Untagged: debian:10.5

root@labserver:~# docker rmi debian:latest

Untagged: debian:latest
Untagged: debian@sha256:1e74c92df240634a39d050a5e23fb18f45df30846bb222f543414da180b47a5d
Deleted: sha256:ee11c54e6bb7b1c022d7a43aaee00eac776367a7a1adb7746a79757fae175a5e
Deleted: sha256:0ced13fcf9441aea6c4ee1defc1549772aa2df72017588a1e05bc11dd30b97b6

Observe que ao remover definitivamente a imagem debian:latest duas outras imagens foram removidas. Isso ocorre porque a imagem é construída em camadas num sistema de arquivos chamado AUFS (baseada em unionfs). Vou deixar para explicar como as camadas funcionam e qual problema ela resolve mais adiante.

A definição de camada transparente - overlay - vem do fato de que uma imagem é composta de camadas.

Toda imagem é construída do zero (FROM scratch) e novas camadas são adicionadas acima dessa.

Observe o Dockerfile (vou ensinar mais a frente) do Debian 10.5:

# Arquivo Dockerfile

ROM scratch
ADD rootfs.tar.xz /
RUN apt-get -y update
CMD ["bash"]



Quando você executa um container baseado numa imagem, o Docker monta a imagem numa pasta e cria uma "camada transparente" do container em execução.
Você pode imaginar que ao rodar 10 containers baseados na mesma imagem que haverá 10x mais alocação de espaço em disco, errado. A grande novidade é que isso não ocorre.


Todos os arquivos da imagem são exclusivos de cada container em modo leitura, mas se você alterar qualquer arquivo uma cópia desse arquivo é puxada para a camada superior e armazenada lá, assim, somente há alocação (duplicação) de espaço quando você violar a imagem (camada inferior).


Quando algum arquivo é alterado, sua cópia na camada do container passa a ser definitiva, o arquivo original na camada de baixo deixa de ser acessível mesmo que você remova o arquivo alterado dentro do container.

Temos aqui uma recomendação muito importante: não altere os arquivos da imagem, o capítulo de montagens e volumes vão mostrar como trabalhar com arquivos dentro do container sem violar esse princípio, que podem levar você a ficar sem disco!



Regra 2 - Todo container precisa do seu processo principal (PID 1) em execução para sustentar sua existência

O Docker possui alguns comandos principais:
  • Baixar imagem: docker pull ...
  • Criar um container: docker create ...
  • Criar e executar um container: docker run ...
  • Interromper a execução de um container: docker stop ...
  • Iniciar um container parado: docker start ...
  • Remover um container parado: docker rm ...

Vale informar que se você tentar usar uma imagem que não existe, o Docker tentará achá-la em algum repositório para baixá-la automaticamente, por conta disso quase não se usa o docker pull no dia-a-dia. É bem raro ver alguém usando docker create pois o comando docker run é a união de create + start.
É mais raro ainda ver alguém usando docker stop seguido de docker rm pois o comando docker -f rm realiza essas duas tarefas (parar e remover).

Vamos criar um container de teste do Debian 10.5 para ver todas as etapas do Docker na prática. Observe:


docker pull debian:10.5

10.5: Pulling from library/debian
d6ff36c9ec48: Downloading [===================>                        ]  22.53MB/50.4MB
d6ff36c9ec48: Extracting [======================================>      ]  38.84MB/50.4MB
d6ff36c9ec48: Pull complete 
Digest: sha256:1e74c92df240634a39d050a5e23fb18f45df30846bb222f543414da180b47a5d
Status: Downloaded newer image for debian:10.5
docker.io/library/debian:10.5

docker image ls

debian              10.5                ee11c54e6bb7        12 days ago         114MB



docker create --name=debian01 -h debian-test.intranet.br debian:10.5 sleep 99

9446e46ec48705f64ae310619f1cbe23dbe85363b0d253bfe4f9414df607c608

docker ps -a

CONTAINER ID  IMAGE       COMMAND     CREATED         STATUS   PORTS    NAMES
9446e46ec487  debian:10.5 "sleep 99"  34 seconds ago  Created           debian01


docker start debian01

debian01

docker ps -a

CONTAINER ID  IMAGE       COMMAND     CREATED        STATUS        NAMES
9446e46ec487  debian:10.5 "sleep 99"  3 minutes ago  Up 1 minute   debian01

#  99 segundos depois ... 

docker ps -a

CONTAINER ID  IMAGE       COMMAND     CREATED        STATUS                      NAMES
9446e46ec487  debian:10.5 "sleep 99"  5 minutes ago  Exited (0) 10 seconds ago   debian01

docker rm debian01

debian01

O que aconteceu acima:

  • 1 - Baixamos a imagem com docker pull
  • 2 - Criamos um container com docker create, informando que ele deve rodar em ambiente Debian 10.5 e rodar o comando sleep 99, o container foi criado mas não foi executado.
  • 3 - Com o comando docker ps -a listamos todos os containers presentes, o argumento -a exibe até mesmo os containers parados. Observe a coluna STATUS onde informa "created".
  • 4 - Iniciamos o container com o comando docker start
  • 5 - Ao listar os processos com docker ps -a, o container estava em execução normalmente.
  • 6 - O comando rodando dentro do container ficará 99 segundos sem fazer nada e em seguida termina. Como o container não existe sem seu processo principal o container é encerrado (todo o ambiente CGROUPS é desmontado pelo Docker).
  • 7 - Uma vez parado, o comando docker rm destruiu o container, tudo dentro dele deixou de existir, a pasta em que a imagem foi descompactada deixou de existir.
  • Tudo que fizemos acima poderia ser feito com um único comando:

    
    docker run -d --rm --name=debian02 -h debian-test.intranet.br debian:10.5 sleep 99
    
    6b4b177ddcc134bbc9e5b85eebd4004ee284c9932f9d728317f83174481cfac1
    
    

    Explicando: docker run é a união de docker create + docker start, -d executa o container em background, --rm informa ao docker para destruir o container quando ele encerrar a atividade.

    Olha esse exemplo interessante: vamos rodar um container auto-destrutivo, vamos rodar um comando que exibe sua configuração IP:

    
    docker run --rm debian:10.5 ip addr show
    
    1: lo:  mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
        inet 127.0.0.1/8 scope host lo
           valid_lft forever preferred_lft forever
    17: eth0@if18:  mtu 1500 qdisc noqueue state UP group default 
        link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
        inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
           valid_lft forever preferred_lft forever
    
    docker ps -a
    
    CONTAINER ID  IMAGE       COMMAND     CREATED        STATUS        NAMES
    
    

    Não rodamos o container em background, não informamos nome de container nem nome de host para ele, informamos o Docker que o container deve ser destruído após sua tarefa, e informamos como tarefa o comando que exibe a configuração IP de dentro do container.

    Ao listar os containers em execução nada aparece, pois era um container "Mr. Meeseeks" (se você não entende a referência, procure no Google).

    Containers auto-destrutivos são muito utilizados por programadores para testar código, executar tarefas específicas em que somente o resultado importa, todo o resto é descartável.

    Vou criar um container auto-destrutivo que executará um sleep de 32 anos. Em seguida vamos entrar no container para ver como ele é.

    
    docker run -d --rm --name=debian03 debian:10.5  sleep 1009152000
    
    docker exec -it  debian03 bash
    
    root@ca61b8b430e1:/# 
    root@ca61b8b430e1:/# ls
    
    bin  boot  dev	etc  home  lib	lib64  media  mnt  opt	proc  root  run  sbin  srv  sys  tmp  usr  var
    
    root@ca61b8b430e1:/# ip addr show
    
    1: lo:  mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
        inet 127.0.0.1/8 scope host lo
           valid_lft forever preferred_lft forever
    25: eth0@if26:  mtu 1500 qdisc noqueue state UP group default 
        link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
        inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
           valid_lft forever preferred_lft forever
    
    root@ca61b8b430e1:/# ping -c 4 8.8.8.8   
    
    PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
    64 bytes from 8.8.8.8: icmp_seq=1 ttl=114 time=35.4 ms
    64 bytes from 8.8.8.8: icmp_seq=2 ttl=114 time=28.7 ms
    64 bytes from 8.8.8.8: icmp_seq=3 ttl=114 time=24.2 ms
    64 bytes from 8.8.8.8: icmp_seq=4 ttl=114 time=29.0 ms
    
    --- 8.8.8.8 ping statistics ---
    4 packets transmitted, 4 received, 0% packet loss, time 13ms
    rtt min/avg/max/mdev = 24.180/29.318/35.409/4.003 ms
    
    root@ca61b8b430e1:/# exit
    
    docker stop debian03
    
    

    O comando docker exec serve parar rodar algum programa adicional dentro do container, no dia-a-dia ele é comumente usado para entrar no container (quando vc roda algum programa de shell, como o bash ou vtysh) ou para obter algum relatório do que está acontecendo lá dentro (ps ax, ping, free, ...).
    Entramos no container e temos o mesmo ambiente de um Linux Debian normal, com instalação mínima.
    Saímos do container com o exit e em seguida paramos o container com docker stop, como ele é auto-destrutivo o Docker não só parou como também exterminou tudo do container.

    Imagino que você esteja ansioso para criar sistemas, rodar muitos programas em Docker, mas continue o tutorial até o final para aprender tudo.





    Camada de rede em Docker

    Tipo 1 - Redes bridge

    Quase todo mundo usa o Docker sem precisar aprender muitos detalhes da parte de redes (docker network) por um simples motivo: todo container é associado a rede "bridge" (docker0) por padrão. A rede "bridge" é uma rede virtual de camada 2 rodando no prefixo 172.17.0.0/16, o primeiro IP (172.17.0.1/16) é associado ao host para servir de gateway padrão para todos os containers.


    Por padrão, toda rede criado no docker, incluindo a rede nativa "bridge" possui isolamento entre containers a nível de rede para que uma rede docker não se comunique com outra rede docker no mesmo host, a menos que você explicitamente permita isso (publicação de portas TCP e UDP).

    docker network create mynet -d bridge --subnet 10.90.0.0/16
    
    7edc2615687a58ea561505e4eddcaa978ae930fbe5231294f45c1fae63d1d06b
    
    docker network ls
    
    NETWORK ID          NAME                DRIVER              SCOPE
    41d27617c1a4        bridge              bridge              local
    06af2efcbcc5        host                host                local
    7edc2615687a        mynet               bridge              local
    7033a35b727e        none                null                local
    
    

    Vamos criar alguns containers para teste e aproveitar para explorar novos argumentos:

    
    docker run -d --name=Container-01 debian:10.5  sleep 1009152000
    docker run -d --name=Container-02 debian:10.5  sleep 1009152000
    
    docker run -d --name=Container-11 --network mynet debian:10.5  sleep 1009152000
    docker run -d --name=Container-12 --network mynet debian:10.5  sleep 1009152000
    
    

    Resultado:


    O firewall padrão (iptables / nftables) impede que a comunicação entre redes. Os containers Container-01 e Container-02 podem se comunicar via rede IP (ping 172.17.0.2, ping 172.17.0.3) e os containers Container-11 e Container-12 podem se comunicar (ping 10.90.0.2, ping 10.90.0.3), mas a comunicação IP entre a rede 172.17.0.0/16 e 10.90.0.0/16 será bloqueada via firewall.


    Para que uma rede possa se comunicar com outra, você pode desativar o firewall do docker (nada normal) ou interferir nas regras de firewall (nada normal tambem).

    Publicar uma porta faz com que o Docker crie um redirecionamento de portas do ip do host (nosso Linux onde o Docker está instalado) e o ip do container.

    Vamos rodar um container de um servidor Web Apache2 (detalhes em https://hub.docker.com/_/httpd), exemplo:

    
    docker run -d --name=Apache-01 -p 8091:80/tcp httpd:2.4
    
    docker run -d --name=Apache-11 -p 8092:80/tcp --network mynet httpd:2.4
    
    
    docker ps -a
    
    CONTAINER ID  IMAGE      COMMAND   CREATED   STATUS   PORTS                 NAMES
    5bb9b04c6afb  httpd:2.4  "htt..."  1min ago  Up 1min  0.0.0.0:8092->80/tcp  Apache-11
    1088317fdcbf  httpd:2.4  "htt..."  2min ago  Up 2min  0.0.0.0:8091->80/tcp  Apache-01
    
    

    Se você acessar o IP do seu Linux na porta 8091 ou 8092 (http://x.x.x.x:8092) você observará a página "It works!"


    Outro detalhe sobre redes em Docker é o NAT. Por padrão toda rede sofre NAT na saída para Internet, onde o IP de origem do container é substituído pelo IP da interface de rede do host.


    Redes IPv4 no Docker sem NAT são usadas para dar IPs públicos direto aos containers, embora não seja uma prática comum. Para criar uma rede Docker sem NAT, observe os exemplo:

    docker network create freenet1  \
         -d bridge --gateway=45.255.129.1 --subnet=45.255.129.0/26 \
         -o "com.docker.network.bridge.enable_icc"="true" \
         -o "com.docker.network.driver.mtu"="1500" \
         -o "com.docker.network.bridge.name"="brfreenet1" \
         -o "com.docker.network.bridge.enable_ip_masquerade"="false"
    
    

    O uso de IPv6 não é padrão do Docker, e por padrão as redes com IPv6 não sofrem NAT MASQUERADE na saída, podemos criar uma rede com IPv6 assim:

    docker network create freenet2  \
         -d bridge \
         --gateway=45.255.130.1 --subnet=45.255.130.0/26 \
         --ipv6 \
         --subnet=2804:cafe:45ff:8100::/64 \
         --gateway=2804:cafe:45ff:8100::1 \
         -o "com.docker.network.bridge.enable_icc"="true" \
         -o "com.docker.network.driver.mtu"="1500" \
         -o "com.docker.network.bridge.name"="brfreenet2" \
         -o "com.docker.network.bridge.enable_ip_masquerade"="false"
    
    

    Caso você tenha IPv6 no seu host mas não tenha prefixos IPv6 para utilizar dentro do Docker, você pode criar uma rede IPv6 privada e fazer NAT MASQUERADE dela, exemplo:

    docker network create freenet3  \
         -d bridge \
         --gateway=10.3.3.1 --subnet=10.3.3.0/24 \
         --ipv6 \
         --subnet=2001:db8:1000:3300::/64 \
         --gateway=2001:db8:1000:3300::1 \
         -o "com.docker.network.bridge.enable_icc"="true" \
         -o "com.docker.network.driver.mtu"="1500" \
         -o "com.docker.network.bridge.name"="brfreenet3" \
         -o "com.docker.network.bridge.enable_ip_masquerade"="false"
    
    
    ip6tables -t nat -A POSTROUTING -s 2001:db8:1000:3300::/64 -j MASQUERADE
    
    

    Tipo 2 - Redes MACVLAN

    Até agora trabalhamos apenas com redes do tipo "bridge" (driver bridge, -d bridge), um driver interessante é o MACVLAN, ele não usa VLAN em sí (tag ethernet 802.1q), ele cria um MAC address virtual na interface de rede do host para representar o container na rede externa do host.

    
    docker network create macvlan0  \
         -d macvlan -o parent=eth0 \
         --gateway=172.19.2.1 --subnet=172.19.2.0/24 --ip-range=172.19.2.224/28
    
    

    
    docker run -d --name=Container-MV01 --network macvlan0 debian:10.5  sleep 1009152000
    
    


    O uso de redes MACVLAN deve ser acompanhada de um controle do uso dos endereços IP, sem especificar um RANGE o Docker irá alocar os IPs para os containers sequencialmente, podendo provocar conflitos de IPs na rede local. No exemplo acima eu tomei o cuidado de especificar o range (--ip-range) de IPs que o Docker poderá usar nos containers criados nessa rede para evitar conflitos com minhas rede local com DHCP.


    Tipo 3 - Rede HOST

    Uma terceira forma de colocar containers em rede é não utilizando nenhuma rede Docker, e simplesmente rodar o container na rede "host", nesse caso o container não terá um IP específico e passará a rodar na camada de rede do proprio Linux host.

    
    docker run -d --name=Container-Host --network host debian:10.5  sleep 1009152000
    
    


    Nota: ao usar redes MACVLAN em máquinas virtuais você pode ter problemas com o controle de MAC do Hypervisor, no VMWARE ESXI você precisa ativar a permissão para clonagem de MAC na PortGroup ou no vSwitch.



    Tipo 4 - Sem rede

    A última forma de rodar um container é na rede "none", onde nenhuma rede será associada ao container.

    Para rodar containers sem rede, use:

    
    docker run -d --name=Container-SemRede --network none debian:10.5  sleep 1009152000
    
    

    Criando imagens próprias

    Quase todo aventureiro em Docker cai no erro de rodar um container, entrar nele e instalar um monte de programas, para, muito tempo depois descobrir que estava fazendo tudo da forma mais difícil possível.

    Devido a natureza COPY-ON-WRITE das camadas transparentes (overlay) do sistema de arquivos usado nas imagens, criar e modificar arquivos dentro do container é uma tarefa fatal: aumenta o uso de espaço em disco e cria processamento adicionais no acesso dos arquivos, visto que para encontrar um arquivo o Linux precisa percorrer todas as camadas da última até a primeira.

    Para evitar isso, sua imagem personalizada deve conter todos os programas que você vai precisar para rodar sua aplicação num mínimo de camadas possíveis (o limite é de 127 camadas).

    Para criar uma imagem personalizada você vai precisar criar um projeto (pasta) contendo um arquivo chamado Dockerfile contendo algumas das seguindos instruções:

    • FROM: especifica a imagem base que você irá usar, todas as camadas que você criar serão empilhadas acima da última camada da imagem base;
    • ENV: Cria uma ou várias variáveis de ambiente, essas variáveis são usadas para mudar o comportamento e guiar seus programas, não são todos os softwares que fazem uso de variáveis de ambiente, mas a maioria costuma usar;
    • COPY: Copia arquivos e diretórios da pasta de projeto para dentro da imagem;
    • ADD: Adiciona arquivos e diretórios dentro da imagem, semelhante ao comando COPY. Possui suporte a adição de arquivos compactados, cujo conteúdo será descompactado dentro da imagem;
    • RUN: O mais importante de todos, esse comando executa um programa dentro da camada em construção;
    • ENTRYPOINT: Define a linha de comando de ponto de entrada, ou seja, o comando que será executado quando um container for construído usando essa nova imagem;
    • CMD: Semelhante ao ENTRYPOINT, define um comando a ser executado durante a inicialização do container, no entando, se o ENTRYPOINT estiver definido esse comando não é executado, mas a linha de comando CMD é informada como argumento da linha ENTRYPOINT;

    Você pode criar uma imagem num único comando:

    docker build -t imagem-rapida:001 -t imagem-rapida:latest - <<EOF
    FROM debian:latest
    RUN apt-get -y update && apt-get -y upgrade && apt-get -y install procps
    CMD ["sleep","99887766"]
    EOF
    
    

    Ou pode criar uma pasta contendo um arquivo Dockerfile, que é o método mais profissional e compatível com trabalho em equipe. Usaremos o método com Dockerfile.

    Algumas imagens, como a do Debian:10.5 são construídas sem ENTRYPOINT e sem CMD, o que nos obriga a informar o comando de ENTRYPOINT no final do comando "docker run".

    Para esclarecer a diferença entre ENTRYPOINT e CMD, vou dar 3 exemplos bem claros.

    1 - Imagem "minha-imagem-001" usando CMD

    mkdir /tmp/minha-imagem-001
    cd /tmp/minha-imagem-001
    touch Dockerfile
    

    # Arquivo /tmp/minha-imagem-001/Dockerfile
    
    FROM debian:10.5
    RUN apt-get -y update && apt-get -y upgrade && apt-get -y install procps
    CMD ["sleep","99887766"]
    

    docker build -t minha-imagem:001 -t minha-imagem:latest /tmp/minha-imagem-001
    
    
    Sending build context to Docker daemon  2.048kB
    Step 1/3 : FROM debian:10.5
     ---> ee11c54e6bb7
    Step 2/3 : RUN apt-get -y update && apt-get -y upgrade && apt-get -y install procps
     ---> Running in df945d8776f1
    Get:1 http://security.debian.org/debian-security buster/updates InRelease [65.4 kB]
    Get:2 http://deb.debian.org/debian buster InRelease [122 kB]
    Get:3 http://security.debian.org/debian-security buster/updates/main amd64 Packages [226 kB]
    Get:4 http://deb.debian.org/debian buster-updates InRelease [51.9 kB]
    Get:5 http://deb.debian.org/debian buster/main amd64 Packages [7906 kB]
    Get:6 http://deb.debian.org/debian buster-updates/main amd64 Packages [7868 B]
    Fetched 8379 kB in 3s (2636 kB/s)
    Reading package lists...
    Reading package lists...
    Building dependency tree...
    Reading state information...
    Calculating upgrade...
    0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
    Reading package lists...
    Building dependency tree...
    Reading state information...
    The following additional packages will be installed:
      libgpm2 libncurses6 libprocps7 lsb-base psmisc
    Suggested packages:
      gpm
    The following NEW packages will be installed:
      libgpm2 libncurses6 libprocps7 lsb-base procps psmisc
    0 upgraded, 6 newly installed, 0 to remove and 0 not upgraded.
    Need to get 612 kB of archives.
    After this operation, 1981 kB of additional disk space will be used.
    Get:1 http://deb.debian.org/debian buster/main amd64 libncurses6 amd64 6.1+20181013-2+deb10u2 [102 kB]
    Get:2 http://deb.debian.org/debian buster/main amd64 libprocps7 amd64 2:3.3.15-2 [61.7 kB]
    Get:3 http://deb.debian.org/debian buster/main amd64 lsb-base all 10.2019051400 [28.4 kB]
    Get:4 http://deb.debian.org/debian buster/main amd64 procps amd64 2:3.3.15-2 [259 kB]
    Get:5 http://deb.debian.org/debian buster/main amd64 libgpm2 amd64 1.20.7-5 [35.1 kB]
    Get:6 http://deb.debian.org/debian buster/main amd64 psmisc amd64 23.2-1 [126 kB]
    debconf: delaying package configuration, since apt-utils is not installed
    Fetched 612 kB in 1s (1176 kB/s)
    Selecting previously unselected package libncurses6:amd64.
    (Reading database ... 6677 files and directories currently installed.)
    Preparing to unpack .../0-libncurses6_6.1+20181013-2+deb10u2_amd64.deb ...
    Unpacking libncurses6:amd64 (6.1+20181013-2+deb10u2) ...
    Selecting previously unselected package libprocps7:amd64.
    Preparing to unpack .../1-libprocps7_2%3a3.3.15-2_amd64.deb ...
    Unpacking libprocps7:amd64 (2:3.3.15-2) ...
    Selecting previously unselected package lsb-base.
    Preparing to unpack .../2-lsb-base_10.2019051400_all.deb ...
    Unpacking lsb-base (10.2019051400) ...
    Selecting previously unselected package procps.
    Preparing to unpack .../3-procps_2%3a3.3.15-2_amd64.deb ...
    Unpacking procps (2:3.3.15-2) ...
    Selecting previously unselected package libgpm2:amd64.
    Preparing to unpack .../4-libgpm2_1.20.7-5_amd64.deb ...
    Unpacking libgpm2:amd64 (1.20.7-5) ...
    Selecting previously unselected package psmisc.
    Preparing to unpack .../5-psmisc_23.2-1_amd64.deb ...
    Unpacking psmisc (23.2-1) ...
    Setting up lsb-base (10.2019051400) ...
    Setting up libgpm2:amd64 (1.20.7-5) ...
    Setting up psmisc (23.2-1) ...
    Setting up libprocps7:amd64 (2:3.3.15-2) ...
    Setting up libncurses6:amd64 (6.1+20181013-2+deb10u2) ...
    Setting up procps (2:3.3.15-2) ...
    update-alternatives: using /usr/bin/w.procps to provide /usr/bin/w (w) in auto mode
    Processing triggers for libc-bin (2.28-10) ...
    Removing intermediate container df945d8776f1
     ---> d1f316160a0a
    Step 3/3 : CMD ["sleep","99887766"]
     ---> Running in ee8e1b9371e2
    Removing intermediate container ee8e1b9371e2
     ---> a334f012d07a
    Successfully built a334f012d07a
    Successfully tagged minha-imagem:001
    Successfully tagged minha-imagem:latest
    
    

    Criamos a imagem chamada "minha-imagem" com a versão "001", que aparecerá na lista de imagens locais:

    docker image ls
    
    REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    minha-imagem        001                 a334f012d07a        3 minutes ago       134MB
    minha-imagem        latest              a334f012d07a        3 minutes ago       134MB
    
    

    Essa imagem conta com um comando CMD, logo, podemos rodar um container com essa imagem usando qualquer um dos dois comandos abaixo:

    docker run -d minha-imagem
    docker run -d minha-imagem:001
    

    docker ps -a
    
    CONTAINER ID  IMAGE             COMMAND        CREATED        STATUS   NAMES
    
    6924d42e525b  minha-imagem:001  "sleep 99.."   1 second ago   Up 1min  happy_ship
    8a654da11b94  minha-imagem      "sleep 99.."   2 seconds ago  Up 1min  blue_matsumoto
    
    

    Nota: como não informamos o nome do container, o Docker associa um nome composto de 2 palavras aleatórias de seu dicionário, como não informamos a rede ele associa a rede "bridge" docker0 e como não informamos nenhum comando ele assume o ENTRYPOINT ou CMD da imagem (sleep 99887766).


    2 - Imagem "minha-imagem-002" usando ENTRYPOINT

    Vamos fazer a mesma imagem, versão 2, usando ENTRYPOINT em vez de CMD:

    mkdir /tmp/minha-imagem-002
    cd /tmp/minha-imagem-002
    touch Dockerfile
    

    # Arquivo /tmp/minha-imagem-002/Dockerfile
    
    FROM debian:10.5
    RUN apt-get -y update && apt-get -y upgrade && apt-get -y install procps
    ENTRYPOINT ["sleep","99887766"]
    

    
    docker build -t minha-imagem:002 -t minha-imagem:latest /tmp/minha-imagem-002
    
    

    Rodando container usando nova imagem:

    
    docker run -d  --name=ct-img-nova minha-imagem
    
    

    3 - Imagem "minha-imagem-003" usando ENTRYPOINT e CMD

    O resultado funcional é o mesmo, agora, se combinarmos as duas instruções ENTRYPOINT e CMD e na mesma imagem, temos um efeito de soma dos dois argumentos num único comando:

    mkdir /tmp/minha-imagem-003
    cd /tmp/minha-imagem-003
    touch Dockerfile
    

    # Arquivo /tmp/minha-imagem-003/Dockerfile
    
    FROM debian:10.5
    RUN apt-get -y update && apt-get -y upgrade && apt-get -y install procps
    ENTRYPOINT ["sleep"]
    CMD ["99887766"]
    

    
    docker build -t minha-imagem:003 -t minha-imagem:latest /tmp/minha-imagem-003
    
    

    Rodando terceiro container usando nova imagem:

    
    docker run -d  --name=ct-img-final minha-imagem
    
    

    Em todos os casos nós temos o mesmo resultado, mas você pode entender a implicação da existência do ENTRYPOINT.

    4 - ENTRYPOINT direto ou interpretado por comando do shell

    Você deve ter muito cuidado pois há uma pegadinha na declaração desses comandos, observe:

    # Arquivo /tmp/test01/Dockerfile
    
    FROM debian:10.5
    ENTRYPOINT ["sleep","99887766"]
    

    # Arquivo /tmp/test02/Dockerfile
    
    FROM debian:10.5
    ENTRYPOINT sleep 99887766
    

    Observe a diferença na hora de listar os processos:

    docker run -d test01
    docker run -d test02
    
    docker ps -a
    
    CONTAINER ID   IMAGE    COMMAND                CREATED        STATUS   NAMES
    
    9eff1d3a7938   test01   "sleep 99887766"       5 minutes ago  Up 1mi   sad_kalam
    c5d0742f1199   test02   /bin/sh -c 'sleep 9…"  5 minutes ago  Up 1mi   inspiring_pare
    
    

    A forma específica entre chaves ["sleep","99887766"] é diretamente executada, enquanto que na ausência dela o processo que realmente é executado é o shell bash (sh -c), o que implica em um software a mais rodando no container, na ausência das chaves, um container sem o bash (comando sh) não irá funcionar.


    5 - Empilhamento de camadas

    Outro detalhe importante na criação de uma imagem é evitar o excesso de camadas, observe a duas imagens personalizadas abaixo:

    # Arquivo /tmp/teste-camadas-01/Dockerfile
    
    FROM debian:10.5
    
    ENV \
        MAINTAINER="Patrick Brandao" \
        EMAIL=patrickbrandao@gmail.com \
        TERM=xterm \
        TZ=America/Sao_Paulo \
        PS1='\u@\h:\w\$ ' \
        LANG=en_US.UTF-8 \
        LANGUAGE=en_US.UTF-8 \
        DOMAIN=intranet.br
    
    RUN ( \
        apt-get -y update || exit 11; \
        apt-get -y upgrade || exit 12; \
        apt-get -y install \
            procps coreutils util-linux psmisc iproute2 net-tools vim \
            tcpdump whois fping nmap nmap-common \
            mtr iputils-arping iputils-ping iputils-tracepath \
            snmp snmpd || exit 13; \
    )
    
    ENTRYPOINT ["sleep","99887766"]
    

    # Arquivo /tmp/teste-camadas-02/Dockerfile
    
    FROM debian:10.5
    
    ENV MAINTAINER="Patrick Brandao" \
    ENV EMAIL=patrickbrandao@gmail.com \
    ENV TERM=xterm \
    ENV TZ=America/Sao_Paulo \
    ENV PS1='\u@\h:\w\$ ' \
    ENV LANG=en_US.UTF-8 \
    ENV LANGUAGE=en_US.UTF-8 \
    ENV DOMAIN=intranet.br
    
    RUN apt-get -y update
    RUN apt-get -y upgrade
    RUN apt-get -y install procps
    RUN apt-get -y install coreutils
    RUN apt-get -y install util-linux
    RUN apt-get -y install psmisc
    RUN apt-get -y install iproute2
    RUN apt-get -y install net-tools
    RUN apt-get -y install vim
    RUN apt-get -y install tcpdump
    RUN apt-get -y install whois
    RUN apt-get -y install fping
    RUN apt-get -y install nmap
    RUN apt-get -y install nmap-common
    RUN apt-get -y install mtr
    RUN apt-get -y install iputils-arping
    RUN apt-get -y install iputils-ping
    RUN apt-get -y install iputils-tracepath
    RUN apt-get -y install snmp
    RUN apt-get -y install snmpd
    
    ENTRYPOINT ["sleep","99887766"]
    

    Ambas criam a mesma imagem funcional, mesmas variáveis, com os mesmos programas, mesmo ENTRYPOINT, no entanto, a primeira criou um número muito menor de camadas. A primeira imagem vai consumir menos recursos do sistema de arquivos, menos recursos de I/O e menos processamento.



    Volumes e montagens

    Uma coisa que ja deve ter ficado clara é que container é lugar de programas, não é lugar de dados.

    Você deve ter total liberdade de destruir um container sem que os dados sejam destruídos juntos.

    Vamos trabalhar o conceito de volumes e montagens usando um exemplo de WebSite hospedado em container.

    Alguém ansioso para hospedar seu WebSite em um container vai procurar uma forma de incluir os arquivos HTML e PHP dentro da imagem, não caia nessa tentação. Imagine o trabalho de gerar uma nova imagem/tag para cada alteração que você faz no website? Nada bom.

    A imagem de container de seu servidor HTTP deve ser estática e só deve ser alterada se houver atualizações no software.

    Os códigos e imagens do seu website são os dados e deve ser atualizados sempre que você alterá-lo.

    Vamos criar um HTML simples:

    mkdir /storage/site01
    
    echo '<h1>Ola mundo</h1>' > /storage/site01/index.html
    
    docker run \
        -d \
        --name Meu-Site-01 \
        -p 8001:80 \
        -v /storage/site01/:/usr/local/apache2/htdocs/  \
        httpd:2.4
    
    

    Ao acessar a URL http://x.x.x.x:8001/ (onde x.x.x.x é o ip do seu Linux) você observará a pagina "Olá mundo"

    O que fizemos acima foi adicionar uma nova camada de sistema de arquivos que coloca a pasta /storage/site01 do host por cima da pasta /usr/local/apache2/htdocs dentro do container. Se existiam arquivos em /usr/local/apache2/htdocs eles não serão mais acessíveis pois foram cobertos pela montagem do volume.

    Você pode trabalhar normalmente na pasta /storage/site01, mexendo nos seus códigos a vontade, que sempre poderá acompanhar pelo navegador suas alterações.

    Se por alguma falha de segurança o software httpd for invadido, o invasor conseguirá alterar os arquivos em /usr/local/apache2/htdocs, o que significa alterar imediatamente os arquivos em /storage/site01

    Se você destruir o container meu-site01 seus arquivos em /storage/site01 não serão afetados, apenas o volume é desmontado.

    Uma forma de impedir que violações e bugs no site permitam alterações é montar as pastas em somente leitura. Observe:

    mkdir /storage/site02
    
    echo '<h1>Ola mundo inseguro</h1>' > /storage/site02/index.html
    
    docker run \
        -d \
        --name Meu-Site-02 \
        -p 8002:80 \
        --mount \
        type=bind,source=/storage/site02/,destination=/usr/local/apache2/htdocs/,readonly=true \
        httpd:2.4
    
    

    Ao acessar a URL http://x.x.x.x:8002/ (onde x.x.x.x é o ip do seu Linux) você observará a pagina "Olá mundo inseguro"

    Entre no container e tente remover o arquivo:

    docker exec -it Meu-Site-02 bash
    
    rm /usr/local/apache2/htdocs/index.html 
    rm: cannot remove '/usr/local/apache2/htdocs/index.html': Read-only file system
    
    

    Containers multiprocessos

    Não é uma pratica comum, mas acontece, de você precisar rodar dois processos em um container.

    Vou criar um exemplo onde eu rodo um servidor Web e um servidor SSH juntos, de maneira que eu possa entrar remotamente direto no container, sem precisar ter acesso ao Host.

    Temos duas formas de fazer isso:

    Método 1 - ENTRYPOINT rodando aplicações em sequência

    Este é o método mais simples, rápido e comum. Simplesmente criaremos um script de ENTRYPOINT que executará os programas secundários e manterá a execução do container num processo principal.

    mkdir /tmp/multiproc-01
    cd /tmp/multiproc-01
    touch Dockerfile
    

    # Arquivo /tmp/multiproc-01/meu-entrypoint.sh
    
    #!/bin/sh
    
    # Senha de root
    [ "x$ROOT_PASSWORD" = "x" ] || {
        echo "root:${ROOT_PASSWORD}" | chpasswd 2>/dev/null 1>/dev/null;
    }
    
    # Subir SSHD
    /usr/sbin/sshd
    
    # Segurar container no processo HTTPd
    exec lighttpd -f /etc/lighttpd/lighttpd.conf -D
    
    

    # Arquivo /tmp/multiproc-01/Dockerfile
    
    FROM debian:10.5
    
    ENV MAINTAINER="Patrick Brandao" \
        EMAIL=patrickbrandao@gmail.com \
        TERM=xterm \
        PS1='\u@\h:\w\$ ' \
        LANG=en_US.UTF-8 \
        LANGUAGE=en_US.UTF-8
    
    USER root
    
    ADD meu-entrypoint.sh /
    
    RUN ( \
        apt-get -y update || exit 11; \
        apt-get -y upgrade || exit 12; \
        apt-get -y install procps lighttpd openssh-server || exit 13; \
        sed -i 's;#PermitRootLogin.*;PermitRootLogin yes;' /etc/ssh/sshd_config; \
        chmod +x /meu-entrypoint.sh; \
        mkdir -p mkdir /run/sshd; \
    )
    
    ENTRYPOINT /meu-entrypoint.sh
    
    

    Construindo imagem e rodando container:

    
    docker build -t multiproc:01 /tmp/multiproc-01
    
    
    mkdir /storage/site03
    echo '<h1>Ola mundo INTEGRADO</h1>' > /storage/site03/index.html
    
    docker run \
        -d \
        --name Meu-Site-03 \
        -p 8003:80 \
        -p 2203:22 \
        -e ROOT_PASSWORD=tulipa \
        --mount \
        type=bind,source=/storage/site03/,destination=/var/www/html/,readonly=true \
        multiproc:01
    
    

    No container acima você pode acessar via http na porta 8003 (aparecerá a mensagem Olá mundo INTEGRADO) ou acessar via SSH na porta 2203 (usuario root senha tulipa).

    O defeito do método acima é que se o processo SSHD morrer você precisará reiniciá-lo manualmente dentro do container ou reiniciar o container.


    Método 2 - ENTRYPOINT rodando boot e segurando container em gerenciador de processos

    Esse é meu método favorito, pois permite que o container se comporte mais parecido com um Linux independente, e caso algum processo morra o gerenciador de processos revive o processo morto.

    Os defeitos dessa técnica são: memory-leak em processos são mais desastrosos, updates envolvem custo de espaço em disco com COPY-ON-WRITE, reconstrução mais demorada das imagens, maior superfície de ataque e falhas de segurança.

    Um problema que eu não abordei até agora é a consequência de montar volumes nos containers. Se você roda um software complexo como o MariaDB (sucessor do MySQL) vai obviamente precisar que o banco de dados inicial esteja criado antes de rodar o processo mysqld, mas se você montou um volume encima da pasta /var/lib/mysql esta pasta se encontrará vazia quando o container for reiniciado.

    Rodar o mysqld aqui resultará em morte do processo, e reiniciá-lo pelo gerenciador de tarefas resultará em loop infinito.

    O ENTRYPOINT, em vez de apenas rodar um processo que segure a existência do container (PID 1), ele pode também executar funções de boot, ajustando o ambiente para que todos os softwares possam rodar de maneira correta.

    Iremos usar o supervisor como gerenciador de processos, mas temos outras opções que não abordarei nesse artigo (S7, SystemD).

    mkdir /tmp/multiproc-02
    cd /tmp/multiproc-02
    touch Dockerfile
    

    # Arquivo /tmp/multiproc-02/entrypoint.sh
    
    #!/bin/sh
    
    DOCKER_CMD="$@"
    
    # Senha de root
    [ "x$ROOT_PASSWORD" = "x" ] || {
        echo "root:${ROOT_PASSWORD}" | chpasswd 2>/dev/null 1>/dev/null;
    }
    
    # Rodar scripts de boot
    cd /opt && {
    	for escript in boot-*.sh; do
    		sh $escript
    	done
    }
    
    # Segurar container no processo supervisor
    exec $DOCKER_CMD
    
    

    # Arquivo /tmp/multiproc-02/boot-mariadb.sh
    
    #!/bin/sh
    
    # Garantir existencia de banco de dados inicial
    doinst=0
    [ -d /var/lib/mysql ] || doinst=1
    [ -d /var/lib/mysql/mysql ] || doinst=2
    [ -d /var/lib/mysql/mysql/user.frm ] || doinst=2
    [ "$doinst" = "0" ] || {
    	echo "# Instalando db inicial"
    	#[ -f /etc/setup/mariadb-default.tgz ] && tar -xvf /etc/setup/mariadb-default.tgz -C /
    	mysql_install_db \
    		--datadir=/var/lib/mysql \
    		--user=mysql \
    		--skip-test-db \
    		--force \
    		--skip-name-resolve \
    		--verbose \
    		--tmpdir=/tmp
    }
    # Garantir diretorios
    mkdir -p /var/lib/mysql/mysql
    mkdir -p /run/mysqld
    # Ajustar permissoes
    chown -R mysql:mysql /var/lib/mysql
    chown -R mysql:mysql /run/mysqld
    
    

    # Arquivo /tmp/multiproc-02/supervisor-mariadb.conf
    
    [program:mariadb]
    command=/usr/bin/mysqld_safe --user=mysql --datadir=/var/lib/mysql --pid-file=/run/mysqld/mysqld.pid --skip-name-resolve --skip-networking=0
    stopwaitsecs=3
    autostart=true
    autorestart=true
    user=root
    

    # Arquivo /tmp/multiproc-02/supervisor-sshd.conf
    
    [program:sshd]
    command=/usr/sbin/sshd -D
    stopwaitsecs=3
    autostart=true
    autorestart=true
    user=root
    

    # Arquivo /tmp/multiproc-02/supervisor-lighttpd.conf
    
    [program:lighttpd]
    command=/usr/sbin/lighttpd -f /etc/lighttpd/lighttpd.conf -D
    stopwaitsecs=3
    autostart=true
    autorestart=true
    user=root
    

    # Arquivo /tmp/multiproc-02/supervisor-mariadb.conf
    
    [program:mariadb]
    command=/usr/bin/mysqld_safe --user=mysql --datadir=/var/lib/mysql --pid-file=/run/mysqld/mysqld.pid --skip-name-resolve --skip-networking=0
    stopwaitsecs=3
    autostart=true
    autorestart=true
    user=root
    

    # Arquivo /tmp/multiproc-02/Dockerfile
    
    FROM debian:10.5
    
    ENV MAINTAINER="Patrick Brandao" \
        EMAIL=patrickbrandao@gmail.com \
        TERM=xterm \
        PS1='\u@\h:\w\$ ' \
        LANG=en_US.UTF-8 \
        LANGUAGE=en_US.UTF-8
    
    USER root
    
    ADD entrypoint.sh /opt/_entrypoint.sh
    ADD boot-mariadb.sh /opt/boot-mariadb.sh
    
    RUN ( \
        apt-get -y update || exit 11; \
        apt-get -y upgrade || exit 12; \
        apt-get -y install procps tar lighttpd openssh-server || exit 13; \
        apt-get -y install supervisor  || exit 14; \
        apt-get -y install mariadb-server|| exit 15; \
        sed -i 's;#PermitRootLogin.*;PermitRootLogin yes;' /etc/ssh/sshd_config; \
        chmod +x /opt/*.sh; \
        mkdir -p mkdir /run/sshd; \
        mkdir -p /run/mysqld; \
        mkdir -p /opt/entrypoints; \
    )
    
    ADD supervisor-mariadb.conf    /etc/supervisor/conf.d/
    ADD supervisor-sshd.conf       /etc/supervisor/conf.d/
    ADD supervisor-lighttpd.conf   /etc/supervisor/conf.d/
    ADD supervisor-mariadb.conf    /etc/supervisor/conf.d/
    
    ENTRYPOINT ["/opt/_entrypoint.sh"]
    CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]
    
    

    Construindo imagem e rodando container:

    
    docker build -t multiproc:02 /tmp/multiproc-02
    
    
    mkdir /storage/site04
    echo '<h1>Ola mundo ATAREFADO</h1>' > /storage/site04/index.html
    
    mkdir /storage/banco04
    
    docker run \
        -d \
        --name Meu-Site-04 \
        --hostname site04.intranet.br \
        -p 8004:80 \
        -p 2204:22 \
        -e ROOT_PASSWORD=tulipa \
        \
        --mount \
        type=bind,source=/storage/site04/,destination=/var/www/html/,readonly=true \
        \
        --mount \
        type=bind,source=/storage/banco04/,destination=/var/lib/mysql/,readonly=false \
        \
        multiproc:02
    
    

    Observe os processo rodando dentro do container:

    
    docker exec -it Meu-Site-04 ps ax
    
       PID TTY      STAT   TIME COMMAND
         1 ?        Ss     0:00 /usr/bin/python2 /usr/bin/supervisord -n -c /etc/sup
        86 ?        S      0:00 /usr/sbin/sshd -D
        87 ?        S      0:00 /bin/sh /usr/bin/mysqld_safe --user=mysql --datadir=
        88 ?        S      0:00 lighttpd -f /etc/lighttpd/lighttpd.conf -D
       227 ?        Sl     0:00 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/m
       228 ?        S      0:00 logger -t mysqld -p daemon error
       276 pts/0    Rs+    0:00 ps ax
    
    

    Embora a demonstração seja básica, a evolução da técnica é por conta de quem deseja fazer uma imagem "tudo-em-um".



    Acredito que até aqui você já tenha pegado o espirito da coisa, agora é explorar mais exemplos e montar suas próprias imagens.

    Obrigado pela audiência!
CONTATO