Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

Dockerfile para Node.js: criar imagem e rodar uma API

Escreva um Dockerfile para uma API Node, entenda build context, COPY, USER, EXPOSE e CMD e corrija um MODULE_NOT_FOUND reproduzido.

Rodolfo Mori5 min de leitura

Um Dockerfile descreve como transformar os arquivos de uma aplicação Node numa imagem reproduzível. Ao final desta lição, você vai construir uma API HTTP, executá-la sem Node instalado no host e reconhecer por que um contêiner encerra com MODULE_NOT_FOUND.

O pré-requisito é distinguir imagem e contêiner. A aplicação usa apenas node:http, então não há pacote externo para esconder o que o Docker está fazendo. Se servidor ainda for novidade, consulte a lição de servidor HTTP no Node e depois volte aqui.

A bancada de embalagem enxerga somente o carrinho

Imagine a expedição de uma pequena cantina. Existe uma lista de embalagem, uma bancada e um carrinho com os itens autorizados a entrar no pacote. O Dockerfile é a lista, o builder é a bancada e o build context é o carrinho. Uma instrução COPY não alcança algo que ficou fora do carrinho.

Voltando ao termo técnico: no comando docker build ... ., o ponto final é o caminho do contexto de build. Docker envia esse conjunto de arquivos ao builder. O Dockerfile pode copiar arquivos dali para camadas da imagem; não pode subir para ../ e buscar qualquer coisa da máquina.

O .dockerignore filtra o carrinho antes do envio. Ele reduz transferência, evita invalidar cache com arquivo irrelevante e, principalmente, impede que .env, .git e node_modules entrem no contexto por acidente.

A API mínima que vai entrar na imagem

Crie uma pasta vazia e salve este server.js:

js
import { createServer } from 'node:http';

const port = Number(process.env.PORT ?? 3000);

const server = createServer((request, response) => {
  const body = JSON.stringify({
    servico: 'cantina',
    node: process.version,
    caminho: request.url,
  });

  response.writeHead(200, {
    'content-type': 'application/json; charset=utf-8',
  });
  response.end(body);
});

server.listen(port, '0.0.0.0', () => {
  console.log(`API Cantina ouvindo em 0.0.0.0:${port}`);
});

O endereço 0.0.0.0 é parte do exemplo, não enfeite. Ele faz o servidor ouvir em todas as interfaces do contêiner. Se a API escutar apenas 127.0.0.1, ela responde dentro do próprio contêiner, mas a porta publicada pelo Docker não consegue entregar a conexão.

O package.json informa ao Node que o arquivo usa módulos ESM e registra o comando inicial:

json
{
  "name": "cantina-docker",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node server.js"
  }
}

Esta API não tem dependências externas, portanto não precisa de npm install. Num projeto com pacotes, copie package.json e package-lock.json primeiro e rode npm ci; só depois copie o código. Assim uma edição no servidor não invalida a camada mais cara de dependências.

Cada instrução do Dockerfile responde uma pergunta

Crie um arquivo chamado exatamente Dockerfile, sem extensão:

text
# syntax=docker/dockerfile:1
FROM node:24-alpine

WORKDIR /app
COPY --chown=node:node package.json ./
COPY --chown=node:node server.js ./

ENV NODE_ENV=production
USER node
EXPOSE 3000
CMD ["npm", "start"]

Leia a receita de cima para baixo:

  • FROM escolhe a imagem base com Node 24 sobre Alpine Linux.
  • WORKDIR cria ou seleciona /app para as próximas instruções.
  • COPY acrescenta os dois arquivos à imagem e entrega a posse ao usuário node.
  • ENV define um valor padrão disponível em execução.
  • USER troca o usuário do processo principal de root para node.
  • EXPOSE registra que a aplicação espera TCP na porta 3000; não publica nada.
  • CMD define o comando padrão na forma de array, sem um shell intermediário.

A linha # syntax seleciona a sintaxe moderna do Dockerfile para o builder. Ela é um parser directive, não um comentário comum apesar do #.

O arquivo que impede bagagem indevida

Na mesma pasta, crie .dockerignore:

text
node_modules
.git
.env
npm-debug.log
coverage

Não coloque um segredo no Dockerfile e tente apagá-lo depois. Camadas anteriores podem preservar o conteúdo. O correto é não enviar o arquivo ao contexto e fornecer configuração em tempo de execução. A lição de variáveis de ambiente no Node explica como separar código e configuração.

O .dockerignore não é controle de acesso. Ele protege este caminho de build; permissão do repositório, cofre de segredo e revisão continuam necessários.

Build transforma receita em imagem

Rode o comando na pasta que contém os quatro arquivos. O ponto é o contexto:

bash
docker build -q -t dc-cantina:1.0 .
sha256:e6b285043cc0daa6f740e982f4a6684e5a11de5c78379a9b03127ff27f49595d

-t dc-cantina:1.0 atribui repositório e tag locais. -q esconde o progresso e deixa o identificador final visível. Seu digest pode mudar se qualquer arquivo, metadado ou versão de base mudar.

Confira se três decisões chegaram à configuração da imagem:

