Introdução
Neste laboratório, vamos aprofundar nossos conhecimentos do laboratório 1, onde usamos comandos Docker para executar containers. Criaremos uma Imagem Docker personalizada construída a partir de um Dockerfile. Uma vez que construirmos a imagem, vamos enviá-la para um registro central onde poderá ser puxada para ser implantada em outros ambientes. Além disso, descreveremos brevemente as camadas da imagem e como o Docker incorpora "copy-on-write" (cópia-na-escrita) e o sistema de arquivos union para armazenar imagens e executar containers de forma eficiente.
Usaremos alguns comandos Docker neste laboratório. Para obter a documentação completa sobre os comandos disponíveis, consulte a documentação oficial.
Criar um App Python (sem usar Docker)
Execute o seguinte comando para criar um arquivo chamado app.py com um programa python simples. (copie e cole todo o bloco de código)
cd ~/project
echo 'from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "hello world!"
if __name__ == "__main__":
app.run(host="0.0.0.0")' > app.py
Este é um aplicativo python simples que usa flask para expor um servidor web http na porta 5000 (5000 é a porta padrão para flask). Não se preocupe se você não estiver muito familiarizado com python ou flask, esses conceitos podem ser aplicados a um aplicativo escrito em qualquer linguagem.
Opcional: Se você tiver python e pip instalados, pode executar este aplicativo localmente. Caso contrário, vá para a próxima etapa.
$ python3 --version
$ pip3 --version
$ pip3 install flask
$ python3 app.py
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
Abra o aplicativo em uma nova aba do navegador usando http://0.0.0.0:5000/.

Criar e Construir a Imagem Docker
Agora, e se você não tiver o python instalado localmente? Não se preocupe! Porque você não precisa. Uma das vantagens de usar containers é que você pode construir python dentro de seus containers, sem ter python instalado em sua máquina host.
Crie um Dockerfile executando o seguinte comando. (copie e cole todo o bloco de código)
echo 'FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app.py"]
COPY app.py /app.py' > Dockerfile
Um Dockerfile lista as instruções necessárias para construir uma imagem docker. Vamos analisar o arquivo acima linha por linha.
FROM python:3.8-alpine
Este é o ponto de partida para seu Dockerfile. Todo Dockerfile deve começar com uma linha FROM que é a imagem inicial para construir suas camadas em cima.
Neste caso, estamos selecionando a camada base python:3.8-alpine (veja Dockerfile para python3.8/alpine3.12) pois ela já possui a versão do python e pip que precisamos para executar nosso aplicativo.
A versão alpine significa que ela usa a distribuição Alpine Linux, que é significativamente menor do que muitas outras versões de Linux, com cerca de 8 MB de tamanho, enquanto uma instalação mínima em disco pode ter cerca de 130 MB. Uma imagem menor significa que ela fará o download (implantação) muito mais rápido, e também tem vantagens para a segurança porque tem uma superfície de ataque menor. Alpine Linux é uma distribuição Linux baseada em musl e BusyBox.
Aqui estamos usando a tag "3.8-alpine" para a imagem python. Dê uma olhada nas tags disponíveis para a imagem python oficial no Docker Hub. É uma boa prática usar uma tag específica ao herdar uma imagem pai para que as alterações na dependência pai sejam controladas. Se nenhuma tag for especificada, a tag "latest" entra em vigor, que atua como um ponteiro dinâmico que aponta para a versão mais recente de uma imagem.
Por razões de segurança, é muito importante entender as camadas sobre as quais você constrói sua imagem docker. Por essa razão, é altamente recomendável usar apenas imagens "oficiais" encontradas no docker hub ou imagens não comunitárias encontradas no docker-store. Essas imagens são vetted para atender a certos requisitos de segurança e também possuem uma documentação muito boa para os usuários seguirem. Você pode encontrar mais informações sobre esta imagem base python, bem como todas as outras imagens que você pode usar, no docker hub.
Para um aplicativo mais complexo, você pode precisar usar uma imagem FROM que esteja mais acima na cadeia. Por exemplo, o Dockerfile pai para nosso aplicativo python começa com FROM alpine, então especifica uma série de comandos CMD e RUN para a imagem. Se você precisasse de um controle mais preciso, poderia começar com FROM alpine (ou uma distribuição diferente) e executar essas etapas você mesmo. Para começar, no entanto, recomendo usar uma imagem oficial que corresponda de perto às suas necessidades.
RUN pip install flask
O comando RUN executa comandos necessários para configurar sua imagem para seu aplicativo, como instalar pacotes, editar arquivos ou alterar permissões de arquivos. Neste caso, estamos instalando flask. Os comandos RUN são executados no momento da construção e são adicionados às camadas de sua imagem.
CMD ["python","app.py"]
CMD é o comando que é executado quando você inicia um container. Aqui estamos usando CMD para executar nosso aplicativo python.
Pode haver apenas um CMD por Dockerfile. Se você especificar mais de um CMD, o último CMD entrará em vigor. O python:3.8-alpine pai também especifica um CMD (CMD python3). Você pode encontrar o Dockerfile para a imagem python:alpine oficial aqui.
Você pode usar a imagem python oficial diretamente para executar scripts python sem instalar python em seu host. Mas hoje, estamos criando uma imagem personalizada para incluir nossa fonte, para que possamos construir uma imagem com nosso aplicativo e enviá-la para outros ambientes.
COPY app.py /app.py
Isso copia o app.py no diretório local (onde você executará docker image build) em uma nova camada da imagem. Esta instrução é a última linha no Dockerfile. Camadas que mudam com frequência, como copiar o código-fonte na imagem, devem ser colocadas na parte inferior do arquivo para aproveitar ao máximo o cache de camadas do Docker. Isso nos permite evitar a reconstrução de camadas que, de outra forma, poderiam ser armazenadas em cache. Por exemplo, se houvesse uma alteração na instrução FROM, ela invalidaria o cache para todas as camadas subsequentes desta imagem. Demonstraremos isso um pouco mais tarde neste laboratório.
Parece contraintuitivo colocar isso após a linha CMD ["python","app.py"]. Lembre-se, a linha CMD é executada apenas quando o container é iniciado, então não receberemos um erro file not found aqui.
E pronto: um Dockerfile muito simples. Uma lista completa de comandos que você pode colocar em um Dockerfile pode ser encontrada aqui. Agora que definimos nosso Dockerfile, vamos usá-lo para construir nossa imagem docker personalizada.
Construa a imagem docker.
Passe -t para nomear sua imagem python-hello-world.
docker image build -t python-hello-world .
Verifique se sua imagem aparece em sua lista de imagens.
docker image ls
Observe que sua imagem base python:3.8-alpine também está em sua lista.
Você pode executar um comando de histórico para mostrar o histórico de uma imagem e suas camadas,
docker history python-hello-world
docker history python:3.8-alpine
Executar a Imagem Docker
Agora que você construiu a imagem, pode executá-la para ver se ela funciona.
Execute a imagem Docker
docker run -p 5001:5000 -d python-hello-world
A flag -p mapeia uma porta em execução dentro do container para seu host. Neste caso, estamos mapeando o aplicativo python em execução na porta 5000 dentro do container para a porta 5001 em seu host. Observe que, se a porta 5001 já estiver em uso por outro aplicativo em seu host, você pode precisar substituir 5001 por outro valor, como 5002.
Navegue até a aba PORTS na janela do terminal e clique no link para abrir o aplicativo em uma nova aba do navegador.

