Skip to main content

Como escolher a melhor imagem Docker para Node.js

hero docker secrets

30 de setembro de 2022

0 minutos de leitura
How to Choose the Best and Secure Node.js Docker Image

Nota do editor:

Atualizado para incluir a imagem Chainguard Distroless para Node.js.

(31 de agosto de 2023) A versão recomendada do Node.js, os exemplos e os resultados da verificação de vulnerabilidades foram atualizados para refletir as versões LTS mais recentes do Node.js.

Escolher uma imagem Docker para Node.js pode parecer algo simples, mas o tamanho da imagem e as possíveis vulnerabilidades podem afetar significativamente seu pipeline de CI/CD e sua postura de segurança. Então, como escolher a melhor imagem Docker para Node.js?

É fácil não perceber os riscos potenciais de usar FROM node:latest ou apenas FROM node (que é um alias do primeiro). Isso é ainda mais preocupante se você não conhece os riscos gerais de segurança e o tamanho enorme dos arquivos que essas opções introduzem no pipeline de CI/CD.

Veja um exemplo de Dockerfile para Node.js que costuma ser apresentado como referência em tutoriais e posts de blog sobre imagens Docker para Node.js — mas esse Dockerfile tem sérias falhas e não é recomendado:

FROM node
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm install
CMD "npm" "start"

Já apresentei um guia passo a passo com 10 práticas recomendadas para conteinerizar aplicações web Node.js com Docker. Ele aprimora esse exemplo e ajuda você a criar uma imagem Docker para Node.js pronta para produção.

Neste post, vamos usar o exemplo hipotético acima como conteúdo de um Dockerfile para encontrar a imagem Docker ideal para Node.js.

Suas opções de imagem Docker para Node.js

Há várias opções para criar sua imagem Node.js. Elas vão desde a imagem Docker oficial do Node.js, mantida pela equipe principal do Node.js, até tags específicas de imagem Node.js que você pode escolher dentro dessa imagem base. Também há alternativas como criar sua aplicação Node.js sobre uma imagem distroless do Google ou da Chainguard, ou uma imagem scratch básica fornecida pela equipe do Docker.

Entre todas essas opções, qual imagem Docker para Node.js é ideal para você?

Vamos analisar cada uma para entender melhor os benefícios e os riscos potenciais.

Nota do autor: Ao longo deste artigo, vou comparar uma versão do Node.js de um momento específico, lançada por volta de abril de 2024: a versão 22.1.0.

A imagem padrão do Node

Vamos começar pela imagem node, mantida oficialmente pela equipe do Node.js Docker. Ela oferece várias tags de imagem base do Docker, associadas a diferentes distribuições (Debian, Ubuntu ou Alpine) e versões do próprio runtime do Node.js. Também existem tags para arquiteturas de CPU específicas, como amd64 ou arm64x8 (o novo Apple M1).

As tags de imagem node mais comuns para a distribuição Debian, como bullseye e bookworm, são baseadas na imagem buildpack-deps, mantida por outra equipe.

O que acontece quando você cria sua imagem Docker para Node.js com base nessa imagem padrão node, usando apenas a dependência npm fastify? Para simplificar, vamos usar este exemplo em vez de implantar uma aplicação completa.

FROM node
WORKDIR /app
RUN npm install fastify

No mesmo diretório, crie a imagem com docker build --no-cache -t mynode . — Também baixei a imagem node:latest neste exemplo para mostrar a diferença de tamanho. O resultado é:

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
mynode       latest    6978fbd7640d   24 seconds ago   1.13GB
node         latest    2fb0552f149e   11 days ago   1.11GB
  • Não especificamos a versão do runtime do Node.js, então node é um alias para node:latest, que no momento aponta para a versão 22.1.0 do Node.js.

  • Nossa imagem Docker tem 1,13 GB.

  • A imagem base node:latest corresponde a 1,11 GB desse total.

