Alternar tema

Dockerfile para iniciantes: crie sua primeira imagem Docker do zero

Easton editorial illustration: image-layer stack

O terminal exibia mais um erro: COPY ../config.json: no such file or directory. Era a oitava falha de build naquela noite. O projeto funcionava normalmente na máquina local, mas começava a apresentar todo tipo de erro assim que era empacotado em uma imagem Docker. Depois de procurar no Stack Overflow e testar várias respostas bem avaliadas, a imagem ainda cresceu de 200 MB para 2 GB.

Ao abrir o Dockerfile do projeto, a tela estava cheia de FROM, RUN, COPY e CMD. Cada palavra isolada parecia simples, mas o conjunto era difícil de entender. Copiar exemplos de tutoriais raramente funcionava de primeira, e as mensagens de erro não ajudavam: o problema estava no caminho ou na instrução usada?

180 vezes
Diferença de tamanho
Alpine vs. imagem completa
10 vezes
Redução de tamanho
Agrupamento de instruções RUN
30 vezes
Build mais rápido
5 minutos → 10 segundos
Source: Dados de testes práticos

Um Dockerfile não é tão misterioso quanto parece. Depois que você entende a função de cada instrução e os erros mais frequentes, tudo se resume a alguns conceitos essenciais. Este artigo mostra, de forma direta, como criar sua primeira imagem Docker do zero. Cada instrução vem acompanhada de código real, além dos três erros que mais confundem iniciantes. Ao final, você terá condições de escrever um Dockerfile funcional para um projeto Node.js ou Python.

O que é um Dockerfile?

Em termos simples, Dockerfile é um arquivo de texto que descreve todas as etapas necessárias para construir uma imagem Docker. Pense nele como a planta de uma reforma: primeiro o piso, depois a pintura e, por fim, as luminárias. O mecanismo do Docker segue essas instruções em ordem, prepara a aplicação passo a passo e empacota o resultado em uma imagem.

Essa imagem funciona como um “instantâneo do ambiente” pronto para ser executado. Se uma aplicação Node.js precisa do Node 18, de determinados pacotes npm e do código-fonte, o Dockerfile coloca tudo isso dentro da imagem. Qualquer pessoa que receber essa imagem poderá executar docker run sem precisar configurar o ambiente manualmente.

Na prática, o Dockerfile faz três coisas:

  1. Escolhe um ambiente base, como Node.js 18
  2. Adiciona seu código e suas dependências
  3. Informa qual comando o contêiner deve executar ao iniciar

Depois de escrever o Dockerfile, basta executar um comando docker build para gerar a imagem. Parece simples? De fato, não é complicado, mas os detalhes fazem diferença. Vamos conhecer as instruções essenciais.

Seis instruções essenciais do Dockerfile

Fluxo completo para criar uma imagem Docker do zero

Aprenda passo a passo a escrever um Dockerfile, desde a escolha da imagem base até o build e a execução da primeira imagem Docker

Estimated time: PT20M

  1. 1

    Step 1: Etapa 1: escolha a imagem base (FROM)

    FROM deve ser a primeira instrução. Escolha uma imagem base adequada:
  2. 2

    Step 2: Etapa 2: defina o diretório de trabalho (WORKDIR)

    Use WORKDIR /app para definir o diretório de trabalho
  3. 3

    Step 3: Etapa 3: copie os arquivos de dependências e instale-as (COPY + RUN)

    Primeiro copie package*.json e depois execute npm install
  4. 4

    Step 4: Etapa 4: copie o código da aplicação (COPY)

    Copie o código-fonte para o contêiner
  5. 5

    Step 5: Etapa 5: documente a porta (EXPOSE)

    Use EXPOSE 3000 para declarar a porta utilizada pelo contêiner
  6. 6

    Step 6: Etapa 6: defina o comando de inicialização (CMD)

    Use CMD [“npm”, “start”] para definir o comando padrão executado ao iniciar o contêiner
  7. 7

    Step 7: Etapa 7: faça o build e execute

    Crie a imagem:

FROM — escolha da imagem base

