Conteúdo
Tutorial de Dockerfile: empacote seu próprio programa em uma imagem
Dockerfile é um arquivo de texto simples que descreve, linha por linha, como criar uma imagem. O Docker executa essas etapas e gera uma imagem. Depois, você pode iniciá-la com docker run, assim como faria com o nginx.
Projeto de exemplo
Crie uma pasta chamada hello-node e coloque nela dois arquivos.
package.json:
{
"name": "hello-node",
"version": "1.0.0",
"scripts": { "start": "node server.js" }
}
server.js:
const http = require("http");
const server = http.createServer((req, res) => {
res.end("Hello from Docker!\n");
});
server.listen(3000, () => console.log("Listening on port 3000"));
Você não precisa instalar Node.js no seu computador. O Node.js fica na imagem; esse é justamente o propósito do Docker.
Escreva o Dockerfile
Na mesma pasta, crie um arquivo chamado Dockerfile (sem extensão):
# 1. Use a imagem oficial do Node.js como base
FROM node:24-alpine
# 2. Defina o diretório de trabalho dentro do contêiner
WORKDIR /app
# 3. Primeiro, copie a lista de dependências e instale-as
COPY package*.json ./
RUN npm install --omit=dev
# 4. Depois, copie o restante do código
COPY . .
# 5. Indique a porta usada pelo contêiner
EXPOSE 3000
# 6. Comando executado quando o contêiner é iniciado
CMD ["npm", "start"]
Agora a pasta contém três arquivos:
$ ls
Dockerfile
package.json
server.js
Construindo a imagem: docker build
docker build -t hello-node .
-t hello-node é o nome da imagem, e o . no final indica que a pasta atual será usada como contexto de build (build context). O Docker envia os arquivos dessa pasta para o mecanismo de build, e a instrução COPY só pode copiar arquivos que estejam dentro do contexto de build.
Durante o build, o terminal exibe o progresso atualizado dinamicamente. Ao terminar, a tela fica assim:
$ docker build -t hello-node .
[+] Building 9.9s (10/10) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 399B 0.0s
=> [internal] load metadata for docker.io/library/node:24-alpine 1.9s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 3.6s
=> => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> => sha256:7ce7dcbafb7f750cf0c6be3c30e34372b06dd1182fa67f6ae30782c8150c5444 0B / 444B 7.9s
=> => sha256:02f40d53b06f6b91fec0cd6a6719656da99270bff8ee54ed8dcdf4c157763fc 1.26MB / 1.26MB 0.7s
=> => sha256:3c504d5a797be28f9de43147d49cd8ecb2eea46dfaf7beb8715db695d5f0a 56.52MB / 56.52MB 1.3s
=> => sha256:e2de96513ba9eb53b431787ec8a65cdde380ac4772a3e4c4b714dcfde2a102b 3.85MB / 3.85MB 0.6s
=> => extracting sha256:e2de96513ba9eb53b431787ec8a65cdde380ac4772a3e4c4b714dcfde2a102b5 0.6s
=> => extracting sha256:3c504d5a797be28f9de43147d49cd8ecb2eea46dfaf7beb8715db695d5f0a7f0 2.2s
=> => extracting sha256:02f40d53b06f6b91fec0cd6a6719656da99270bff8ee54ed8dcdf4c157763fc1 0.1s
=> => extracting sha256:7ce7dcbafb7f750cf0c6be3c30e34372b06dd1182fa67f6ae30782c8150c5444 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 759B 0.0s
=> [2/5] WORKDIR /app 0.0s
=> [3/5] COPY package*.json ./ 0.0s
=> [4/5] RUN npm install --omit=dev 1.4s
=> [5/5] COPY . . 0.0s
=> exporting to image 2.7s
=> => exporting layers 0.2s
=> => exporting manifest sha256:b311d231396fab904d11e3fbac8bade8302e8e7401fe14ba1e4a736f4fed 0.0s
=> => exporting config sha256:fa1ad1088d08bf3f3f035dfe9dbed3cf5a008492316926ba23fae3e58d6852 0.0s
=> => exporting attestation manifest sha256:0ea092cede0047cad91ad77043f79ef84c7cab9153913acf 0.0s
=> => exporting manifest list sha256:62664c2b6134a7c365eacb28019730c69b7d1d0fd855c559f6994c3 0.0s
=> => naming to docker.io/library/hello-node:latest 0.0s
=> => unpacking to docker.io/library/hello-node:latest 2.5s
Cada linha com => corresponde a uma etapa:
| Etapa | Instrução correspondente do Dockerfile | Descrição |
|---|---|---|
load build definition | — | Lê o Dockerfile |
load metadata for …node:24-alpine | FROM | Consulta a versão mais recente da imagem base |
[1/5] FROM … | FROM node:24-alpine | Baixa a imagem base (os sha256:… abaixo mostram o progresso do download de cada camada; na primeira vez, demora mais) |
load build context | — | Envia o contexto de build (desta vez, apenas 759B, pois há somente 3 arquivos pequenos) |
[2/5] WORKDIR /app | WORKDIR | Cria /app e muda para ela |
[3/5] COPY package*.json ./ | COPY | Copia a lista de dependências |
[4/5] RUN npm install --omit=dev | RUN | Instala as dependências na imagem |
[5/5] COPY . . | COPY | Copia o restante do código |
exporting to image | — | Salva o resultado como uma imagem e a nomeia hello-node:latest |
EXPOSE e CMD apenas definem configurações; não geram etapas de build.
Executando
$ docker images hello-node
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
hello-node:latest 62664c2b6134 245MB 62.2MB
$ docker run -d -p 3000:3000 --name hello hello-node
2c4745bd0187d9ba7f0e9af096519ce9a2ad4436395490f5af24cd142f3bef4c
$ curl -s http://localhost:3000
Hello from Docker!
$ docker logs hello
> hello-node@1.0.0 start
> node server.js
Listening on port 3000