Qual é o conjunto de dependências e vulnerabilidades de segurança dessa imagem mais recente do Node.js? Podemos descobrir executando uma verificação do contêiner. Se quiser acompanhar e executar as mesmas verificações nas suas compilações, você precisará ter uma conta gratuita na Snyk e instalar a Snyk CLI. Para isso, basta executar o comando `npm install -g snyk` ou seguir as instruções na nossa documentação.

Vamos executar snyk container test mynode --file=Dockerfile --exclude-app-vulns. O resultado é:

  • Um total de 413 dependências — bibliotecas de código aberto detectadas pelo gerenciador de pacotes do sistema operacional, como curl/libcurl4, git/git-man ou imagemagick/imagemagick-6-common.

  • Foram encontradas 179 falhas de segurança nessas dependências, como estouros de buffer, erros de uso após liberação de memória, gravações fora dos limites e outros.

  • Imagens mais antigas do Node.js, como a versão 20.5.1, foram identificadas como vulneráveis a diferentes problemas de segurança, como DNS Rebinding, HTTP Request Smuggling e Configuration Hijacking.

Você realmente precisa de wget, git ou curl disponíveis na imagem Node.js da sua aplicação? O cenário não é nada bom: ter centenas de dependências e ferramentas na imagem Docker do Node.js, com centenas de vulnerabilidades associadas a elas, abre muitas possibilidades para ataques.

Opções no Docker Hub para Node.js: node:buster vs. node:bullseye vs. node:bookworm

Ao consultar as tags disponíveis no repositório do Node.js no Docker Hub, você encontra várias alternativas de imagem para Node.js, incluindo node:buster, node:bullseye e node:bookworm.

As três tags de imagem Docker são baseadas em versões da distribuição Debian. A tag buster corresponde ao Debian 10, cujo fim do ciclo de vida ocorreu entre agosto de 2022 e 2024 — portanto, não é uma boa escolha. A tag bullseye corresponde ao Debian 11, considerado a versão "old stable" atual do Debian, com fim do ciclo de vida previsto para junho de 2026. Por fim, bookworm é a versão “stable” atual e, no momento em que este artigo foi escrito, ainda não tinha uma data definida para o fim do ciclo de vida. No entanto, com base no histórico, é razoável supor que isso aconteça em algum momento de 2028.

Nota do autor: Por isso, recomendamos fortemente migrar todas as imagens Docker novas e existentes do Node.js das tags node:buster para node:bullseye, node:bookworm, ou outras alternativas adequadas.

Vamos criar uma nova imagem Docker para Node.js com base em:

FROM node:bookworm

Se você criar uma imagem Node.js com essa tag e comparar com os resultados acima, verá exatamente o mesmo tamanho, a mesma quantidade de dependências e as mesmas vulnerabilidades. Isso acontece porque node, node:latest e node:bookworm apontam para a mesma tag de imagem Node.js.

Tag de imagem Node.js para imagens mais leves

A equipe oficial do Node.js Docker também mantém uma tag de imagem que inclui explicitamente as ferramentas necessárias para um ambiente Node.js funcional, e nada além disso.

Essas tags de imagem Node.js são identificadas pela variante slim, como node:bookworm-slim, ou por uma versão específica do Node.js, como node:20-slim.

Vamos criar uma imagem slim do Node.js com base na versão estável atual do Debian, bookworm:

FROM node:bookworm-slim

O tamanho da imagem já caiu drasticamente: de quase 1 GB para 231 MB. A verificação do conteúdo também revela uma redução significativa no conjunto de softwares, com 89=8 dependências e apenas 37 vulnerabilidades.

A imagem node:bookworm-slim já é um ponto de partida melhor em termos de tamanho e postura de segurança do contêiner.

Uma imagem Docker LTS do Node.js

Até agora, nossas imagens Docker do Node.js eram baseadas na versão atual do Node.js, a versão 22. Mas, de acordo com o cronograma de lançamentos do Node.js, essa versão só entra oficialmente no status Active LTS em outubro de 2024.