FROM deve ser a primeira instrução do Dockerfile, com exceção de comentários e ARG. Ela define a “fundação” do ambiente.

Assim como uma construção começa pela base, um Dockerfile começa com uma imagem base. Sua aplicação usa Node.js? Escolha node:18-alpine. Para um projeto Python, python:3.11-slim é uma boa opção. Se o Nginx será usado como proxy reverso, você pode partir diretamente de nginx:alpine.

# Usa a versão Alpine Linux do Node.js 18 como imagem base
FROM node:18-alpine

Vale entender a diferença entre alpine e slim. alpine é uma imagem extremamente leve, baseada no Alpine Linux, com cerca de 5 MB e adequada para produção. Uma imagem completa do Node pode chegar a 900 MB, uma diferença de 180 vezes. Porém, alpine usa musl libc em vez de glibc, e algumas dependências nativas podem apresentar erros. Se ocorrer um erro de compilação incomum, experimente node:18-slim.

Erro comum de iniciante: escolher qualquer imagem do Node e descobrir depois que a versão não atende aos requisitos do projeto. Confira a versão do Node indicada em package.json e use a versão correspondente em FROM.

RUN — execução de comandos durante o build

RUN executa comandos enquanto a imagem é construída. Ele pode instalar programas, criar diretórios ou alterar arquivos de configuração. O ponto principal é que cada instrução RUN cria uma nova camada na imagem.

Veja um exemplo ruim:

# ❌ Abordagem ruim: cria três camadas na imagem
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get clean

Cada RUN acrescenta uma camada, como as camadas de uma cebola. Por causa do sistema de armazenamento em camadas do Docker, excluir um arquivo em uma etapa posterior não remove os dados gravados em uma camada anterior. Assim, a imagem continua grande. Esse erro é fácil de cometer: sete instruções RUN separadas podem transformar uma imagem em um arquivo de 2 GB que demora muito para chegar ao servidor.

A maneira correta é encadear os comandos com &&:

# ✅ Abordagem recomendada: cria apenas uma camada
RUN apt-get update && \
    apt-get install -y python3 && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

A barra invertida \ permite quebrar a linha para melhorar a leitura. O último rm é importante: ele remove o cache dos pacotes e pode economizar dezenas de megabytes.

Outro cuidado: nunca escreva RUN apt-get update isoladamente. O Docker armazena cada camada em cache. Se update estiver em uma camada separada, uma instalação posterior poderá usar um cache antigo e deixar de encontrar uma versão nova. Coloque update e install sempre na mesma instrução.

COPY vs. ADD — cópia de arquivos

As duas instruções copiam arquivos para a imagem, mas COPY é simples e previsível, enquanto ADD oferece mais funções e pode gerar efeitos inesperados. A recomendação oficial do Docker é clara: se COPY resolver, não use ADD.

Veja primeiro o uso básico de COPY:

# Copia um único arquivo
COPY package.json /app/

# Copia um diretório inteiro
COPY ./src /app/src

# Copia todo o conteúdo do diretório atual para /app no contêiner
COPY . /app

Parece simples, mas existe uma armadilha importante: o caminho é relativo ao contexto de build, não ao Dockerfile.

O que é o contexto de build? É o diretório indicado pelo último ponto (.) no comando docker build. Se você executar docker build . na raiz do projeto, essa raiz será o contexto de build. COPY só poderá acessar arquivos dentro dela e de seus subdiretórios.

Essa é a causa do erro citado no começo:

# ❌ Incorreto: tenta sair do contexto de build
COPY ../config.json /app/
COPY /opt/myfile.txt /app/

A primeira linha tenta acessar o diretório pai; a segunda usa um caminho absoluto. Ambas falham. Esse limite existe para segurança e para garantir builds reproduzíveis: o processo não pode acessar qualquer arquivo da máquina host.

Há duas soluções:

  • Mover config.json para dentro do diretório do projeto
  • Executar o build no diretório acima: docker build -f myproject/Dockerfile .

Agora, sobre ADD: além de copiar arquivos, ele pode extrair pacotes tar automaticamente e baixar arquivos por URL:

# ADD extrai o arquivo automaticamente
ADD myarchive.tar.gz /app/

# ADD pode baixar uma URL, mas isso não é recomendado
ADD https://example.com/file.txt /app/

Pode parecer conveniente, mas o comportamento não é explícito. Quem lê uma instrução ADD não sabe imediatamente se o arquivo será extraído. Se precisar extrair algo, prefira RUN tar -xzf; para downloads, use RUN curl. Assim, a intenção fica clara.

WORKDIR — definição do diretório de trabalho

WORKDIR equivale ao comando cd do Linux: ele define o diretório de trabalho das instruções seguintes. Se o diretório não existir, o Docker o criará automaticamente.

WORKDIR /app
COPY . .  # Agora os arquivos são copiados para /app
RUN npm install  # Executado dentro de /app

Prefira caminhos absolutos. Caminhos relativos são calculados a partir do WORKDIR anterior e podem causar confusão.

Com WORKDIR, você não precisa repetir cd /app && em cada RUN, o que deixa o Dockerfile mais limpo.

CMD vs. ENTRYPOINT — comando de inicialização do contêiner

Essas duas instruções costumam gerar dúvidas. A forma mais simples de lembrar é: CMD pode ser sobrescrito; ENTRYPOINT não.

CMD define o comando padrão executado quando o contêiner inicia:

CMD ["node", "server.js"]

Nesse caso, docker run my-app executa node server.js. Porém, se você usar docker run my-app npm test, CMD será sobrescrito e o comando executado será npm test.

ENTRYPOINT, por sua vez, define o processo principal do contêiner e não é sobrescrito:

ENTRYPOINT ["node"]
CMD ["server.js"]

Com essa combinação, docker run my-app executa node server.js, enquanto docker run my-app script.js executa node script.js. ENTRYPOINT permanece fixo, e os argumentos de CMD ou de docker run são acrescentados depois dele.

Quando usar cada opção?

  • Apenas CMD: serviços de aplicação que podem exigir formas diferentes de inicialização, como npm start em produção e npm test em testes
  • ENTRYPOINT + CMD: imagens de ferramentas cujo comando principal é fixo e apenas os argumentos mudam, como um script Python em que python é o comando e o nome do script é o argumento
  • Apenas ENTRYPOINT: cenários muito específicos em que o contêiner executa uma única função

Veja uma comparação:

# Cenário 1: aplicação web (usa CMD)
FROM node:18-alpine
WORKDIR /app
COPY . .
CMD ["npm", "start"]
# docker run my-app → npm start
# docker run my-app npm test → npm test (CMD é sobrescrito)

# Cenário 2: ferramenta Python (usa ENTRYPOINT + CMD)
FROM python:3.11-slim
ENTRYPOINT ["python"]
CMD ["main.py"]
# docker run my-tool → python main.py
# docker run my-tool script.py → python script.py

Uma frase ajuda a fixar: ENTRYPOINT define “o que fazer”, enquanto CMD define “como fazer”.

ENV — variáveis de ambiente

ENV define variáveis de ambiente que ficam disponíveis durante a execução do contêiner. Essas variáveis também podem ser usadas por instruções posteriores, como RUN e CMD.

ENV NODE_ENV=production
ENV PORT=3000

# Usa a variável em RUN
RUN echo "Environment: $NODE_ENV"

# O código da aplicação também pode acessar essas variáveis
CMD ["node", "server.js"]

Usos comuns:

  • Definir NODE_ENV=production para informar ao Node.js que o ambiente é de produção
  • Alterar PATH para adicionar comandos personalizados
  • Configurar parâmetros da aplicação, como porta e endereço do banco de dados

As variáveis definidas com ENV permanecem na imagem final. Não use essa instrução para informações sensíveis, como senhas. Passe os valores ao executar o contêiner com docker run -e ou use Docker Secrets.

Prática: crie sua primeira imagem

Depois da teoria, é hora de praticar. Vamos usar uma aplicação Node.js simples para criar uma primeira imagem Docker passo a passo.

Etapa 1: prepare o projeto

