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.
Sumário
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:
- gcr.io/distroless/static-debian11
- gcr.io/distroless/base-debian11
- gcr.io/distroless/java11-debian11
- gcr.io/distroless/java17-debian11
- gcr.io/distroless/cc-debian11
- gcr.io/distroless/nodejs-debian11
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
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.