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:

EtapaInstrução correspondente do DockerfileDescrição
load build definition—Lê o Dockerfile
load metadata for …node:24-alpineFROMConsulta a versão mais recente da imagem base
[1/5] FROM …FROM node:24-alpineBaixa 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 /appWORKDIRCria /app e muda para ela
[3/5] COPY package*.json ./COPYCopia a lista de dependências
[4/5] RUN npm install --omit=devRUNInstala as dependências na imagem
[5/5] COPY . .COPYCopia 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
O navegador exibe Hello from Docker!
Abra http://localhost:3000 no navegador
  • 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 por npm start; em seguida, vem Listening 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 e npm install não precisa ser executado novamente.
  • Somente [5/5] COPY . . foi executada novamente, porque server.js mudou.
$ 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:

  1. O build fica mais rápido, especialmente em projetos com node_modules ou .git grandes.
  2. Alterações nesses arquivos não invalidam o cache de COPY . ..
  3. Evita incluir arquivos confidenciais, como .env e chaves privadas, na imagem.

O node_modules do 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 exclua node_modules no .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 alpine ou slim para 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 apenas node.
  • É 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 .dockerignore para excluir arquivos desnecessários, como node_modules, .git e .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