Skip to main content

3 dicas para recriar uma imagem Docker mais rápido (e economizar segundos no CI!)

Escrito por
feature docker rebuild

28 de dezembro de 2023

0 minutos de leitura

O Docker se tornou uma ferramenta indispensável, que simplifica o processo de criar, distribuir e executar aplicações. Com o Docker, desenvolvedores podem empacotar uma aplicação com todas as suas dependências em uma unidade padronizada conhecida como imagem de contêiner.

# Example of a Dockerfile
FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py

Esse Dockerfile é um documento simples que contém todos os comandos necessários para criar uma imagem de contêiner. Essa imagem, por sua vez, servirá de base para um contêiner Docker quando for executada.

Esses contêineres são pacotes leves, independentes e executáveis que incluem tudo o que é necessário para executar um software, como código, ferramentas do sistema, bibliotecas e configurações.

A grande vantagem dos contêineres é a portabilidade. Eles garantem que a aplicação seja executada sem problemas, independentemente do ambiente em que for implantada. Isso simplificou bastante o processo de desenvolvimento de software e se tornou um dos pilares da abordagem DevOps, viabilizando a integração e a implantação contínuas.

A necessidade de criar imagens Docker mais rápido

Embora as imagens de contêiner sejam fundamentais para executar aplicações em diferentes ambientes, criá-las pode levar tempo. Isso é especialmente verdadeiro para aplicações maiores, com muitas dependências.

# Building a container image can take time
docker build -t my-application .

No exemplo acima, o Docker lê o Dockerfile no diretório atual e cria uma imagem chamada my-application. Dependendo da complexidade e do tamanho da aplicação, isso pode levar bastante tempo.

A arte de criar imagens Docker

Criar imagens Docker é uma parte fundamental do trabalho com contêineres. As imagens Docker são, essencialmente, um “instantâneo” de um contêiner Docker em determinado momento e servem de base para criar novos contêineres Docker.

Criar uma imagem Docker envolve a criação de um Dockerfile, um arquivo de texto que contém todos os comandos necessários para criar uma imagem. Um Dockerfile é como uma receita: ele diz ao Docker o que fazer quando você quiser criar uma imagem.

Confira um exemplo de Dockerfile simples:

# Use an official Python runtime as a base image
FROM python:3.7-slim

# Set the working directory in the container
WORKDIR /app

# Add the current directory contents into the container at /app
ADD . /app

# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Make port 80 available to the world outside this container
EXPOSE 80

# Define environment variable
ENV NAME World

# Run app.py when the container launches
CMD ["python", "app.py"]

Depois de criar um Dockerfile, você pode usar o comando docker build para criar uma imagem. Veja como seria:

docker build -t my-python-app .

Esse comando instrui o Docker a criar uma imagem usando o Dockerfile no diretório atual (o . no fim do comando) e a identificá-la (-t) como my-python-app.

Observação: Se você desenvolve em Python ou faz parte de uma equipe DevOps que cria contêineres para aplicações Python, recomendo muito conferir e aplicar as práticas recomendadas para conteinerizar aplicações Python com Docker.

Desafios comuns na criação de imagens Docker

Criar imagens Docker nem sempre é simples. Desenvolvedores e equipes DevOps podem enfrentar vários desafios comuns:

  • Gerenciamento de dependências: um dos desafios mais comuns é gerenciar dependências. As dependências da sua aplicação podem mudar com o tempo, e pode ser difícil manter sua imagem Docker atualizada. Para resolver isso, pode ser útil usar um gerenciador de pacotes que cuide das dependências automaticamente.

  • Tamanho da imagem: outro desafio é manter as imagens Docker o menores possível. Imagens grandes demoram mais para serem criadas e enviadas a um repositório Docker, o que pode deixar seu pipeline de CI/CD mais lento. Para reduzir o tamanho da imagem, você pode usar uma imagem base menor, remover arquivos desnecessários e fazer builds em vários estágios para minimizar o conteúdo da imagem final.

  • Segurança: a segurança é uma preocupação importante ao criar imagens Docker. Você precisa garantir que suas imagens não contenham vulnerabilidades que possam ser exploradas por invasores. Para ajudar com isso, use ferramentas como Docker Bench for Security e Snyk Container para analisar suas imagens em busca de vulnerabilidades.

Dica 1: adicione um arquivo .dockerignore ao seu repositório

