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.
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:
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:
{
"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:
# 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:
FROMescolhe a imagem base com Node 24 sobre Alpine Linux.WORKDIRcria ou seleciona/apppara as próximas instruções.COPYacrescenta os dois arquivos à imagem e entrega a posse ao usuárionode.ENVdefine um valor padrão disponível em execução.USERtroca o usuário do processo principal derootparanode.EXPOSEregistra que a aplicação espera TCP na porta 3000; não publica nada.CMDdefine 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:
node_modules
.git
.env
npm-debug.log
coverageNã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:
docker build -q -t dc-cantina:1.0 .-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:
docker image inspect dc-cantina:1.0 --format 'Usuário={{.Config.User}} | Diretório={{.Config.WorkingDir}} | Porta={{json .Config.ExposedPorts}}'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:
docker run -d --name dc-cantina-api -p 127.0.0.1:3066:3000 dc-cantina:1.0O 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:
curl -sS http://127.0.0.1:3066/pedidosO 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:
docker logs dc-cantina-apiAPI Cantina ouvindo em 0.0.0.0:3000
E confirme o usuário efetivo dentro do contêiner:
docker exec dc-cantina-api idUSER 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:
FROM node:24-alpine
WORKDIR /app
CMD ["node", "server.js"]Salve como Dockerfile.erro, construa e rode:
docker build -f Dockerfile.erro -t dc-cantina:erro .
docker run --name dc-cantina-erro dc-cantina:erroError: 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:
docker inspect dc-cantina-erro --format 'Estado={{.State.Status}} | ExitCode={{.State.ExitCode}}'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.
Perguntas frequentes
O arquivo precisa se chamar Dockerfile?
EXPOSE publica a porta da aplicação?
Preciso copiar node_modules para a imagem?
Por que usar USER node?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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
- Docker Docs — Node.js language-specific guide — docs.docker.com
- Docker Docs — Dockerfile reference — docs.docker.com
- Docker Docs — Building best practices — docs.docker.com
- Docker Docs — Build, tag, and publish an image — docs.docker.com


