Você está na página 1de 22

Open in app

Search

Desmistificando a escrita de Dockerfile e


melhores práticas
Igoreulalio Ie · Follow
10 min read · Dec 15, 2023

Share More

De acordo com um estudo da Flexera, 78% das pequenas e médias empresas estão
usando containers. Entre as Fortune 500, 50% estão utilizando Kubernetes.

Ao longo de alguns anos trabalhando na indústria, encontrei inúmeras organizações


utilizando containers. Apesar disso, ainda é muito comum encontrar containers
rodando em ambiente produtivo que não seguem as melhores práticas, ou algum
conceito que não é claro para maioria das pessoas.

O foco deste artigo é desmistificar alguns conceitos na escrita de Dockerfiles, um


dos principais componentes utilizados durante o build de um container. No próximo
artigo desta série, vamos discutir sobre algumas boas práticas para uso de
containers, com foco em ambiente enterprise, dado que certos problemas só
começam a aparecer em um determinado nível de escalabilidade, porém também se
aplica a ambientes menores.

Dockerfile
O Dockerfile é um script de configuração de texto simples que contém instruções
para construir uma imagem específica.

É ainda muito comum ouvir o termo "imagem docker", apesar de não ser um termo
assertivo, gerando muita confusão entre as pessoas.
Apesar do nome Dockerfile, outras containers engine (daemonless ou não), também
podem ser usadas para criar imagens a partir do Dockerfile, como Podman.

Para quem não é familiarizado em como escrever imagens, podem seguir a


documentação oficial do Docker para seus passos inicias.

Agora que estamos todos na mesma página, vamos entender alguns conceitos mais
a fundo, preparado?

Image Layers
Todos que fizeram algum docker pull ou docker push (irei usar docker por
convenção, o mesmo se aplica para podman) devem ter reparado que ambos
comandos tem um output baseado em pequenos chunks, nesse caso podemos ver
9fda…, 60e4…, daa2… e outros pedaços sendo baixados.

Esses pedaços são conhecidos como Image Layers.

Muito se ouve falar sobre redução de tamanho de imagens e entender o conceito por
trás de Image Layers é imprescindível para atingir esse resultado.

Para entendermos melhor, vamos iniciar criando um Dockerfile e efetuar o build:

# Base image
FROM alpine:latest

CMD ["echo", "Hello, World!"]


docker build -t igoreulalio/docker-sample:v1 .

Fiquem a vontade para tagear (através da flag -t) como acharem melhor.

Agora, com nossa imagem criada, vamos inspeciona-la:

docker inspect igoreulalio/docker-sample:v1

Esse comando irá te retornar muitas informações sobre a imagem, inclusive, muito
útil no dia dia!
O que estamos buscando é a quantidade de layers dessa imagem, podem verificar
isso no campo RootFS:

"RootFS": {
"Type": "layers",
"Layers": [
"sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470e
]
}

No seu caso, o SHA256 será diferente, mas o que importa é ver que existe apenas
uma única layer dentro do array de Layers.

Para termos certeza, vamos inspecionar a nossa imagem base:

docker inspect alpine:latest

. . . output removido para facilidade de leitura

"RootFS": {
"Type": "layers",
"Layers": [
"sha256:b2191e2be29d816fa6fbde954316d54e10df9a882c7ca38e3e087d9
]
}

Ótimo, também temos apenas uma layer para a nossa base image, faz sentido,
certo? Nosso Dockerfile apenas altera o CMD da nossa imagem base, alpine:latest,
portanto, não adicionando nenhuma layer.

Existem 2 tipos de comando no Dockerfile, os que adicionam Layers e os que


adicionam apenas Metadata. Como devem imaginar, o comando CMD adiciona
apenas metadata, juntamente com comandos como: LABEL, EXPOSE, ENV, ARG,
WORKDIR, ENTRYPOINT, USER e VOLUME, vamos falar mais sobre alguns desses
em breve.

Por outro lado, comandos como COPY, ADD, RUN, FROM, geram layers.

