Usar imagens prontas do Docker Hub é o ponto de partida. O passo seguinte — e o mais importante para qualquer profissional de DevOps — é saber construir imagens customizadas que empacotam aplicações reais.
O Dockerfile é o arquivo de texto que descreve, passo a passo, como uma imagem deve ser construída. Cada instrução do Dockerfile cria uma camada na imagem final. Entender como essas camadas funcionam é a diferença entre uma imagem bem construída e uma imagem pesada, lenta de construir e difícil de manter.
Estrutura Básica de um Dockerfile
Um Dockerfile mínimo para uma aplicação Node.js:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["node", "src/index.js"]
Cada instrução tem um papel específico.
FROM — define a imagem base. Todo Dockerfile começa aqui. A imagem base fornece o sistema operacional e, neste caso, o runtime do Node.js. O sufixo alpine indica uma variante baseada no Alpine Linux — uma distribuição minimalista de apenas 5MB que resulta em imagens significativamente menores.
WORKDIR — define o diretório de trabalho dentro do container. Todos os comandos subsequentes são executados a partir desse diretório. Se o diretório não existir, o Docker o cria automaticamente.
COPY — copia arquivos do contexto de build (a máquina local) para dentro da imagem. O primeiro argumento é a origem, o segundo é o destino dentro da imagem.
RUN — executa um comando durante a construção da imagem. O resultado é persistido na camada gerada. Usado para instalar dependências, compilar código, criar diretórios.
EXPOSE — documenta que o container escuta na porta indicada. É apenas documentação — não publica a porta automaticamente. A publicação acontece no docker run -p.
CMD — define o comando padrão executado quando o container inicia. Deve ser especificado no formato de array JSON (forma exec), não como string.
Construindo e Testando a Imagem
Com o Dockerfile criado na raiz do projeto:
# Constrói a imagem com uma tag
docker build -t minha-app:1.0.0 .
# O ponto final indica que o contexto de build é o diretório atual
Acompanhar a saída do build é valioso — cada linha corresponde a uma instrução do Dockerfile e mostra o hash da camada gerada:
[+] Building 12.4s (10/10) FINISHED
=> [internal] load build definition from Dockerfile
=> [1/6] FROM node:20-alpine
=> [2/6] WORKDIR /app
=> [3/6] COPY package*.json ./
=> [4/6] RUN npm ci
=> [5/6] COPY . .
=> exporting to image
Rodando e testando:
docker run -d -p 3000:3000 --name minha-app minha-app:1.0.0
curl http://localhost:3000
docker logs minha-app
A Ordem das Instruções Importa — e Muito
O Docker utiliza um mecanismo de cache por camada: se uma instrução e todos os seus predecessores não mudaram desde o último build, o Docker reutiliza a camada cacheada em vez de reconstruí-la. Isso acelera enormemente o ciclo de desenvolvimento.
O problema é que qualquer mudança invalida o cache de todas as camadas subsequentes. Por isso a ordem das instruções no Dockerfile não é arbitrária — ela deve colocar o que muda com menos frequência primeiro e o que muda com mais frequência por último.
Considere a diferença entre essas duas abordagens:
Ordem ineficiente — invalida o cache de dependências a cada mudança de código:
FROM node:20-alpine
WORKDIR /app
COPY . . # copia tudo, incluindo o código
RUN npm ci # instala dependências APÓS o código
CMD ["node", "src/index.js"]
Ordem eficiente — dependências são cacheadas separadamente do código:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./ # copia apenas o manifesto de dependências
RUN npm ci # instala dependências — esta camada só é reconstruída
# quando package.json muda
COPY . . # copia o código — muda frequentemente, mas não invalida
# a camada de dependências acima
CMD ["node", "src/index.js"]
Na prática, a segunda abordagem reduz o tempo de build de dezenas de segundos para poucos segundos na maioria dos ciclos de desenvolvimento.
O Arquivo .dockerignore
Assim como o .gitignore evita que arquivos desnecessários entrem no repositório, o .dockerignore evita que arquivos desnecessários entrem no contexto de build — o conjunto de arquivos enviados ao Docker daemon durante o docker build.
# .dockerignore
node_modules
.git
.gitignore
*.log
.env
.env.*
dist
coverage
README.md
Dockerfile
.dockerignore
Sem o .dockerignore, a pasta node_modules inteira seria enviada ao daemon e depois sobrescrita pelo npm ci — um desperdício considerável de tempo e banda.
Multi-stage Builds: Imagens Enxutas para Produção
Um padrão avançado mas essencial: o multi-stage build permite usar múltiplas imagens base em um único Dockerfile, copiando apenas os artefatos necessários de cada estágio para o próximo. O resultado é uma imagem final que contém apenas o necessário para executar a aplicação — sem compiladores, sem ferramentas de build, sem dependências de desenvolvimento.
Exemplo para uma aplicação Node.js com TypeScript:
# ── Estágio 1: Build ──────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build # compila TypeScript para JavaScript em /app/dist
# ── Estágio 2: Produção ───────────────────────────
FROM node:20-alpine AS production
WORKDIR /app
# Copia apenas o manifesto e instala somente dependências de produção
COPY package*.json ./
RUN npm ci --only=production
# Copia apenas o código compilado do estágio anterior
COPY --from=builder /app/dist ./dist
# Usuário não-root por segurança
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
O estágio builder tem todas as ferramentas de desenvolvimento. O estágio production começa do zero e importa apenas o resultado compilado. Uma aplicação que geraria uma imagem de 800MB pode resultar em menos de 100MB com essa técnica.
Instruções Complementares
ENV — define variáveis de ambiente que estarão disponíveis durante o build e em tempo de execução:
ENV NODE_ENV=production
ENV PORT=3000
ARG — define variáveis disponíveis apenas durante o build, não em tempo de execução. Útil para passar tokens de autenticação durante a construção sem que fiquem na imagem final:
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc \
&& npm ci \
&& rm .npmrc
HEALTHCHECK — define como o Docker deve verificar se o container está saudável:
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
ENTRYPOINT vs CMD — a distinção é sutil mas importante. O CMD define argumentos padrão que podem ser substituídos ao rodar o container. O ENTRYPOINT define o executável principal, que não é substituído facilmente:
# Com ENTRYPOINT, o container sempre usa 'node' como executável
# O CMD fornece o argumento padrão, substituível no docker run
ENTRYPOINT ["node"]
CMD ["dist/index.js"]
# Roda com o argumento padrão
docker run minha-app
# Substitui o CMD para rodar outro arquivo
docker run minha-app dist/worker.js
Um Dockerfile Completo e Comentado
# Imagem base com Node.js 20 em Alpine Linux
FROM node:20-alpine AS base
# Instala dependências do sistema necessárias
RUN apk add --no-cache tini
# ── Estágio de dependências ───────────────────────
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# ── Estágio de build ──────────────────────────────
FROM deps AS builder
COPY . .
RUN npm run build
# ── Estágio de produção ───────────────────────────
FROM base AS production
WORKDIR /app
# Variáveis de ambiente de produção
ENV NODE_ENV=production
ENV PORT=3000
# Dependências apenas de produção
COPY package*.json ./
RUN npm ci --only=production
# Código compilado
COPY --from=builder /app/dist ./dist
# Usuário não-root — boa prática de segurança
USER node
# Porta exposta
EXPOSE 3000
# Healthcheck
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
# tini como init process — gerencia sinais corretamente
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/index.js"]
O Que Vem a Seguir
No próximo artigo serão abordados volumes e redes no Docker — como persistir dados além do ciclo de vida de um container e como fazer containers diferentes se comunicarem entre si. São os dois temas que completam o entendimento do Docker como plataforma de execução de serviços.
Referências para Aprofundamento
Documentação oficial
- Dockerfile Reference — docs.docker.com — Referência completa de todas as instruções do Dockerfile com exemplos para cada uma.
- Multi-stage Builds — docs.docker.com — Guia oficial sobre multi-stage builds com exemplos em múltiplas linguagens.
Boas práticas
- Best Practices for Writing Dockerfiles — docs.docker.com — Guia oficial de boas práticas, cobrindo cache de camadas, tamanho de imagem e segurança.
- Dockerfile Linting with Hadolint — Linter para Dockerfiles que verifica boas práticas e erros comuns. Pode ser integrado ao pipeline de CI.
Prática
- Docker Build — Getting Started — docs.docker.com — Guia prático do Docker Build com exemplos progressivos de complexidade crescente.