Crie uma aplicação Node.js mínima:

mkdir my-node-app
cd my-node-app

Crie o arquivo package.json:

{
  "name": "my-node-app",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}

Crie server.js:

const express = require('express');
const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.send('Hello from Docker!');
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Etapa 2: escreva o Dockerfile

Crie um arquivo chamado Dockerfile, sem extensão, na raiz do projeto:

# 1. Escolhe a imagem base
FROM node:18-alpine

# 2. Define o diretório de trabalho
WORKDIR /app

# 3. Copia os arquivos de dependências para aproveitar o cache
COPY package*.json ./

# 4. Instala as dependências
RUN npm install --production

# 5. Copia o código da aplicação
COPY . .

# 6. Documenta a porta
EXPOSE 3000

# 7. Inicia a aplicação
CMD ["npm", "start"]

Por que copiar package.json separadamente do código-fonte? A resposta está no mecanismo de cache do Docker.

O Docker processa as instruções em ordem, e cada uma gera uma camada. Quando uma camada muda, todas as camadas posteriores precisam ser reconstruídas. package.json muda pouco, mas o código-fonte muda com frequência. Se você copiar tudo antes de instalar as dependências, qualquer alteração no código exigirá uma nova instalação, o que torna o build muito mais lento.

Com a ordem acima, enquanto package.json não mudar, o Docker reutiliza a camada de dependências em cache e segue diretamente para a cópia do código.

Etapa 3: crie a imagem

Na raiz do projeto, execute:

docker build -t my-node-app:1.0 .

Significado dos parâmetros:

  • -t my-node-app:1.0: atribui uma tag à imagem no formato nome:versão
  • .: define o diretório atual como contexto de build

O terminal mostrará várias mensagens, e cada etapa corresponderá a uma instrução do Dockerfile. Se tudo correr bem, a saída terminará com Successfully built xxx.

Etapa 4: execute o contêiner

docker run -p 3000:3000 my-node-app:1.0

Significado dos parâmetros:

  • -p 3000:3000: faz o mapeamento no formato porta-do-host:porta-do-contêiner
  • my-node-app:1.0: indica a imagem que será executada

O terminal exibirá Server running on port 3000.

Etapa 5: confira o resultado

Abra http://localhost:3000 no navegador. Se você vir Hello from Docker!, deu certo.

Pressione Ctrl+C para interromper o contêiner.

Resumo do processo

O fluxo completo é:

  1. Escrever o código, com package.json e server.js
  2. Escrever o Dockerfile para informar ao Docker como empacotar o projeto
  3. Criar a imagem com docker build
  4. Executar o contêiner com docker run

Não é tão complicado quanto parecia. O essencial é entender a função de cada instrução, o contexto de build e o mecanismo de cache.

Como evitar os erros mais comuns

Agora que você já viu a abordagem correta, vamos analisar os erros que mais atingem iniciantes. Conhecê-los pode poupar bastante tempo.

Erro 1: caminho incorreto no contexto de build

Sintoma: COPY retorna no such file or directory, embora o arquivo exista.

Causa: o caminho de COPY é relativo ao contexto de build, não ao Dockerfile.

# ❌ Incorreto: tenta acessar o diretório pai
COPY ../config.json /app/

# ❌ Incorreto: tenta acessar um caminho absoluto
COPY /opt/myfile.txt /app/

Soluções:

  1. Mova o arquivo para dentro do diretório do projeto
  2. Ajuste o comando de build: docker build -f subdir/Dockerfile .-f indica o caminho do Dockerfile, enquanto o ponto final mantém o diretório acima como contexto

Há ainda uma armadilha menos evidente. Ao executar docker build . na raiz, o Docker envia todo o diretório ao daemon, incluindo pastas grandes como node_modules e .git. Isso pode significar vários gigabytes transferidos e muitos minutos de espera antes mesmo do início do build.

Crie um arquivo .dockerignore para excluir o que não é necessário:

node_modules
.git
.env
*.log

Erro 2: camadas demais aumentam o tamanho da imagem

