---
id: production-dockerfile
domain: devops
agents: [devops]
when: "ao escrever um Dockerfile destinado a produção"
---

# Dockerfile de produção — imagens enxutas, seguras e cacheáveis

## O problema

Um Dockerfile que "funciona" na sua máquina quase nunca é um Dockerfile de produção.
O tell de um Dockerfile amador é reconhecível:

- **Roda como `root`** porque nunca declarou `USER` — qualquer RCE no app vira root no host.
- **Uma imagem de 1.2 GB** porque empacotou compilador, headers e cache do gerenciador de
  pacotes junto com o binário de runtime.
- **`COPY . .` no topo** — toda mudança em qualquer arquivo (até um `README`) invalida o cache
  de `npm install`/`pip install` e o build inteiro roda de novo.
- **`FROM node:latest`** — build não-reproduzível; a `latest` de amanhã não é a de hoje.
- **`ARG NPM_TOKEN` / `ENV AWS_SECRET=...`** — o segredo fica gravado para sempre numa layer e
  aparece em `docker history`.
- **`RUN apt-get install ...`** em uma layer e `rm -rf /var/lib/apt/lists/*` em outra — a layer
  intermediária ainda carrega o cache; limpar depois não remove nada do tamanho final.

Tudo isso passa no `docker build`. Nenhum disso passa numa revisão séria. O conhecimento abaixo
é a régua.

## O conhecimento

### 1. `USER` não-root antes do `CMD` — sempre

Container que sobe como `root` é o default do Docker, e é o default errado. Crie um usuário
dedicado e troque para ele **antes** do `CMD`/`ENTRYPOINT`.

**Ruim:**
```dockerfile
FROM node:22-slim
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
CMD ["node", "server.js"]   # roda como root (UID 0)
```

**Bom:**
```dockerfile
FROM node:22-slim
WORKDIR /app

# cria grupo/usuário com UID/GID explícito (determinístico entre rebuilds)
RUN groupadd --system --gid 10001 app \
 && useradd  --system --uid 10001 --gid app --no-create-home app

COPY --chown=app:app . .
RUN npm ci --omit=dev

USER 10001:10001            # numérico: o Kubernetes valida runAsNonRoot
CMD ["node", "server.js"]
```

Por que UID/GID **numérico e explícito**: a doc oficial avisa que usuários criados sem UID
recebem um valor não-determinístico ("o próximo livre"), que pode mudar entre rebuilds. E com
`USER nonroot:nonroot` (nome), o `runAsNonRoot: true` do Kubernetes não consegue provar que o
UID != 0 — ele só confia em UID numérico. Use `USER 10001:10001`, não `USER app`.

### 2. Multi-stage build — deixe o compilador para trás

Ferramentas de build (compiladores, devDependencies, headers) não têm o que fazer na imagem de
runtime. Separe em estágios e copie só o artefato.

**Ruim** (toolchain inteiro no final, imagem gigante):
```dockerfile
FROM golang:1.25
WORKDIR /src
COPY . .
RUN go build -o /bin/app ./cmd/app
CMD ["/bin/app"]            # carrega o Go SDK inteiro em produção
```

**Bom** (Go compila em `build`, runtime recebe só o binário):
```dockerfile
# --- estágio de build ---
FROM golang:1.25 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/app ./cmd/app

# --- estágio de runtime ---
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /bin/app /app
USER 65532:65532
ENTRYPOINT ["/app"]
```

`COPY --from=build` traz só `/bin/app`. O Go SDK e os intermediários ficam no estágio `build` e
nunca entram na imagem final.

### 3. Base slim/distroless + pin por digest (sha256)

| Base | Tamanho aprox. | Tem shell/gerenciador de pacotes? | Quando usar |
|---|---|---|---|
| `node:22` / `python:3.13` | 1+ GB | sim, toolchain completa | só no estágio de build |
| `*-slim` (ex.: `node:22-slim`) | ~200 MB | sim, mínima | runtime de apps interpretados |
| `alpine:3.21` | < 6 MB + libs | sim (busybox, apk) | quando precisa de shell pequeno |
| `gcr.io/distroless/*:nonroot` | ~2–20 MB | **não** (sem shell, sem apt) | runtime de produção endurecido |

Distroless não tem shell — reduz superfície de ataque (sem `sh` para um atacante explorar) e já
vem com a variante `:nonroot` (UID 65532).

**Ruim** (tag móvel, build não-reproduzível):
```dockerfile
FROM node:latest
```

**Bom** (tag + digest — imutável mesmo que o publisher reescreva a tag):
```dockerfile
FROM node:22.11-slim@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
```

O digest `@sha256:...` garante o mesmo bit-a-bit em qualquer máquina e em qualquer momento. Tag
sozinha (`node:22-slim`) ainda é alvo móvel: o registry pode reescrever para onde ela aponta.

### 4. Ordem das camadas para cache — deps antes do código

O Docker cacheia cada layer; quando uma instrução muda, **todas as seguintes** são reconstruídas.
Coloque o que muda raramente em cima (manifesto de dependências) e o que muda a cada commit
embaixo (código-fonte).

**Ruim** (qualquer mudança no código reinstala todas as deps):
```dockerfile
COPY . .
RUN npm ci --omit=dev
```

**Bom** (só `package*.json` invalida o `npm ci`; editar `src/` reaproveita o cache de install):
```dockerfile
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
```

Mesmo padrão em outras stacks:
```dockerfile
# Python
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
```

### 5. `.dockerignore` — não mande lixo (nem segredo) pro contexto

Sem `.dockerignore`, `COPY . .` empacota `.git/`, `node_modules/`, `.env` — infla o contexto e
pode vazar segredo para dentro da imagem.

**`.dockerignore` mínimo:**
```dockerignore
.git
node_modules
.env
.env.*
*.log
Dockerfile
.dockerignore
**/__pycache__
dist
coverage
```

### 6. Limpeza na MESMA camada — sem cache vazando em layer

Layers são imutáveis e empilhadas: o que entra numa layer permanece no tamanho final mesmo que
uma layer **posterior** apague. Instale e limpe no **mesmo** `RUN`.

**Ruim** (o cache do apt já foi gravado na layer anterior; o `rm` posterior não reduz nada):
```dockerfile
RUN apt-get update && apt-get install -y curl ca-certificates
RUN rm -rf /var/lib/apt/lists/*
```

**Bom** (instala e limpa num único `RUN` → a layer já nasce enxuta):
```dockerfile
RUN apt-get update \
 && apt-get install -y --no-install-recommends curl ca-certificates \
 && rm -rf /var/lib/apt/lists/*
```

`--no-install-recommends` evita arrastar pacotes sugeridos que você não pediu. Em Alpine, o
equivalente é `apk add --no-cache`.

### 7. Nunca segredos em `ENV`/`ARG` — use `RUN --mount=type=secret`

`ENV` persiste na imagem e é lido por qualquer `docker inspect`. `ARG` fica visível em
`docker history`. Nenhum dos dois serve para token/credencial — a própria doc avisa: *"não é
recomendado usar build arguments para passar segredos; eles são visíveis em `docker history`"*.

**Ruim** (token gravado para sempre, vaza em `docker history`):
```dockerfile
ARG NPM_TOKEN
RUN npm config set //registry.npmjs.org/:_authToken=${NPM_TOKEN} && npm ci
```

**Bom** (BuildKit monta o segredo só durante o `RUN`; nunca entra em layer nem no cache):
```dockerfile
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci --omit=dev
```
Build:
```bash
DOCKER_BUILDKIT=1 docker build \
  --secret id=npmrc,src=$HOME/.npmrc -t app .
```

O arquivo montado existe só no sistema de arquivos daquele `RUN` e desaparece depois — não fica
na imagem nem no build cache.

### 8. `HEALTHCHECK` — o orquestrador precisa saber se o app está vivo

Sem `HEALTHCHECK`, o Docker só sabe se o **processo** existe, não se o app **responde**. Um app
travado mas com PID vivo é reportado como saudável.

**Bom:**
```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD curl -fsS http://localhost:3000/health || exit 1
```

Defaults do Docker quando você só passa `CMD`: `--interval=30s`, `--timeout=30s`, `--retries=3`,
`--start-period=0s`. Ajuste `--start-period` para a janela de boot do app (evita marcar
`unhealthy` durante a partida). Se a base for distroless (sem `curl`), use o healthcheck do
orquestrador ou compile um probe binário no estágio de build.

### 9. `COPY` específico em vez de `COPY . .`

`COPY . .` arrasta tudo e invalida cache à toa. Copie só o que cada estágio precisa.

**Ruim:**
```dockerfile
COPY . .
```

**Bom:**
```dockerfile
COPY package.json package-lock.json ./
COPY src ./src
COPY public ./public
```

Bônus: combine com `--chown` para já entregar a posse ao usuário não-root sem um `RUN chown`
extra (`COPY --chown=10001:10001 src ./src`).

## Checklist

Antes de marcar um Dockerfile como pronto para produção — qualquer "não" é bloqueio:

- [ ] Existe `USER <uid numérico>` (≠ 0) **antes** do `CMD`/`ENTRYPOINT`?
- [ ] O build é multi-stage e o estágio final **não** contém compilador/devDependencies?
- [ ] A base de runtime é `slim`/`distroless`/`alpine` (não a tag "gorda")?
- [ ] Todo `FROM` está pinado por **tag + `@sha256:` digest**?
- [ ] O manifesto de deps é copiado e instalado **antes** de copiar o código-fonte?
- [ ] Existe `.dockerignore` cobrindo `.git`, `node_modules`, `.env*`, build artifacts?
- [ ] Instalação de pacotes e limpeza (`rm -rf /var/lib/apt/lists/*`) estão no **mesmo** `RUN`?
- [ ] `--no-install-recommends` (apt) ou `--no-cache` (apk) presente?
- [ ] Zero segredos em `ENV`/`ARG`; tokens entram via `RUN --mount=type=secret`?
- [ ] Existe `HEALTHCHECK` apontando para um endpoint real de saúde?
- [ ] Os `COPY` são específicos (sem `COPY . .` no topo cacheável)?
- [ ] `COPY --chown` entrega a posse ao usuário não-root (sem `RUN chown` extra)?

## Tabela de decisão

| Situação | Faça |
|---|---|
| App compilado (Go, Rust, binário estático) | Multi-stage → final em `gcr.io/distroless/static-debian12:nonroot` |
| App interpretado (Node, Python) em produção | Multi-stage → final em `*-slim` ou `distroless/nodejs`/`distroless/python3`, `USER` numérico |
| Precisa de shell para debug em runtime | `*-slim` ou `alpine`; nunca a base "gorda" |
| Precisa instalar pacotes do sistema | Um único `RUN apt-get update && install --no-install-recommends ... && rm -rf /var/lib/apt/lists/*` |
| Precisa de token/credencial no build (npm, pip privado, git) | `RUN --mount=type=secret` + `docker build --secret` — nunca `ARG`/`ENV` |
| Build lento, reconstrói deps a cada commit | Copie manifesto (`package.json`/`requirements.txt`) e instale **antes** de `COPY . .` |
| Quer reprodutibilidade total entre máquinas/CI | Pin de `FROM` por `@sha256:` digest |
| Distroless sem `curl` mas precisa de HEALTHCHECK | Probe via orquestrador, ou compile um healthcheck binário no estágio de build |
| Config dinâmica em runtime (não-secreta) | `ENV` é ok; para secreta, injete em runtime (orquestrador/secrets manager), não no Dockerfile |