- A imagem tem cerca de 245 MB, e a maior parte desse tamanho vem do próprio Node.js (node:24-alpine).
- Em
docker logs,> node server.jsé uma mensagem exibida pornpm start; em seguida, vemListening on port 3000, do próprio programa.
Modificando o código e reconstruindo: cache
Altere o texto em server.js para Hello from Docker! (v2) e faça o build novamente:
$ docker build -t hello-node .
[+] Building 0.4s (10/10) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 399B 0.0s
=> [internal] load metadata for docker.io/library/node:24-alpine 0.3s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 294B 0.0s
=> CACHED [2/5] WORKDIR /app 0.0s
=> CACHED [3/5] COPY package*.json ./ 0.0s
=> CACHED [4/5] RUN npm install --omit=dev 0.0s
=> [5/5] COPY . . 0.0s
=> exporting to image 0.1s
=> => exporting layers 0.0s
=> => exporting manifest sha256:667a883ab027b5db474215519a69eef0b162988f9f6384d75c7e6e819ebc 0.0s
=> => exporting config sha256:c3795d6af309587aa9f021692bf628df45d1d38a5e12d05388447644423a01 0.0s
=> => exporting attestation manifest sha256:4598cd1b9ff7979933f7bc06c48df78bb906278618ad587a 0.0s
=> => exporting manifest list sha256:fb09b954543fd2fa9a8d93763672d6a0b8d3ac26e7751e3b3f69b79 0.0s
=> => naming to docker.io/library/hello-node:latest 0.0s
=> => unpacking to docker.io/library/hello-node:latest 0.0s
Desta vez, levou apenas 0.4 segundo:
[2/5],[3/5]e[4/5]estão marcadas como CACHED: essas etapas são exatamente iguais às da execução anterior, então o resultado anterior é reutilizado enpm installnão precisa ser executado novamente.- Somente
[5/5] COPY . .foi executada novamente, porqueserver.jsmudou.
$ docker run --rm -d -p 3000:3000 --name hello hello-node
9d4e4fc490b7b2e5d4c4c5ba729359c63271cbfa7934f6fff011a4fd4d6e87ca
$ curl -s http://localhost:3000
Hello from Docker! (v2)
$ docker stop hello
hello
Por que copiar package.json primeiro
Cada linha do Dockerfile gera uma camada, e o Docker armazena essas camadas em cache. Quando uma linha (e os arquivos que ela copia) muda, todas as etapas seguintes precisam ser refeitas.
Se você começar com COPY . ., o cache de npm install será invalidado a cada alteração em uma linha de código, e será preciso instalar todos os pacotes novamente. Copie primeiro apenas package*.json e depois instale: assim, a reinstalação só será necessária quando a lista de pacotes mudar. Quanto mais pacotes o projeto tiver, maior será a diferença. Veja uma comparação detalhada em Cache de build e otimização.
.dockerignore: não envie arquivos desnecessários para o build
Durante o desenvolvimento local, a pasta costuma conter muitos arquivos, como node_modules, .git e arquivos de log. Para demonstrar, coloque um node_modules de 30 MB na pasta e faça o build novamente. Observe a linha load build context:
$ docker build -t hello-node .
[+] Building 2.0s (10/10) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 399B 0.0s
=> [internal] load metadata for docker.io/library/node:24-alpine 0.2s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> [internal] load build context 0.4s
=> => transferring context: 30.01MB 0.3s
=> CACHED [2/5] WORKDIR /app 0.0s
=> CACHED [3/5] COPY package*.json ./ 0.0s
=> CACHED [4/5] RUN npm install --omit=dev 0.0s
=> [5/5] COPY . . 0.1s
=> exporting to image 1.3s
=> => exporting layers 1.0s
=> => exporting manifest sha256:5c28f64104e51f64822405ae7a67a9016f62e9c5a4c8425b5b92c47791af 0.0s
=> => exporting config sha256:21b97fa4b8887f00cdf80b0dbeb6658f8dd678b8c4008f8d1dcb5c191e82ba 0.0s
=> => exporting attestation manifest sha256:bc9366f473fd5bb6fdffec42cdcd8155bd12b28cfc6ac23d 0.0s
=> => exporting manifest list sha256:f36d091892aca26420e44f681f5279c82f8881c32eb6d77978d99ee 0.0s
=> => naming to docker.io/library/hello-node:latest 0.0s
=> => unpacking to docker.io/library/hello-node:latest 0.2s
transferring context: 30.01MB: todo o node_modules é enviado ao mecanismo de build, e COPY . . também o copia para a imagem, sobrescrevendo a versão instalada na imagem com npm install.
Crie um .dockerignore e liste os arquivos que não devem ser enviados para o build:
node_modules
.git
.env
*.log
Faça o build novamente:
$ docker build -t hello-node .
[+] Building 0.4s (10/10) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 399B 0.0s
=> [internal] load metadata for docker.io/library/node:24-alpine 0.3s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 69B 0.0s
=> [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 159B 0.0s
=> CACHED [2/5] WORKDIR /app 0.0s
=> CACHED [3/5] COPY package*.json ./ 0.0s
=> CACHED [4/5] RUN npm install --omit=dev 0.0s
=> [5/5] COPY . . 0.0s
=> exporting to image 0.1s
=> => exporting layers 0.0s
=> => exporting manifest sha256:c7dd5ef36303b2a808ee6b594946ee3c029769363a3e7eecfbc53f1c3795 0.0s
=> => exporting config sha256:b23dccadd37348cdcc5e2ca60934e15f7f7f309f477651ab90b6f3759e2e25 0.0s
=> => exporting attestation manifest sha256:217b64bdf5112df5d8d6131ebb08c0fbdf3b30db091103a9 0.0s
=> => exporting manifest list sha256:4063457524bbb10ef5c91def983dc1385e7aac61245c61fc8a4d568 0.0s
=> => naming to docker.io/library/hello-node:latest 0.0s
=> => unpacking to docker.io/library/hello-node:latest 0.0s
O contexto do build passou a ter 159B. Vantagens do .dockerignore:
- O build fica mais rápido, especialmente em projetos com
node_modulesou.gitgrandes. - Alterações nesses arquivos não invalidam o cache de
COPY . .. - Evita incluir arquivos confidenciais, como
.enve chaves privadas, na imagem.
O
node_modulesdo host pode conter módulos nativos compilados para Windows ou macOS. Copiá-los para um contêiner Linux pode causar erros. Em projetos Node.js, sempre excluanode_modulesno.dockerignore.
Parar o contêiner
$ time docker stop hello
hello
real 0m 0.69s
user 0m 0.01s
sys 0m 0.00s
Ele parou em apenas 0,7 segundo. O processo principal do contêiner é npm. Ao receber o SIGTERM enviado por docker stop, ele o encaminha para o node e termina normalmente. Se você usar o node diretamente como processo principal, precisará tratar o SIGTERM no programa; caso contrário, docker stop aguardará os 10 segundos completos. Veja os testes em Referência completa dos comandos do Dockerfile.
Dicas
- Escolha uma imagem base na variante
alpineouslimpara reduzir bastante o tamanho da imagem (veja a comparação de tamanhos em Imagens). - Fixe a versão da imagem base, por exemplo,
node:24-alpine; não escreva apenasnode. - É recomendável usar o formato de array JSON em
CMD(["npm", "start"]), pois ele deixa o comportamento mais explícito.
A próxima seção explica cada instrução do Dockerfile em detalhes.
Resumo
- O Dockerfile começa com
FROM, e cada instrução gera uma camada. docker build -t nome .faz o build; o.no final é o contexto do build.- Copie primeiro a lista de dependências e depois instale os pacotes. Assim, o cache pode ser usado quando você alterar apenas o código.
- Use
.dockerignorepara excluir arquivos desnecessários, comonode_modules,.gite.env.
Exercícios
Exercício 1: Altere o texto da resposta em server.js e faça o build novamente. Quais etapas mostram CACHED?
Resposta
FROM, WORKDIR, COPY package*.json e RUN npm install ficam CACHED. Somente as etapas depois de COPY . . são executadas novamente, porque apenas o código mudou.
Exercício 2: Mova COPY package*.json ./ e RUN npm install no Dockerfile para depois de COPY . .. Em seguida, altere server.js novamente e faça o build. O que muda?
Resposta
RUN npm install também será executado novamente, e o build ficará mais lento. Como o conteúdo de COPY . . mudou, cada camada posterior precisa ser reconstruída. Consulte o cache de build.
Perguntas frequentes
O arquivo precisa se chamar Dockerfile?
Por padrão, sim. Para usar outro nome, especifique-o com docker build -f nome-do-arquivo .; veja os parâmetros em [docker build](/pt/docker/docker-build/).
Preciso instalar Node.js no meu computador?
Não. O Node.js fica na imagem, e o build e a execução acontecem dentro do contêiner.
Existe uma ferramenta que gere um Dockerfile para mim?
O Docker Desktop inclui o docker init. Responda a algumas perguntas para gerar um Dockerfile e um compose.yaml; veja o teste em [docker init](/pt/docker/docker-init/).
Atualizado 14 de outubro de 2026