bash
docker image inspect dc-cantina:1.0 --format 'Usuário={{.Config.User}} | Diretório={{.Config.WorkingDir}} | Porta={{json .Config.ExposedPorts}}'
Usuário=node | Diretório=/app | Porta={"3000/tcp":{}}

Esse resultado prova o usuário, o diretório e a porta documentada. Ainda não há contêiner nem porta do host.

Rodar publica uma porta de verdade

Crie o contêiner e conecte a porta 3066 da sua máquina à 3000 dele:

bash
docker run -d --name dc-cantina-api -p 127.0.0.1:3066:3000 dc-cantina:1.0
a3e8793791ad3c950d139e1bc4840dafd46d5ce6bd9279c96a5b15bb69e05785

O prefixo 127.0.0.1 limita a publicação ao próprio host, adequado para este laboratório local. Depois que o processo iniciar, faça uma requisição:

bash
curl -sS http://127.0.0.1:3066/pedidos
{"servico":"cantina","node":"v24.19.0","caminho":"/pedidos"}

O JSON prova três coisas ao mesmo tempo: a porta atravessou o isolamento, a rota chegou ao servidor e o Node da imagem é v24.19.0.

Veja a inicialização que o processo enviou à saída padrão:

bash
docker logs dc-cantina-api
> start > node server.js

API Cantina ouvindo em 0.0.0.0:3000

E confirme o usuário efetivo dentro do contêiner:

bash
docker exec dc-cantina-api id
uid=1000(node) gid=1000(node) groups=1000(node)

USER node não foi apenas escrito na receita; o id comprova o comportamento em execução.

Erro real: CMD aponta para um arquivo que não entrou

Agora reproduza um Dockerfile incompleto. Ele define o comando, mas esquece o COPY:

text
FROM node:24-alpine
WORKDIR /app
CMD ["node", "server.js"]

Salve como Dockerfile.erro, construa e rode:

bash
docker build -f Dockerfile.erro -t dc-cantina:erro .
docker run --name dc-cantina-erro dc-cantina:erro
node:internal/modules/cjs/loader:1520 throw err; ^

Error: Cannot find module ‘/app/server.js’ at Module._resolveFilename (node:internal/modules/cjs/loader:1517:15) at wrapResolveFilename (node:internal/modules/cjs/loader:1071:27) at defaultResolveImplForCJSLoading (node:internal/modules/cjs/loader:1095:10) at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1122:12) at Module._load (node:internal/modules/cjs/loader:1294:5) at wrapModuleLoad (node:internal/modules/cjs/loader:255:19) at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5) at node:internal/main/run_main_module:33:47 { code: ‘MODULE_NOT_FOUND’, requireStack: [] }

Node.js v24.19.0

O nome técnico é módulo de entrada ausente na imagem. Traduzindo: CMD pediu /app/server.js, mas nenhuma camada colocou esse arquivo lá. O contêiner foi criado, o processo Node tentou iniciar, devolveu exit code 1 e encerrou.

Comprove sem depender só do stack trace:

bash
docker inspect dc-cantina-erro --format 'Estado={{.State.Status}} | ExitCode={{.State.ExitCode}}'
Estado=exited | ExitCode=1

A correção mínima é restaurar COPY server.js ./ e reconstruir. Reiniciar o contêiner antigo não funciona, porque ele continua ligado à imagem incompleta.

Missão: mudar a resposta e provar que houve novo build

Troque servico: 'cantina' por servico: 'cantina-escolar', gere a tag dc-cantina:1.1 e execute outro contêiner na porta 3067. A missão passa quando o curl da porta 3066 ainda devolve cantina e o da 3067 devolve cantina-escolar. Isso comprova que contêiner antigo não muda quando você cria outra imagem.

Finalize removendo os contêineres do laboratório, sem apagar imagens e volumes de outros projetos. O próximo passo é tirar dados importantes da camada gravável com volumes Docker. Para uma preparação de produção mais completa, continue depois no deploy de API Node.

  • docker
  • dockerfile
  • node
  • imagem docker
  • api
  • seguranca

Perguntas frequentes

O arquivo precisa se chamar Dockerfile?
Esse é o nome padrão procurado por docker build. Outro nome funciona com a opção -f, mas Dockerfile é a convenção que reduz configuração e deixa o projeto reconhecível por pessoas e ferramentas.
EXPOSE publica a porta da aplicação?
Não. EXPOSE documenta a porta esperada na imagem. Para acessar pelo host, use -p no docker run ou ports no Compose.
Preciso copiar node_modules para a imagem?
Não. Copie package.json e o lockfile, execute npm ci durante o build e exclua node_modules no .dockerignore. Assim as dependências são instaladas para o sistema e a arquitetura da imagem.
Por que usar USER node?
Para que o processo da aplicação não rode como root dentro do contêiner. Isso não elimina outras medidas de segurança, mas reduz permissões e a consequência de uma falha explorada.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Docker Engine 29.5.3, Docker Compose 5.1.4 e Node 24.19.0 (node:24-alpine) no macOS ARM64, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. Docker Docs — Node.js language-specific guide — docs.docker.com
  2. Docker Docs — Dockerfile reference — docs.docker.com
  3. Docker Docs — Building best practices — docs.docker.com
  4. Docker Docs — Build, tag, and publish an image — docs.docker.com

Continue por aqui