Vamos verificar isso na prática!!

# Base image
FROM alpine:latest

# Adicionando um run command para criação de layer


RUN touch abc.txt

CMD ["echo", "Hello, World!"]

docker build -t igoreulalio/docker-sample:v2 .

docker inspect igoreulalio/docker-sample:v2

. . . output removido para facilidade de leitura

"RootFS": {
"Type": "layers",
"Layers": [
"sha256:b2191e2be29d816fa6fbde954316d54e10df9a882c7ca38e3e087d9
"sha256:776a98989b1849ba22587754e65429864bc6602dea2b6d1ebe11bb9
]
}
Como esperado, temos agora 2 layers. Vamos entender no detalhe o que cada layer
dessa representa.

Para isso, podemos salvar nossa imagem como um tar file e verificar a imagem
mais profundamente:

docker save igoreulalio/docker-sample:v2 -o docker-sample.tar

mkdir docker-sample && cd docker-sample && tar -xzf ../docker-sample.tar

Esse é o resultado final após save e extração do arquivo:

Aqui podemos ver nosso manifest.json, que contem informacões sobre a imagem e
nossas 2 layers, 6d52… e 6b9a…

Acessem cada uma das layers e extraia o arquivo layer.tar dentro de cada um deles.

Após a extração, essa é nossa visão de layers:


Aqui podemos ver claramente as duas layers, sendo a 6d52… nossa base image e a
outra sendo nosso comando RUN touch…

Com essa visão conseguimos descobrir várias coisas interessantes sobre nossa
imagem:
1. Toda estrutura do FS da nossa base image alpine está visível na sua respectiva
layer.

2. Nosso arquivo abc.txt pode ser acessado diretamente por a layer que o cria, não
estando disponível para a outra layer.
3. Uma layer é completamente diferente da outra, se um arquivo for criado em
uma layer e deletado em outra, acessando a imagem nesse formato, conseguimos
ver qual arquivo foi deletado.

Esse ponto 3 tem uma relevância enorme do ponto de vista de segurança, vamos
entender melhor.

Para isso, vamos escrever um valor sensível no nosso arquivo abc.txt:

# Base image
FROM alpine:latest

RUN echo "my-super-secret-value" > abc.txt

RUN rm abc.txt

CMD ["echo", "Hello, World!"]

docker build -t igoreulalio/docker-sample:v3 .

## Repita o processo anterior: docker save, tar -xzf, etc

Como devem imaginar, temos 3 instruções que geram layers, logo nossa imagem
agora possui 3 layers:

Extraia todas as layers e procure pela layer que contém o arquivo abc.txt:
Voilà!! Conseguimos encontrar o nosso valor sensível dentro do arquivo abc.txt.
Esse erro é muito comum de ser encontrado e pode gerar grandes prejuízos. No
nosso caso, o valor sensível está também no Dockerfile, apenas para ilustrar o
mecanismo, porém, isso também acontece ao copiar arquivos sensíveis para dentro
do container durante o build e apagarem logo na sequência, deixando a sensação de
que estão seguros. Qualquer um com acesso a imagem pode facilmente inspecionar
suas layers e buscar arquivos que foram deletados entre uma layer e outra, portanto,
muita atenção.

Multi-stage build
Agora que entendemos o que são layers, vamos falar melhor sobre uma estratégia
que utiliza do conceito de layers para prover imagens menores.

Primeiro, é importante entender porque todos estão sempre buscando diminuir o


tamanho de suas imagens.

Em resumo, imagens menores podem ser baixadas mais rapidamente, reduzindo o


network bandwith entre nodes do seu orquestrador favorito e o registry de imagens,
tornando mais rápido o processo de rodar um novo container, e possuem uma
superfície de ataque menor, melhorando a postura de segurança da imagem.

Vamos entender primeiro como o conceito funciona e depois aplica-lo na prática.


Esses são os passos que caracterizam o multi-stage build:
1. Uma imagem base que contém as dependências necessárias para buildar sua
aplicação é baixada como imagem inicial, normalmente chamada de build image.

2. A aplicação é buildada a partir dessa imagem, que por precisar de muitas


ferramentas, é geralmente uma imagem grande.

3. Após o build, os artefatos são gerados: artefatos nesse caso estamos falando de
outputs de mecanismos de build, como mvn package, go build e semelhantes para
outras linguagens.
4. Uma vez que temos nossos artefatos criados, usamos uma outra imagem,
normalmente chamada de run image, a image que será usada de fato para rodar sua
aplicação.

5. Os artefatos são copiados da imagem build para a imagem run, apenas eles.

6. Nossa imagem run não possui as bibliotecas necessárias para executar o build,
apenas o artefato e qualquer ferramenta necessária para rodar aquele artefato,
como por exemplo jdk para java ou o linux kernel para Golang.

Com esses passos em mente, podemos aplicar o mesmo conceito para qualquer tipo
de tecnologia. O grande truque aqui é sempre se provocar olhando para sua imagem
final e pensar: minha imagem tem apenas o que é necessário para rodar minha
aplicação? Se a resposta for sim, você aplicou corretamente o multi-stage build.

OBS: É muito comum incluir também ferramentas de troubleshooting na imagem


final, o que pode torna-la um pouco maior e menos segura, porém com mais
usabilidade para efeitos de debugging. É importante ter ciência que as mesmas
ferramentas usadas para troubleshooting, também podem ser usadas em ataques a
sua infraestrutura, a imagem fica com mais usabilidades nos dois sentidos,
portanto, use com cautela!

Vamos agora partir para a criação da nossa imagem, para isso, clonem este
repositório e sigam a partir dele, para facilitar o entendimento.

Acessem a pasta resultado do clone e buildem a imagem:

docker build -t igoreulalio/repository-service:v6-a .

Ao visualizar o Dockerfile, vocês irão perceber que existem múltiplos passos para
criação da imagem, entre eles a compilação da nossa aplicação Golang:

# Start from the official Golang image to build the binary file
FROM golang:1.20 as builder -> GERA LAYER

ENV GO111MODULE=on
WORKDIR /app

COPY . . -> GERA LAYER

RUN go mod download -> GERA LAYER

WORKDIR /app/cmd/repositories-service

RUN CGO_ENABLED=0 GOOS=linux go build -o ./main . -> GERA LAYER

# Multi stage build


FROM alpine:latest -> GERA LAYER

RUN apk --no-cache add ca-certificates -> GERA LAYER

WORKDIR /root/

COPY --from=builder /app/cmd/repositories-service/main . -> GERA LAYER

EXPOSE 8080

CMD ["./main"]

Ao visualizar o Dockerfile pela primeira vez e identificar todos comandos que geram
Layer, nos deparamos com 6 comandos que geram novas layers, porém se
inspecionarmos a imagem, apenas 4 estão presente:

docker inspect igoreulalio/repository-service:v6-a

. . . output removido para facilidade de leitura

"RootFS": {
"Type": "layers",
"Layers": [
"sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470e
"sha256:b628a3cc1206e750fe115340e766b8ac4f3122cbde95dfc0888e023
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf1
"sha256:598affc186cb2864cda396e5d722430beaef1c28d88fdee620e8919
]
}

Uma dessas layers é vazia, pois essa imagem foi buildada juntamente com outra,
portanto apenas 3 layers com conteúdo estão na imagem final.
Com isso, conseguimos atingir uma imagem de apenas 28MB, pequena e segura :)

docker images | grep igoreulalio/repository-service | grep v6-a


igoreulalio/repository-service. v6-a 28MB

COPY vs ADD
Outro conceito que gera grande confusão e que muita das vezes funciona de
maneira semelhantes, podendo trazer comportamentos indesejados.

Ambos comandos tem a mesma funcionalidade na maioria dos casos, copiar um


arquivo para dentro do container durante o build, porém com sutis diferenças.

A regra de ouro aqui é a seguinte: caso seu objetivo seja apenas copiar um arquivo
do build context para o container:

Para exemplificar, cria um arquivo local-file.txt juntamente com o Dockerfile abaixo


e execute seu container:

# Base image
FROM alpine:latest

COPY local-file.txt /app/local-file.txt

Dentro de /app, teremos nosso arquivo local-file.txt.

Da mesma forma com ADD, teremos também o mesmo resultado:

# Base image
FROM alpine:latest

ADD local-file.txt /app/local-file.txt

Para esse cenário, ambos se comportam da mesma forma, a diferença aparece


quando estamos falando de 2 outros cenários:
1. Arquivos tar

2. URLs remotas

# Base image
FROM alpine:latest

ADD local-file.tar.gz /app/

ADD https://example.com/remote-file.txt /app/remote-file.txt

Nesse cenário, que funciona apenas com ADD, estamos copiando um arquivo tar.gz
para dentro do container e também adicionando uma URL remota.

O ADD irá extrair o resultado de local-file.tar.gz para /app/ e também irá se


comunicar com a URL acima, baixar o conteúdo e inserir no arquivo /app/remote-
file.txt.

Por mais que pareça inofensivo, o comando ADD pode abrir brechas para ataques ao
seu sistema, ao permitir que conteúdos remotos sejam baixados e inseridos dentro
da imagem de maneira transparente, portanto, caso seu intuito seja apenas copiar
um arquivo, o que é a realidade para maioria dos casos, use COPY e seja feliz :)

Entrypoint vs CMD
Esses são outros dois comandos frequentemente confundidos quando se trabalha
com Dockerfiles. Ambos são usados para especificar um comando que será
executado quando um contêiner for iniciado a partir da imagem, mas eles são
usados de maneiras ligeiramente diferentes e podem ser combinados de maneiras
interessantes.

A regra de ouro é: use ENTRYPOINT quando você quer que o container execute um
comando específico como um executável, ou seja, sempre executar aquele comando
na inicialização, e CMD quando você quer definir um comando padrão que pode ser
substituído na linha de comando quando o contêiner é iniciado.

Primeiro, vamos entender o uso do CMD:


# Base image
FROM alpine:latest

CMD ["echo", "Hello, World!"]

Builde e rode sua imagem, voce verá Hello, World! no console.

Agora com ENTRYPOINT:

# Base image
FROM alpine:latest

ENTRYPOINT ["echo"]

Builde e rode sua imagem, desta vez passando "Hello, World!" como parametro de
execução:

docker run igoreulalio/sample-docker:v3 "Hello, World!"

Voce vera o mesmo “Hello, World!”, mesmo não tendo especificado que este
comando deveria ser inserido como parametro do echo.

Combinando ambos:

# Base image
FROM alpine:latest

ENTRYPOINT ["echo"]

CMD ["Hello, World!"]

Desta vez, podemos rodar a imagem sem nenhum parametro, ou com parametros:
Ao executar com parametros, voce verá que o parametro sera printado, ao invés de
Hello, World!. Isso acontece porque o parametro substitui o CMD do container,
sendo executado como um parametro para o ENTRYPOINT, neste caso, a função
echo.

Entender esses dois parametros é essencial tanto para desenvolvedores quanto para
adminstradores e devops engineers, pois eles definem como o seu container irá
rodar. Uma outra dica, caso esteja lidando com uma imagem pública ou até mesmo
privada mas que você não tenha ciencia de como foi criada, utilize o docker inspect
para validar tanto CMD quanto ENTRYPOINT defaults da imagem e poder utiliza-la
da forma como faz sentido para o seu caso de uso:

docker inspect grafana/grafana

... output omitido para facilitar a leitura


"Entrypoint": [
"/run.sh"
],
"Cmd": null,

Mesmo sem saber o código que buildou a imagem, consegui entender exatamente o
que irá acontecer quando executar a imagem grafana/grafana: um script chamado
/run.sh irá executar, portanto posso acessar esse script para entender melhor sobre
a sua inicialização.

Outra dica muito importante para troubleshooting: Caso esteja tendo algum
problema na inicialização de um container mas não consegue debugar pois ele
morre antes, sobrescreva o ENTRYPOINT e o CMD de maneira que o comando
executado não falhe.

Vamos aplicar isso na prática para podermos ver o arquivo run.sh do grafana,
supondo que o container esteja dando erro na execução.

docker run -it --entrypoint "sh" grafana/grafana

Neste cenário, estamos utilizando a flag " — entrypoint" para sobrescrever o


entrypoint e a flag -it para termos acesso iterativo dentro do container. Com isso,
conseguimos acessa-lo, mesmo sem ter o processo do grafana rodando. Com isso
conseguimos debugar nosso container, como por exemplo ler o arquivo de
configuração e executar troubleshooting com ferramentas de network.

/usr/share/grafana $ cat /run.sh


#!/bin/bash -e

PERMISSIONS_OK=0

if [ ! -r "$GF_PATHS_CONFIG" ]; then
echo "GF_PATHS_CONFIG='$GF_PATHS_CONFIG' is not readable."
PERMISSIONS_OK=1
fi

... resto do output omitido

Containers Docker Kubernetes Golang AWS

Follow

Written by Igoreulalio Ie
41 Followers

Igor is a Solutions Architect and AWS Community Builder in Serverless.

More from Igoreulalio Ie


Igoreulalio Ie in AWS Tip

Monitoring Lambdas using AWS Powertools


Serverless tecnology brought a lot of good things in game, but how to understand deeply what
are happening with your lambda functions in…

4 min read · Mar 9, 2022

25

Igoreulalio Ie in Towards Dev


MSK and Glue Schema Registry: managed event stream platform on
AWS.
Since Kafka first release, kafka have been used across multiple corporations as Event
Streaming Platform, because of your multiple…

5 min read · Mar 30, 2022

12

Igoreulalio Ie

Desmistificando arquitetura OpenTelemetry e deployando sua primeira


aplicação instrumentada em…
O ecossistema tecnológico está em constante evolução, trazendo consigo desafios e
demandas cada vez mais complexas. Neste contexto, o…

8 min read · Sep 18, 2023

8
Igoreulalio Ie

Melhorando a resiliência dos seus deploys de Lambda com Canary


Deployments.
Ao longo dos anos, inúmeras empresas pensaram em indicadores que de alguma maneira
pudessem metrificar a produtividade de times de pessoas…

6 min read · Aug 18, 2022

58

See all from Igoreulalio Ie

Recommended from Medium


Guillermo Quiros in ITNEXT

K8Studio Kubernetes IDE


It’s been an exhilarating journey since we first embarked on the K8studio project four years
ago. Although there were pauses along the way…

4 min read · Jan 16

595 9

RimonTawadrous
Why UUID7 is better than UUID4 as clustered index in RDBMS
In the Introduction To Database Indexing Article, We discussed database indexes, Their type,
representations, and use cases.

9 min read · Jan 15

680 6

Lists

General Coding Knowledge


20 stories · 825 saves

Natural Language Processing


1122 stories · 591 saves

Aman Pathak in Stackademic

Advanced End-to-End DevSecOps Kubernetes Three-Tier Project using


AWS EKS, ArgoCD, Prometheus…
Project Introduction:

23 min read · Jan 18

756 9
William Donze

Uptime Kuma: self-hosted monitoring tool


Learn how to monitor your infrastructure with uptime kuma

6 min read · Dec 28, 2023

55

Nidhey Indurkar
How did PayPal handle a billion daily transactions with eight virtual
machines?
I recently came across a reddit post that caught my attention: ‘How PayPal Scaled to Billions of
Transactions Daily Using Just 8VMs’…

7 min read · Jan 1

3.3K 36

Jacob Bennett in Level Up Coding

The 5 paid subscriptions I actually use in 2024 as a software engineer


Tools I use that are cheaper than Netflix

· 5 min read · Jan 4

4.3K 55

See more recommendations

Você também pode gostar