Sintoma: a imagem ocupa vários gigabytes, embora o código tenha apenas alguns megabytes.

Causa: cada RUN, COPY e ADD cria uma nova camada. Mesmo que um arquivo seja apagado depois, os dados continuam presentes na camada anterior.

# ❌ Cria sete camadas, e cada uma preserva seus dados
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN curl -o tool.sh https://example.com/tool.sh
RUN chmod +x tool.sh
RUN ./tool.sh
RUN rm tool.sh  # A exclusão não resolve: tool.sh continua na camada anterior

Solução: agrupe as instruções RUN para instalar, usar e limpar tudo na mesma camada:

# ✅ Cria apenas uma camada, então a limpeza funciona
RUN apt-get update && \
    apt-get install -y curl git && \
    curl -o tool.sh https://example.com/tool.sh && \
    chmod +x tool.sh && \
    ./tool.sh && \
    rm tool.sh && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Sete instruções RUN separadas podem produzir uma imagem de 2 GB. Agrupá-las pode reduzir o resultado para 200 MB, uma diferença de dez vezes.

10 vezes
Redução de tamanho
Source: Instruções RUN agrupadas: 2 GB → 200 MB

Erro 3: invalidação do cache de dependências

Sintoma: toda execução do build reinstala as dependências e demora demais.

Causa: a ordem de COPY está errada. Quando o código e os arquivos de dependências são copiados juntos, qualquer mudança no código aciona uma nova instalação.

# ❌ Ordem errada: qualquer alteração no código reinstala as dependências
COPY . .
RUN npm install

O cache do Docker segue a ordem das instruções. Nesse exemplo, até uma pequena mudança no código invalida a camada de COPY e força a execução de npm install novamente.

Solução: copie primeiro os arquivos que descrevem as dependências, instale-as e só depois copie o código-fonte:

# ✅ Ordem correta: só uma mudança em package.json reinstala as dependências
COPY package*.json ./
RUN npm install
COPY . .

Assim, alterações no código não executam npm install novamente. O build pode cair de 5 minutos para 10 segundos.

Dica extra: EXPOSE não é obrigatório

Muitos tutoriais incluem EXPOSE 3000, e isso pode dar a impressão de que a porta não funciona sem a instrução. Na verdade, EXPOSE apenas documenta qual porta a imagem usa. O contêiner pode funcionar sem ela.

O comando docker run -p é o que realmente controla o mapeamento:

# Funciona mesmo que o Dockerfile não tenha EXPOSE
docker run -p 3000:3000 my-app

Ainda assim, vale a pena incluir EXPOSE para facilitar a compreensão de quem usa a imagem.

Conclusão

Para começar a usar Dockerfile, concentre-se em três pontos:

  1. Entenda as instruções essenciais: FROM escolhe a imagem base, RUN instala recursos, COPY move arquivos e CMD inicia a aplicação; WORKDIR e ENV complementam o processo
  2. Compreenda o contexto de build: COPY só pode acessar o diretório indicado pelo último ponto no comando docker build; não tente acessar diretórios acima nem caminhos absolutos
  3. Aproveite o cache: coloque primeiro as operações que mudam pouco, como a instalação de dependências, e deixe para depois as que mudam com frequência, como a cópia do código; agrupe instruções RUN para reduzir o número de camadas

Agora escolha um projeto pequeno e teste. Comece com um Dockerfile básico que simplesmente funcione, sem buscar a perfeição. Quando estiver mais confortável, avance para builds multi-stage e outras técnicas de redução de imagem.

Docker não precisa ser difícil; o mais importante é praticar. Os primeiros erros podem tomar uma noite inteira, mas, depois que os conceitos se encaixam, o processo fica muito mais simples.

Próximos assuntos para estudar:

  • Docker Compose para gerenciar aplicações com vários contêineres
  • Builds multi-stage para reduzir ainda mais o tamanho das imagens
  • Redes e volumes do Docker para comunicação entre contêineres e persistência de dados

Boa sorte na criação da sua primeira imagem Docker. Se tiver alguma dúvida, deixe um comentário.

FAQ

