Dockerfile: escrevendo do zero

1. O que é um Dockerfile e sua estrutura básica

Um Dockerfile é um documento de texto que contém todas as instruções necessárias para construir uma imagem Docker automaticamente. No ciclo DevOps, ele funciona como a "receita" da aplicação, garantindo que o ambiente de execução seja reproduzível, consistente e versionável.

A estrutura básica de um Dockerfile utiliza diretivas fundamentais:

  • FROM: define a imagem base
  • RUN: executa comandos durante o build
  • CMD: especifica o comando padrão quando o container inicia
  • ENTRYPOINT: define o executável principal do container

Exemplo de um Dockerfile mínimo para uma aplicação Node.js:

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "index.js"]

2. Escolhendo a imagem base correta

Imagens oficiais vs. imagens de terceiros

Imagens oficiais são mantidas pela Docker Inc. ou pelos próprios desenvolvedores da tecnologia (Node.js, Python, Nginx). Elas passam por revisões de segurança e são atualizadas regularmente. Imagens de terceiros podem conter vulnerabilidades ou malware.

Alpine, Slim e Full: trade-offs

  • Alpine (~5MB): baseada em musl libc e BusyBox. Extremamente leve, mas pode causar incompatibilidades com algumas bibliotecas nativas (ex: bcrypt em Node.js).
  • Slim (~50-150MB): versão reduzida da imagem completa, mantendo compatibilidade.
  • Full (~300MB+): contém todas as ferramentas do sistema, útil para debugging.

Boas práticas

Use sempre tags específicas em vez de latest:

# Evite
FROM node:latest

# Prefira
FROM node:18.17.1-alpine

Tags específicas garantem builds reproduzíveis e evitam surpresas com atualizações inesperadas.

3. Instalação de dependências e execução de comandos

A diretiva RUN cria uma nova camada (layer) na imagem. Muitas camadas aumentam o tamanho final. Combine comandos com && e limpe caches para reduzir o número de layers.

Exemplo prático: instalando pacotes Python

FROM python:3.11-slim

# Instala dependências do sistema e limpa cache em um único RUN
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    libpq-dev \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

Exemplo prático: instalando pacotes Node.js

FROM node:18-alpine

RUN apk add --no-cache \
    python3 \
    make \
    g++ \
    && npm cache clean --force

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

4. Copiando arquivos e configurando o ambiente de execução

COPY vs. ADD

  • COPY: copia arquivos do host para a imagem. Simples e previsível.
  • ADD: similar ao COPY, mas suporta URLs e extração automática de arquivos tar. Use apenas quando necessário, pois seu comportamento pode ser surpreendente.
# Prefira COPY para arquivos locais
COPY ./app /app

# Use ADD apenas para extrair tarballs automaticamente
ADD https://example.com/file.tar.gz /tmp/

Uso de .dockerignore

Crie um arquivo .dockerignore para evitar copiar arquivos desnecessários:

node_modules
.git
.env
*.log
__pycache__

Configurando ambiente

ENV NODE_ENV=production
ENV PORT=3000
WORKDIR /app

5. Definindo o comportamento do container em execução

CMD vs. ENTRYPOINT

  • ENTRYPOINT: define o executável principal (não pode ser sobrescrito facilmente)
  • CMD: fornece argumentos padrão para o ENTRYPOINT ou define o comando completo

Combinação comum

ENTRYPOINT ["python"]
CMD ["app.py"]

Ao executar docker run minha-imagem, o container roda python app.py. Se o usuário passar argumentos, eles substituem o CMD: docker run minha-imagem server.py executa python server.py.

Exemplo: servidor web com Nginx

FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY ./static /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Exemplo: servidor Flask

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

6. Otimizações para build eficiente e segurança

Ordenação estratégica de instruções

O Docker cacheia cada layer. Instruções que mudam com menos frequência devem vir primeiro:

# 1. Imagem base (quase nunca muda)
FROM node:18-alpine

# 2. Dependências do sistema (mudam raramente)
RUN apk add --no-cache curl

# 3. Arquivos de dependências (mudam com commits)
COPY package*.json ./
RUN npm ci

# 4. Código fonte (muda frequentemente)
COPY . .

Executando como usuário não-root

FROM node:18-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .

Removendo credenciais durante o build

Nunca copie arquivos .env ou credenciais para a imagem. Use segredos do Docker BuildKit:

# syntax=docker/dockerfile:1
FROM node:18-alpine
RUN --mount=type=secret,id=mysecret \
    cat /run/secrets/mysecret

7. Integrando o Dockerfile no pipeline DevOps e Kubernetes

Multi-stage builds

Reduzem drasticamente o tamanho da imagem final. Exemplo para aplicação Go:

# Stage 1: Build
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .

# Stage 2: Runtime
FROM alpine:3.18
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]

Exemplo para React

# Stage 1: Build
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Serve com Nginx
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Dicas para Kubernetes

Configure liveness e readiness probes no deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: minha-app
spec:
  template:
    spec:
      containers:
      - name: app
        image: minha-app:latest
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 3
          periodSeconds: 5

Referências