No universo do Docker e da segurança de contêineres, eficiência é essencial. Se você é desenvolvedor ou profissional de DevOps, sabe que cada segundo conta nos processos de integração contínua (CI).

Uma maneira simples e eficaz de acelerar a criação de imagens Docker é adicionar um arquivo .dockerignore ao seu repositório. Vamos entender como funciona um arquivo .dockerignore, como ele pode acelerar a criação da imagem Docker e conferir um passo a passo para adicionar um arquivo .dockerignore a um repositório.

O que é um arquivo .dockerignore?

Assim como um arquivo .gitignore, um arquivo .dockerignore é um arquivo de texto que especifica padrões de arquivos e diretórios que o Docker deve ignorar ao criar uma imagem a partir de um Dockerfile. Em outras palavras, ele informa ao Docker quais arquivos ou diretórios devem ser desconsiderados durante o build.

# Sample .dockerignore file
node_modules
npm-debug.log
Dockerfile*
*.md

O trecho de código acima é um exemplo de arquivo .dockerignore que instrui o Docker a ignorar o diretório node_modules, qualquer arquivo npm-debug.log, qualquer Dockerfile (independentemente da extensão) e todos os arquivos Markdown.

Como um arquivo .dockerignore acelera a criação de imagens Docker?

Quando o Docker cria uma imagem a partir de um Dockerfile, ele envia todo o contexto do build (ou seja, o diretório atual e seus subdiretórios) para o daemon do Docker. Sem um arquivo .dockerignore, o daemon recebe arquivos e diretórios desnecessários, o que pode aumentar bastante o tempo de build.

Ao adicionar um arquivo .dockerignore ao seu repositório, você instrui o Docker a desconsiderar arquivos e diretórios desnecessários, reduzindo o tamanho do contexto do build. Assim, o Docker pode criar imagens mais rápido e economizar segundos valiosos de CI.

Passo a passo para adicionar um arquivo .dockerignore a um repositório

Veja como adicionar um arquivo .dockerignore ao seu repositório:

1. Crie um arquivo chamado .dockerignore no diretório raiz do seu repositório.

touch .dockerignore


2. Abra o arquivo .dockerignore recém-criado no editor de texto de sua preferência.

nano .dockerignore

3. Adicione padrões para os arquivos e diretórios que devem ser ignorados durante a criação da imagem Docker. Por exemplo, você pode ignorar todos os arquivos de log e o diretório node_modules, como mostrado abaixo:

*.log
node_modules/

4. Salve e feche o arquivo .dockerignore. Na próxima vez que você criar uma imagem Docker a partir desse repositório, o Docker ignorará os arquivos e diretórios especificados, e o build da imagem será mais rápido!

Um arquivo .dockerignore é uma ferramenta simples, mas poderosa, que pode reduzir bastante o tempo de criação de imagens Docker. Ao adicioná-lo aos seus repositórios, você pode tornar seus processos com Docker e CI mais eficientes e seguros.

Dica 2: use um arquivo de lock de dependências

Um arquivo de lock de dependências é uma ferramenta essencial para fazer builds Docker mais rápidos e determinísticos. Ele fixa as versões das dependências do seu projeto, garantindo que todos os builds futuros usem exatamente as mesmas versões. Isso não só acelera os builds, como também aumenta a confiabilidade e a segurança das imagens Docker.

O que é um arquivo de lock de dependências e por que ele é importante?

Um arquivo de lock de dependências é gerado pelo gerenciador de pacotes e descreve as versões exatas e as dependências do seu projeto em determinado momento. É como um instantâneo das dependências do projeto. No contexto do Docker, ele registra quais versões de software foram usadas para criar uma imagem Docker específica.

O arquivo de lock é essencial por vários motivos. Ele garante que seus builds Docker sejam determinísticos e repetíveis. Isso significa que você terá o mesmo resultado sempre que criar a imagem Docker. Isso é fundamental para depuração e conformidade com requisitos de segurança, pois garante que você sempre saiba exatamente o que há na imagem Docker.

Além disso, um arquivo de lock de dependências pode acelerar bastante os builds Docker. Como ele contém as versões exatas das dependências, o Docker pode armazenar essas camadas em cache e pular a etapa de instalação nos builds seguintes, reduzindo o tempo de build.

Como um arquivo de lock de dependências garante builds mais rápidos e determinísticos