Quais são as principais instruções de um Dockerfile?
As seis principais instruções são:
1) FROM escolhe a imagem base e deve ser a primeira instrução
2) RUN executa comandos durante o build; use && para agrupar comandos e reduzir camadas
3) COPY copia arquivos; o caminho é relativo ao contexto de build e não pode usar ../ nem caminhos absolutos
4) WORKDIR define o diretório de trabalho; prefira caminhos absolutos
5) CMD define o comando de inicialização do contêiner e pode ser sobrescrito
6) ENV define variáveis de ambiente

Também existem ENTRYPOINT, que fixa o comando principal, e EXPOSE, que apenas documenta a porta.
Por que COPY ../config.json gera erro?
O caminho de COPY é relativo ao contexto de build, não ao Dockerfile.

O contexto de build é o diretório indicado pelo último ponto (.) no comando docker build. COPY só pode acessar arquivos dentro desse diretório e de seus subdiretórios; não pode acessar o diretório pai nem caminhos absolutos.

Soluções:
• Mova o arquivo para dentro do diretório do projeto
• Ou ajuste o comando de build: docker build -f subdir/Dockerfile .

Crie um .dockerignore para excluir pastas grandes, como node_modules e .git, e acelerar o build.
Como reduzir o tamanho de uma imagem Docker?
Três métodos:

1) Use uma imagem Alpine:
• Cerca de 5 MB contra 900 MB de uma imagem completa, uma diferença de 180 vezes

2) Agrupe instruções RUN:
• Encadeie os comandos com && para instalar, usar e limpar tudo na mesma camada
• Isso pode reduzir uma imagem de 2 GB para 200 MB, ou seja, 10 vezes menor

3) Crie um .dockerignore para excluir arquivos desnecessários

Sempre escreva apt-get update e install juntos para evitar o uso de um cache antigo.
Como usar o cache do Docker para acelerar o build?
O segredo é colocar primeiro as operações que mudam pouco e deixar por último as que mudam com frequência.

Ordem correta:
• Primeiro, copie package*.json e instale as dependências
• Depois, copie o código-fonte
• Assim, enquanto package.json não mudar, a camada de dependências não será reconstruída
• O build pode cair de 5 minutos para 10 segundos, uma aceleração de 30 vezes

Ordem errada:
• Copiar todos os arquivos antes de instalar as dependências
• Toda alteração no código força uma nova instalação, deixando o build muito lento
Qual é a diferença entre CMD e ENTRYPOINT?
CMD pode ser sobrescrito pelos argumentos de docker run; ENTRYPOINT não.

Casos de uso:
1) Apenas CMD: serviços de aplicação que podem precisar de formas diferentes de inicialização, como npm start em produção e npm test em testes
2) ENTRYPOINT + CMD: imagens de ferramentas com comando principal fixo e parâmetros variáveis, como um script Python
3) Apenas ENTRYPOINT: cenários muito específicos

Lembre-se: ENTRYPOINT define ‘o que fazer’, enquanto CMD define ‘como fazer’.
A instrução EXPOSE é obrigatória?
Não. EXPOSE serve apenas como documentação para indicar qual porta a imagem usa. O contêiner funciona mesmo sem essa instrução.

Quem realmente controla o mapeamento de portas é o comando docker run -p. Mesmo que o Dockerfile não tenha EXPOSE, docker run -p 3000:3000 my-app permitirá o acesso normalmente.

Ainda assim, é recomendável usar EXPOSE para facilitar a compreensão da porta utilizada pela imagem.
Qual é a diferença entre imagens Alpine e slim?
Alpine é baseada no Alpine Linux:
• Tem cerca de 5 MB e é adequada para produção
• Usa musl libc em vez de glibc, então algumas dependências nativas podem apresentar erros
• Uma imagem completa pode ter 900 MB, uma diferença de 180 vezes

Se você encontrar erros de compilação inesperados, experimente a variante slim, como node:18-slim.

Regra prática: comece com Alpine e troque para slim se houver problemas de compatibilidade.

15 min de leitura · Publicado em: 17 dez 2025 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog