Boas práticas na otimização de imagens docker

Boas práticas na otimização de imagens docker

Não é incomum você criar um contêiner docker e se assustar com o tamanho da imagem gerada. Entenda que a imagem docker funciona em um conjunto de camadas e parte delas são reaproveitadas em outras imagens, mas ainda assim é importante te-las bastante reduzidas. Há diversas práticas que podem ser consideradas para reduzir o tamanho. Vamos falar sobre elas as boas práticas na otimização de imagens docker.

Mas otimizar imagens Docker não significa apenas reduzir o tamanho em bytes. Entenda que quanto mais recursos a imagem tiver, maior é a superfície de ataques. Por esse motivo contêineres com menos recursos são mais seguros.

Utilização de imagens Alpine

Veja que no docker hub há imagens que prometem reduzir drasticamente o tamanho dos contêineres. Por exemplo: uma imagem de mysql no Ubuntu pesa 145MB contra 36MB numa distribuição Alpine. Vários dos contêineres que trabalho usam a imagens Alpine com a instalação manual do produto que estou interessado em usar (MySQL, NodeJS, Matomo, etc).

Entretanto deve-se observar as diferentes tags de imagens disponibilizadas pelo fornecedor que você está utilizando. Por exemplo: o Matomo (uma espécie de Google Analytics corporativo) possui as imagens com tag -alpine que podem ser utilizadas para gerar imagens mais leves.

Porém, por experiência própria, construir imagens do zero no Alpine costuma gerar imagens mais leves.

https://hub.docker.com/_/matomo

Utilização de imagens Distroless

Imagens Docker podem ser mais pesadas ou mais leves. Observe as imagens geradas pelos fabricantes: normalmente são bastante pesadas cheias de recursos interessantes. Por outro lado elas não costumam ser muito interessantes para ambientes de produção, uma vez que a superfície de ataque é elevada.

Uma prática possível é a criação de imagens distroless. Essas são imagens tão cruas mas tão cruas que não é possível nem rodar um sh. Esse é um projeto construído e mantido pelo google e pode ser muito útil. Veja que as máquinas Alpine são leves mas essas podem ser ainda mais leves.

Quando você for rodar uma aplicação C ou Go, por exemplo, você pode apenas mover os arquivos necessários e rodá-los, sem a necessidade de instalar quaisquer outras dependências externas.

Veja a seguir uma listagem de imagens distroless interessantes:

Instalação específica do que é necessário

Uma prática simples e eficaz é instalar apenas o que é necessário. Se você não precisa do VIM ou NANO então não instale.

Para rodar uma Rust numa imagem Alpine, por exemplo, não é necessário instalar o Rust, mas apenas o LibGCC. O DOCKERFILE abaixo é o suficiente, pesando apenas 8MB. Enquanto a imagem oficial do Rust pesa incríveis 1.31GB

FROM alpine:3.15
RUN apk add libgcc
COPY /home/rust/app/ /
ENTRYPOINT ["./main"]

Multistage building

Essa é uma prática muito comum para construção de imagens docker. Ela consiste em você criar imagens intermediárias que desembocam seus dados entre elas, de modo que você pode utilizar imagens pesadas numa parte do processo, mas a imagem final será outra, mais leve e otimizada.

Para fazer um build com multistage você precisa nomear os comandos FROM do DOCKERFILE e, além disso, precisa copiar arquivos de uma imagem para outra. Veja um exemplo simples:

FROM alpine:latest AS stage1   # Criação de um primeiro estágio em uma imagem Alpine para compilar a aplicação Go

WORKDIR /app
COPY    /app .

RUN apk add --no-cache go
RUN go build app.go

FROM alpine:latest AS stage2
   # Criação do segundo estágio, também com uma imagem Alpine, podém sem a instalação do Go, apenas executando o arquivo compilado no primeiro estágio
WORKDIR /app
COPY --from=stage1 /app .
CMD ["./app"]

Dockerfile com o número adequado de passos

Outra prática recomendável é a criação de um DOCKERFILE com poucos passos. Mas esse ponto deve ser feito com muita sabedoria. Entenda que os passos são colocados em cache de modo que a criação de novas imagens sejam rápidas (exceto se tiver o parâmetro –no-cache do docker build). Desse modo, saiba claramente quais são as etapas que podem ser alteradas com frequência e quais não são. Juntá-las pode ser prejudicial.

Quando falo em juntar linhas no DOCKERFILE me refiro ao uso de comandos RUN com linhas do bash do contêiner, mas essas práticas podem se estender a elementos que dependem unicamente de sua criatividade. Veja um exemplo de DOCKERFILE que não possui a otimização que comentei:

FROM ubuntu:22.04 # Utilização de imagem ubuntu para aplicação .NET sem otimização do dockerfile
LABEL maintainer="ANSELME Containers <[email protected]>"

WORKDIR /app
COPY    /app .

RUN     apt-get update
RUN     apt-get install -y wget
RUN     apt-get install -y apt-transport-https

RUN     wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
RUN     dpkg -i packages-microsoft-prod.deb
RUN     rm packages-microsoft-prod.deb

RUN     apt-get update
RUN     apt-get install -y dotnet-sdk-6.0
RUN     apt-get install -y aspnetcore-runtime-6.0

RUN     dotnet build

RUN     mkdir /dotnet
RUN     apt-get install -y curl
RUN     curl -sSl https://dotnet.microsoft.com/download/dotnet/scripts/v1/dotnet-install.sh -o /app/bin/Debug/net6.0/dotnet-install.sh
RUN     chmod +x /app/bin/Debug/net6.0/dotnet-install.sh;

     
CMD ["./dotnet", "app.dll"]

Mas por outro lado, veja essa imagem com a otimização completa

FROM ubuntu:22.04 AS stage1
LABEL maintainer="ANSELME Containers <[email protected]>"

WORKDIR /app
COPY    /app .

RUN     apt-get update; \
        apt-get install -y wget; \
        apt-get install -y apt-transport-https;

RUN     wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb; \
        dpkg -i packages-microsoft-prod.deb; \
        rm packages-microsoft-prod.deb;

RUN     apt-get update; \
        apt-get install -y dotnet-sdk-6.0; \
        apt-get install -y aspnetcore-runtime-6.0;

RUN     dotnet build

RUN     mkdir /dotnet; \
        apt-get install -y curl; \
        curl -sSl https://dotnet.microsoft.com/download/dotnet/scripts/v1/dotnet-install.sh -o /app/bin/Debug/net6.0/dotnet-install.sh; \
        chmod +x /app/bin/Debug/net6.0/dotnet-install.sh;

FROM alpine:3.15 AS stage2
ARG     DOTNET6_VERSION=6.0.7
WORKDIR /app
COPY --from=stage1 /app/bin/Debug/net6.0/ .

RUN     apk update; \ 
        apk add --no-cache libgdiplus --repository https://dl-3.alpinelinux.org/alpine/edge/testing/ ; \
        apk add --no-cache bash icu-libs krb5-libs libc6-compat libgcc libintl libssl1.1 libstdc++ zlib; 

RUN echo $DOTNET6_VERSION
RUN     ./dotnet-install.sh --runtime dotnet --version $DOTNET6_VERSION  --install-dir /app;        
CMD ["./dotnet", "app.dll"]

Utilize imagens from scratch

Algumas imagen conseguem ser simplificadas de tal modo que ela não precisa ter nada. Por exemplo, os códigos escritos em Go podem ser executados diretamente pelo sistema operacional sem a necessidade de instalar nenhuma depedência.

Seguindo o exemplo do golang, veja que a imagem oficial pesa 965MB, a imagem enxugada com o Alpine Linux pesa 7.29MB, a imagem distroless (gcr.io/distroless/base-debian11) pesa 22MB e por fim a imagem from scratch pesa apenas 1.76 MB. Veja a seguir o dockerfile utilizado nessa cenário

FROM alpine:latest AS stage1

WORKDIR /app
COPY    /app .

RUN apk add --no-cache go
RUN go build app.go

FROM scratch AS stage2
WORKDIR /app
COPY --from=stage1 /app .
CMD ["./app"]

Utilize o arquivo .dockerignore

Semelhante ao .gitignore é possível criar o arquivo .dockerignore. Imagine, por exemplo, que na solução local haja arquivos com senhas que não possam ir para o contêiner, ou arquivos de log, ou quaisquer outras estruturas. O arquivo dockerignore garante que esses arquivos não sejam copiados através do comando COPY. Veja um exemplo simples a seguir:

passphrase.txt
logs/
.git
*.md
.cache
dockerfile

Thiago Anselme
Thiago Anselme - Gerente de TI - Arquiteto de Soluções

Ele atua/atuou como Dev Full Stack C# .NET / Angular / Kubernetes e afins. Ele possui certificações Microsoft MCTS (6x), MCPD em Web, ITIL v3 e CKAD (Kubernetes) . Thiago é apaixonado por tecnologia, entusiasta de TI desde a infância bem como amante de aprendizado contínuo.

Deixe um comentário