Vamos considerar um Dockerfile simples, sem arquivo de lock:

FROM node:20
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD [ "node", "app.js" ]

Sempre que executamos um build, npm install busca as versões mais recentes das dependências definidas em package.json. Esse processo leva tempo e torna nossos builds não determinísticos, pois as dependências instaladas podem variar de um build para outro.

Observação: Se você cria aplicações Node.js com Docker, recomendo muito conferir as 10 práticas recomendadas para conteinerizar aplicações web Node.js com Docker.

Agora, vamos usar um arquivo de lock:

FROM node:20
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD [ "node", "app.js" ]

Aqui, copiamos o package-lock.json para a imagem Docker e usamos npm ci em vez de npm install. O comando npm ci usa o arquivo de lock para instalar as versões exatas das dependências, garantindo builds determinísticos e permitindo que o Docker armazene a camada de dependências em cache para acelerar os builds.

Como usar um arquivo de lock de dependências no Docker

Para usar um arquivo de lock de dependências no Docker, ele precisa estar incluído no contexto do build. Você pode fazer isso copiando o arquivo para a imagem Docker com o comando COPY no Dockerfile.

Confira um exemplo com um projeto Python que usa Pipfile.lock:

FROM python:3.7
WORKDIR /app
COPY Pipfile Pipfile.lock ./
RUN pip install pipenv && pipenv install --system --deploy
COPY . .
CMD [ "python", "app.py" ]

Neste Dockerfile, copiamos Pipfile e Pipfile.lock para a imagem Docker. Em seguida, usamos pipenv install --system --deploy para instalar as versões exatas das dependências definidas em Pipfile.lock. Isso garante builds determinísticos e otimiza o desempenho ao aproveitar o mecanismo de cache do Docker.

Um arquivo de lock de dependências é uma ferramenta essencial para fazer builds Docker mais rápidos e determinísticos. Ele não só mantém suas imagens Docker consistentes e confiáveis, como também aumenta a segurança delas, garantindo que você sempre saiba quais versões de software estão dentro das imagens.

Dica 3: agrupe os comandos de acordo com a probabilidade de mudança

Práticas eficientes fortalecem a segurança de contêineres e o desenvolvimento de software, e a criação de imagens Docker não é exceção. Uma dessas práticas é agrupar os comandos de acordo com a probabilidade de mudança. Esse método pode economizar bastante tempo e recursos ao recriar imagens Docker. Vamos entender como funciona.

O princípio de agrupar comandos no Docker

No Docker, cada comando escrito em um Dockerfile cria uma nova camada na imagem Docker. O Docker usa um mecanismo de cache para essas camadas e, assim, acelera a criação de imagens.

Ao criar uma imagem, o Docker processa cada comando linha por linha. Se um comando não mudou desde o último build, o Docker usa a camada armazenada em cache em vez de executá-lo novamente, reduzindo bastante o tempo de build.

No entanto, se uma linha mudar, o Docker não poderá usar o cache para essa linha nem para as linhas seguintes. Portanto, se uma linha que muda com frequência estiver no início do Dockerfile, o cache não será aproveitado de forma eficiente, e o Docker precisará recriar a maior parte da imagem a cada vez. É aí que entra o agrupamento de comandos.

Recrie imagens Docker mais rápido agrupando comandos

Ao agrupar e organizar os comandos de acordo com a probabilidade de mudança, você pode otimizar o uso do cache do Docker e acelerar bastante o processo de recriação de imagens Docker.

Em geral, organize seu Dockerfile para deixar no final os comandos que mudam com frequência. Assim, o Docker pode usar as camadas em cache dos comandos que não mudaram e são atualizados com menos frequência, recriando apenas as camadas correspondentes aos comandos que mudaram.

Coloque no início os comandos com menor probabilidade de mudança

Outra dica desta seção para recriar imagens Docker mais rápido é colocar no início do Dockerfile os comandos com menor probabilidade de mudança, melhorando o mecanismo de cache do Docker.

Em um Dockerfile, cada comando que você escreve cria uma nova camada na imagem Docker. Essas camadas são empilhadas umas sobre as outras e formam a imagem Docker final. O Docker usa um mecanismo de cache para essas camadas e acelerar a criação das imagens. Se uma camada não mudou (ou seja, o comando é o mesmo das compilações anteriores), o Docker usa a versão em cache em vez de recriar essa camada.