E se usássemos sempre as versões de suporte de longo prazo (LTS) nas imagens Docker do Node.js que criamos? Vamos atualizar a tag da imagem e criar uma nova imagem Node.js:

FROM node:lts-bookworm-slim

A versão LTS mais leve do Node.js (20.13.1) tem uma quantidade semelhante de dependências e vulnerabilidades de segurança na imagem, que fica um pouco menor, com 219 MB.

Como vimos, embora você possa ter requisitos específicos para escolher entre as versões LTS e Current do runtime do Node.js, nenhuma delas afeta significativamente o conjunto de softwares da imagem.

node:alpine é uma escolha melhor para uma imagem Node.js?

A equipe do Node.js Docker mantém a tag de imagem node:alpine e suas variantes, que combinam versões específicas das distribuições Alpine Linux com as versões do runtime do Node.js.

O projeto Alpine Linux é conhecido por oferecer imagens extremamente pequenas. Isso é ótimo porque significa um conjunto menor de softwares e, consequentemente, menos vulnerabilidades expostas.

FROM node:alpine
...

Com isso, a imagem Docker fica com 167 MB, 64 MB menor que as imagens Node.js slim. Na tag de imagem Alpine, até o dia em que este artigo foi escrito, foram detectadas apenas 17 dependências do sistema operacional e uma vulnerabilidade de segurança. Isso pode indicar que a tag de imagem alpine é uma boa opção para reduzir tanto o tamanho da imagem quanto a quantidade de vulnerabilidades.

node:alpine é a melhor escolha para uma imagem Docker de Node.js?

A variante de imagem Alpine para Node.js pode oferecer um tamanho geral menor e até menos vulnerabilidades. No entanto, é importante saber que o projeto Alpine usa musl como implementação da biblioteca padrão C, enquanto as tags de imagem Node.js do Debian, como bullseye ou slim, usam a implementação glibc. Essas diferenças podem causar problemas de desempenho, bugs funcionais ou até falhas da aplicação, devido às diferenças na biblioteca C subjacente. Itamar Turner-Trauring também escreveu sobre problemas inesperados em tempo de execução relacionados às tags de imagem Alpine para imagens Docker de Python.

Escolher uma tag de imagem alpine para Node.js significa, na prática, optar por um runtime não oficial do Node.js. A equipe do Node.js Docker não oferece suporte oficial a compilações de imagens de contêiner baseadas em Alpine. Por isso, ela afirma que as tags de imagem baseadas em Alpine são experimentais, podem apresentar inconsistências e estão disponíveis nas compilações não oficiais a seguir. Veja o que diz o repositório de tags de imagem Unofficial Builds:

O projeto Unofficial Builds tenta fornecer binários básicos do Node.js para algumas plataformas que não têm suporte ou têm apenas suporte parcial do Node.js. O projeto não oferece garantias e seus resultados não são testados rigorosamente. As compilações disponíveis em nodejs.org seguem padrões muito elevados de qualidade de código, suporte às plataformas relevantes e prazos e métodos de entrega. As compilações disponibilizadas pelo Unofficial Builds passam por poucos testes ou nenhum; as plataformas talvez não estejam incluídas na infraestrutura oficial de testes do Node.js. Essas compilações são oferecidas para conveniência da comunidade de usuários, mas espera-se que essas comunidades ajudem na manutenção.

Algumas observações importantes sobre a compatibilidade da tag de imagem alpine com o Node.js:

  • Incompatibilidade com Yarn (issue #1716).

  • Se você precisa de node-gyp para compilar bindings nativos em C para outras plataformas, Python, uma dependência desse processo, não está disponível na imagem Alpine e você terá que resolver isso por conta própria (issue #1706).

Imagens Docker Distroless para Node.js

Os últimos itens da comparação são as imagens Distroless. Há duas opções principais: as originais imagens de contêiner distroless do Google e as mais recentes imagens de contêiner distroless da Chainguard.

O que é uma imagem Docker distroless?

Essas imagens são ainda mais leves que a tag de imagem Node.js slim , pois incluem apenas a aplicação e suas dependências de runtime. Assim, uma imagem Docker distroless não tem gerenciador de pacotes, shell nem dependências de outras ferramentas de uso geral, o que reduz seu tamanho e o conjunto de vulnerabilidades.

Imagem Distroless do Google

O projeto Google Distroless mantém uma imagem Docker distroless específica para o runtime do Node.js, identificada pelo namespace completo gcr.io/distroless/nodejs22-debian12 e disponível no registro de contêineres do Google (a parte gcr.io).

Observação: também existem combinações mais antigas de Debian e Node.js, mas escolha uma versão que ainda receba manutenção. Para conferir as imagens compatíveis mais recentes, consulte a tabela no repositório do Distroless no GitHub.

Como as imagens de contêiner do Distroless não incluem software, podemos usar um fluxo de trabalho Docker com vários estágios para instalar as dependências do contêiner e copiá-las para as imagens distroless:

FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY . /app
RUN npm install

FROM gcr.io/distroless/nodejs22-debian12
COPY --from=build /app /usr/src/app
WORKDIR /usr/src/app
CMD ["server.js"]

A criação dessa imagem Docker distroless gera um arquivo de 177 MB, reduzindo o tamanho em relação às variantes de tags de imagem slim e alpine .

Se você está pensando em usar imagens Docker distroless, é importante considerar alguns pontos:

  • Elas são baseadas nas versões estáveis atuais do Debian, ou seja, estão atualizadas e têm uma data de fim do suporte bem distante — o que é ótimo.

  • Como são baseadas no Debian, elas usam a implementação glibc e têm menos chances de causar surpresas com problemas em produção.

  • Você logo vai perceber que a equipe do Distroless não mantém versões específicas do runtime Node.js. Isso significa que você precisa usar a tag de uso geral latest, que recebe atualizações frequentes, ou instalar a imagem com base no hash SHA256 correspondente a um determinado momento.

Imagem Distroless da Chainguard

Outra opção de imagem distroless é a Chainguard. A Chainguard oferece várias imagens, incluindo versões para versões mais antigas do Node e versões com FIPS habilitado. No plano gratuito para desenvolvedores, há duas imagens disponíveis: "latest", que executa o Node 22.1.0 no momento da redação deste artigo, e "latest-dev", que amplia a imagem latest com um gerenciador de pacotes e um shell para desenvolvimento e depuração.

Podemos usar um exemplo muito parecido com o anterior, da imagem distroless do Google:

FROM cgr.dev/chainguard/node:latest-dev AS build
WORKDIR /app
COPY . /app
USER root
RUN npm install

FROM cgr.dev/chainguard/node:latest
COPY --from=build /app /usr/src/app
WORKDIR /usr/src/app
CMD ["server.js"]

Isso gera uma imagem de 142 MB, semelhante à imagem Distroless do Google. Nenhuma vulnerabilidade foi reportada.

Vale mencionar que todas as Chainguard Images são criadas com base na distribuição Linux Wolfi, da Chainguard. A Wolfi é compilada com glibc, portanto não há problemas de compatibilidade com musl.

Comparação entre tags de imagens Docker do Node.js

A tabela a seguir resume nossa comparação entre diferentes tags de imagens Docker do Node.js, com dados atualizados em 15 de maio de 2024:

Tag da imagem

Versão do runtime Node.js

Dependências do sistema operacional

Vulnerabilidades de segurança do sistema operacional

Vulnerabilidades altas e críticas

Vulnerabilidades médias

Vulnerabilidades baixas

Vulnerabilidades do runtime Node.js

Tamanho da imagem

Yarn disponível

node:latest

22.1.0

413

179

3

0

176

0

1135MB

Sim

node:bookworm

22.1.0

413

179

3

0

176

0

1135MB

Sim

node:bookworm-slim

22.1.0

88

37

2

0

35

0

233MB

Sim

node:lts-bookworm-slim

20.13.1

88

37

2

0

35

0

219MB

Sim

node:alpine

22.1.0

17

1

0

0

1

0

145MB

Sim

22.1.0

8

16

0

0

16

0

186MB

Não

cgr.dev/chainguard/node:latest

22.1.0

25

0

0

0

0

0

134MB

Não

cgr.dev/chainguard/node:latest-dev

22.1.0

66

0

0

0

0

0

651MB

Sim

Vamos analisar os dados e as conclusões que obtivemos com cada uma das diferentes tags de imagem do Node.js e decidir qual é a mais adequada.

Paridade entre desenvolvimento e produção

Se você está escolhendo uma tag de imagem do Node.js pensando em manter a consistência entre desenvolvimento e produção — ou seja, quer que os dois ambientes sejam exatamente iguais —, talvez essa batalha já esteja perdida. Na maioria dos casos, os três principais sistemas operacionais usam implementações diferentes da biblioteca C. O Linux usa glibc, o Alpine usa musl e o macOS tem sua própria implementação BSD libc.

Tamanho da imagem Docker

Às vezes, o tamanho importa. Mas, falando com mais precisão, o objetivo não é ter a menor imagem possível, e sim a menor área de software possível. Nesse caso, as tags de imagem slim não diferem muito em tamanho das correspondentes alpine: todas têm, em média, cerca de 211 MB. Ainda assim, a área de software das imagens slim continua bem maior (89, contra 17 do alpine) e, por isso, elas têm uma superfície de vulnerabilidade maior (28 na slim contra 0 na alpine).

Vulnerabilidades de segurança

As vulnerabilidades são uma preocupação importante e já foram o foco de muitos artigos que explicam por que você deve reduzir o tamanho das imagens de contêiner. No entanto, o contexto das questões de segurança também importa muito.

Deixando de lado as imagens node e node:bullseye, que têm uma área de software maior e mais vulnerabilidades de segurança, podemos nos concentrar no conjunto menor de tipos de imagem. Na comparação entre slim, alpine e distroless, a diferença no número de vulnerabilidades de segurança altas e críticas não é grande: varia de 0 a 2. É um risco administrável, que pode até ser irrelevante para o caso de uso da sua aplicação.

Suporte e resiliência

É muito importante que a equipe do Node.js Docker possa priorizar e resolver problemas relacionados às compilações das imagens de contêiner em tempo hábil. Com qualquer imagem que não seja uma das tags oficiais baseadas no Debian, você praticamente não consegue marcar esse item como concluído na sua lista.

Ao usar as tags de imagem node ou node:22.1.0-bookworm-slim, você recebe a versão mais recente do runtime Node.js, tanto ao optar por uma imagem completa do sistema operacional quanto por uma versão com menos dependências. Embora seja uma versão par (Node.js 22.1.0), no momento da redação deste artigo ela ainda não faz parte do ciclo de suporte de longo prazo (LTS). Isso significa que incluirá novas versões de outros componentes dependentes, como as versões mais recentes do próprio npm, conhecido por introduzir comportamentos inesperados que podem levar um tempo para se estabilizar.

Em resumo?

A imagem Docker ideal para Node.js seria uma versão enxuta de um sistema operacional Debian moderno, com uma versão estável do Node.js em suporte ativo de longo prazo (LTS).

A escolha é a tag de imagem node:lts-bookworm-slim do Node.js. Prefiro usar tags de imagem determinísticas, então faria apenas uma pequena alteração: usaria o número da versão subjacente em vez do alias lts.

A tag ideal de imagem Docker para Node.js é node:20.13.1-bookworm-slim.

Se você trabalha em uma equipe DevOps experiente, capaz de oferecer suporte a imagens base personalizadas, minha segunda recomendação seria a tag de imagem distroless do Google, pois ela mantém a compatibilidade com glibc para as versões oficiais do runtime Node.js. Esse fluxo de trabalho exige manutenção, então só recomendo essa opção se você tiver condições de mantê-la.

Segurança de contêineres para DevSecOps

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