Em um terminal, execute curl localhost:5001, que retorna hello world!.
Verifique a saída de log do container.
Se você quiser ver os logs do seu aplicativo, pode usar o comando docker container logs. Por padrão, docker container logs imprime o que é enviado para a saída padrão pelo seu aplicativo. Use docker container ls para encontrar o ID do seu container em execução.
labex:project/ $ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
52df977e5541 python-hello-world "python app.py" 2 minutes ago Up 2 minutes 0.0.0.0:5001->5000/tcp, :::5001->5000/tcp heuristic_lamport
labex:project/ $ docker container logs 52df977e5541
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.17.0.2:5000
Press CTRL+C to quit
172.17.0.1 - - [23/Jan/2024 02:43:10] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [23/Jan/2024 02:43:10] "GET /favicon.ico HTTP/1.1" 404 -
O Dockerfile é como você cria builds reproduzíveis para seu aplicativo. Um fluxo de trabalho comum é ter sua automação CI/CD executar docker image build como parte de seu processo de build. Depois que as imagens são construídas, elas serão enviadas para um registro central, onde podem ser acessadas por todos os ambientes (como um ambiente de teste) que precisam executar instâncias desse aplicativo. Na próxima etapa, enviaremos nossa imagem personalizada para o registro público do docker: o docker hub, onde ela pode ser consumida por outros desenvolvedores e operadores.
Enviar para um Registro Central
Navegue até Docker Hub e crie uma conta, caso ainda não tenha uma. Alternativamente, você também pode usar https://quay.io, por exemplo.
Para este laboratório, usaremos o Docker Hub como nosso registro central. O Docker Hub é um serviço gratuito para armazenar imagens publicamente disponíveis, ou você pode pagar para armazenar imagens privadas. Vá para o site Docker Hub e crie uma conta gratuita.
A maioria das organizações que usam o docker intensamente configurará seu próprio registro internamente. Para simplificar as coisas, usaremos o Docker Hub, mas os seguintes conceitos se aplicam a qualquer registro.
Login
Você pode fazer login na conta do registro de imagens digitando docker login em seu terminal, ou se estiver usando podman, digite podman login.
labex:project/ $ export DOCKERHUB_USERNAME=<your_docker_username>
labex:project/ $ docker login docker.io -u $DOCKERHUB_USERNAME
Password:
WARNING! Your password will be stored unencrypted in /home/labex/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
Marque sua imagem com seu nome de usuário
A convenção de nomenclatura do Docker Hub é marcar sua imagem com [nome de usuário do dockerhub]/[nome da imagem]. Para fazer isso, vamos marcar nossa imagem criada anteriormente python-hello-world para se adequar a esse formato.
docker tag python-hello-world $DOCKERHUB_USERNAME/python-hello-world
Envie sua imagem para o registro
Depois de termos uma imagem devidamente marcada, podemos usar o comando docker push para enviar nossa imagem para o registro do Docker Hub.
docker push $DOCKERHUB_USERNAME/python-hello-world
Verifique sua imagem no docker hub em seu navegador
Navegue até Docker Hub e vá para seu perfil para ver sua imagem recém-carregada em https://hub.docker.com/repository/docker/<dockerhub-username>/python-hello-world.
Agora que sua imagem está no Docker Hub, outros desenvolvedores e operações podem usar o comando docker pull para implantar sua imagem em outros ambientes.
Observação: As imagens Docker contêm todas as dependências de que precisam para executar um aplicativo dentro da imagem. Isso é útil porque não precisamos mais lidar com a deriva do ambiente (diferenças de versão) quando confiamos em dependências que são instaladas em todos os ambientes em que implantamos. Também não precisamos passar por etapas adicionais para provisionar esses ambientes. Apenas uma etapa: instale o docker e você está pronto para começar.
Implantando uma Mudança
O aplicativo "hello world!" é superestimado, vamos atualizar o aplicativo para que ele diga "Hello Beautiful World!" em vez disso.
Atualizar app.py
Substitua a string "Hello World" por "Hello Beautiful World!" em app.py. Você pode atualizar o arquivo com o seguinte comando. (copie e cole todo o bloco de código)
echo 'from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "hello beautiful world!"
if __name__ == "__main__":
app.run(host="0.0.0.0")' > app.py
Reconstruir e Enviar sua Imagem
Agora que seu aplicativo está atualizado, você precisa repetir as etapas acima para reconstruir seu aplicativo e enviá-lo para o registro do Docker Hub.
Primeiro, reconstrua, desta vez use seu nome de usuário do Docker Hub no comando de build:
docker image build -t $DOCKERHUB_USERNAME/python-hello-world .
Observe "Using cache" (Usando cache) para as etapas 1-3. Essas camadas da Imagem Docker já foram construídas e docker image build usará essas camadas do cache em vez de reconstruí-las.
docker push $DOCKERHUB_USERNAME/python-hello-world
Existe um mecanismo de cache em vigor também para enviar camadas. O Docker Hub já possui todas as camadas, exceto uma, de um envio anterior, então ele envia apenas a camada que foi alterada.
Quando você altera uma camada, cada camada construída em cima dela terá que ser reconstruída. Cada linha em um Dockerfile constrói uma nova camada que é construída na camada criada a partir das linhas anteriores. É por isso que a ordem das linhas em nosso Dockerfile é importante. Otimizamos nosso Dockerfile para que a camada com maior probabilidade de mudar (COPY app.py /app.py) seja a última linha do Dockerfile. Geralmente, para um aplicativo, seu código muda com a taxa mais frequente. Essa otimização é particularmente importante para processos CI/CD, onde você deseja que sua automação seja executada o mais rápido possível.
Compreendendo as Camadas da Imagem
Uma das principais propriedades de design do Docker é o uso do sistema de arquivos de união (union file system).
Considere o Dockerfile que criamos anteriormente:
FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app.py"]
COPY app.py /app.py
Cada uma dessas linhas é uma camada. Cada camada contém apenas o delta, a diferença ou as alterações das camadas anteriores. Para juntar essas camadas em um único contêiner em execução, o Docker utiliza o union file system para sobrepor as camadas de forma transparente em uma única visualização.
Cada camada da imagem é read-only (somente leitura), exceto a camada superior, que é criada para o contêiner em execução. A camada de contêiner de leitura/gravação implementa "copy-on-write" (cópia sob escrita), o que significa que os arquivos armazenados nas camadas inferiores da imagem são puxados para a camada de contêiner de leitura/gravação somente quando edições são feitas nesses arquivos. Essas alterações são então armazenadas na camada do contêiner em execução. A função "copy-on-write" é muito rápida e, em quase todos os casos, não tem um efeito perceptível no desempenho. Você pode inspecionar quais arquivos foram puxados para o nível do contêiner com o comando docker diff. Mais informações sobre como usar docker diff podem ser encontradas aqui.

Como as camadas da imagem são read-only, elas podem ser compartilhadas por imagens e por contêineres em execução. Por exemplo, a criação de um novo aplicativo python com seu próprio Dockerfile com camadas base semelhantes, compartilharia todas as camadas que tinha em comum com o primeiro aplicativo python.
FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app2.py"]
COPY app2.py /app2.py

Você também pode experimentar o compartilhamento de camadas ao iniciar vários contêineres a partir da mesma imagem. Como os contêineres usam as mesmas camadas somente leitura, você pode imaginar que iniciar contêineres é muito rápido e tem uma pegada muito baixa no host.
Você pode notar que existem linhas duplicadas neste Dockerfile e no Dockerfile que você criou anteriormente neste laboratório. Embora este seja um exemplo muito trivial, você pode puxar linhas comuns de ambos os Dockerfiles para um Dockerfile "base", ao qual você pode apontar com cada um de seus Dockerfiles filhos usando o comando FROM.
O image layering (camadas de imagem) permite o mecanismo de cache do docker para builds e pushes. Por exemplo, a saída do seu último docker push mostra que algumas das camadas da sua imagem já existem no Docker Hub.
$ docker push $DOCKERHUB_USERNAME/python-hello-world
Para olhar mais de perto as camadas, você pode usar o comando docker image history da imagem python que criamos.
$ docker image history python-hello-world
Cada linha representa uma camada da imagem. Você notará que as linhas superiores correspondem ao seu Dockerfile que você criou, e as linhas abaixo são puxadas da imagem python pai. Não se preocupe com as tags "<missing>". Estas ainda são camadas normais; elas simplesmente não receberam um ID pelo sistema docker.
Limpeza
Completar este laboratório resulta em um monte de contêineres em execução no seu host. Vamos limpá-los.
Execute docker container stop [container id] para cada contêiner que estiver em execução
Primeiro, obtenha uma lista dos contêineres em execução usando docker container ls.
$ docker container ls
Em seguida, execute o comando para cada contêiner na lista.
$ docker container stop <container_id>
Remova os contêineres parados
docker system prune é um comando muito útil para limpar seu sistema. Ele removerá quaisquer contêineres parados, volumes e redes não utilizados e imagens pendentes.
$ docker system prune
WARNING! This will remove:
- all stopped containers
- all volumes not used by at least one container
- all networks not used by at least one container
- all dangling images
Are you sure you want to continue? [y/N] y
Deleted Containers:
0b2ba61df37fb4038d9ae5d145740c63c2c211ae2729fc27dc01b82b5aaafa26
Total reclaimed space: 300.3kB
Resumo
Neste laboratório, você começou a agregar valor criando seus próprios contêineres Docker personalizados.
Principais Conclusões:
- O Dockerfile é como você cria builds reproduzíveis para sua aplicação e como você integra sua aplicação com o Docker no pipeline CI/CD.
- Imagens Docker podem ser disponibilizadas para todos os seus ambientes através de um registro central. O Docker Hub é um exemplo de registro, mas você pode implantar seu próprio registro em servidores que você controla.
- Imagens Docker contêm todas as dependências que precisam para executar uma aplicação dentro da imagem. Isso é útil porque não precisamos mais lidar com a deriva de ambiente (diferenças de versão) quando dependemos de dependências que são instaladas em cada ambiente que implantamos.
- O Docker utiliza o sistema de arquivos de união (union file system) e "copy on write" (cópia sob escrita) para reutilizar camadas de imagens. Isso diminui a pegada de armazenamento de imagens e aumenta significativamente o desempenho da inicialização de contêineres.
- As camadas de imagem são armazenadas em cache pelo sistema de build e push do Docker. Não há necessidade de reconstruir ou reenviar as camadas de imagem que já estão presentes no sistema desejado.
- Cada linha em um Dockerfile cria uma nova camada e, por causa do cache de camadas, as linhas que mudam com mais frequência (por exemplo, adicionar código-fonte a uma imagem) devem ser listadas perto da parte inferior do arquivo.