O detalhe é que o Docker invalida o cache de uma camada se qualquer uma das camadas das quais ela depende tiver sido alterada. Isso significa que, se você modificar um comando no início do Dockerfile, todas as camadas abaixo dele serão recriadas, mesmo que os comandos delas não tenham mudado.

Ao colocar no início do Dockerfile os comandos com menor probabilidade de mudar, você garante que o Docker use o cache dessas camadas na maioria das compilações. Essa abordagem reduz o número de camadas que o Docker precisa recriar e, assim, acelera o processo de compilação.

Exemplos práticos de agrupamento de comandos

Veja este exemplo de Dockerfile:

FROM debian:stretch

RUN apt-get update && apt-get install -y \
    python3 \
    python3-pip

COPY . /app

WORKDIR /app

RUN pip3 install -r requirements.txt

CMD ["python3", "app.py"]

Neste Dockerfile, é provável que o comando COPY . /app mude com frequência à medida que você atualiza o código da aplicação. Ao mover esse comando e o comando RUN pip3 install -r requirements.txt que vem em seguida (que também muda sempre que o arquivo requirements.txt é alterado) para o fim do Dockerfile, o Docker pode usar o cache para o comando RUN apt-get update && apt-get install -y python3 python3-pip, que muda com menos frequência. 

O Dockerfile otimizado ficaria assim:

FROM debian:stretch

RUN apt-get update && apt-get install -y \
    python3 \
    python3-pip

WORKDIR /app

COPY . /app

RUN pip3 install -r requirements.txt

CMD ["python3", "app.py"]

Ao agrupar os comandos de acordo com a probabilidade de mudarem, desenvolvedores e profissionais de DevOps podem otimizar a recriação de imagens Docker, economizar segundos no CI e aprimorar a segurança de contêineres e as práticas de desenvolvimento de software.

Conclusão

A vantagem de recriar imagens Docker mais rapidamente vai além do tempo economizado. Isso influencia significativamente a eficiência do pipeline de CI e o processo geral de desenvolvimento de software.

Um processo mais rápido de criação de imagens Docker reduz o tempo de compilação no pipeline de CI e, consequentemente, acelera o ciclo de feedback para os desenvolvedores. Isso dá mais ritmo ao desenvolvimento de software e permite que os desenvolvedores iterem sobre as mudanças com mais rapidez.

Além disso, há uma redução de custos, pois menos recursos computacionais são usados no pipeline de CI. Na computação em nuvem, em que os custos costumam estar diretamente ligados ao uso de CPU e ao tempo de compilação, acelerar a criação de imagens Docker pode gerar uma economia significativa ao longo do tempo.

Por fim, é possível melhorar a postura de segurança da aplicação. Compilações mais rápidas significam receber mais cedo o feedback sobre possíveis problemas de segurança nas imagens Docker, permitindo corrigi-los rapidamente e, assim, reduzir o período de exposição.

Em conclusão, adotar essas estratégias para recriar imagens Docker mais rapidamente não é apenas algo desejável: é essencial no ritmo acelerado do desenvolvimento de software atual. Isso é fundamental não só para aumentar a velocidade e a eficiência do pipeline de CI, mas também para aprimorar a segurança e reduzir os custos do processo de desenvolvimento de software.

Segurança de contêineres para DevSecOps

Encontre e corrija vulnerabilidades em contêineres gratuitamente com a Snyk.

Publicado em:

Leia mais

Live Stream

Agentes de Remediação, Desmistificados: Por Que Corrigir é Melhor do que Encontrar

Veja como o Remediation Agent da Snyk usa inteligência de segurança, análise de explorabilidade e validação para transformar vulnerabilidades em pull requests prontos para merge.

blog feature snyk secrets aazgdq
Blog

Acabe com a proliferação de segredos: Snyk Secrets já está disponível para todos

Snyk Secrets já está disponível para todos, trazendo detecção contextual com ML, prevenção antes do commit e governança unificada de segredos para a Snyk AI Security Platform.

feature insights context
Blog

Por dentro do comprometimento do keyv npm: malware no preinstall, procedência confiável e hooks de IDE

O keyv 6.0.0 e outras dez versões do npm distribuíram malware executado durante a instalação. Confira as versões afetadas, os hashes, as etapas de detecção e a ordem segura de correção.