Docker Completo: do zero ao production-ready
Docker resolveu um problema velho e chato: “na minha máquina funciona”. Antes dele, subir uma aplicação em outro servidor era um ritual — instalar a versão certa da linguagem, as libs do sistema, configurar permissões, rezar. Container empacota o código junto com todo o ambiente que ele precisa, gera um artefato imutável (a imagem) e executa esse artefato em qualquer lugar que tenha um runtime Docker. Este guia vai do porquê dos containers existirem até Dockerfiles profissionais, multi-stage, redes, volumes, segurança e otimização. É denso de propósito — leia sem pressa, cole os comandos no terminal, teste.
De onde veio tudo isso: uma linha do tempo de 25 anos
Docker não inventou containers. Inventou a experiência de uso. A ideia de isolar processos é velha:
A revolução do Docker não foi técnica — foi de interface. Antes dele, usar LXC exigia conhecimento profundo de namespaces e cgroups. Docker entregou três comandos (build, run, push), um formato de imagem versionável e um registry público (Docker Hub). Desenvolvedores adotaram em massa.
Container vs VM — a diferença que todo dev precisa entender
| Critério | Container | VM |
|---|---|---|
| Kernel | Compartilhado com o host | Próprio (duplica kernel) |
| Boot | 50-500 ms | 30 s - vários minutos |
| Tamanho | ~10 MB - 500 MB | ~500 MB - 20 GB |
| Overhead de CPU/RAM | Quase zero | Significativo (5-15%) |
| Densidade | Centenas por host | Dezenas por host |
| Isolamento | Namespaces + cgroups (software) | Hardware (ring -1) |
| Multi-OS | Só mesmo kernel (Linux/Win separados) | Qualquer OS em cima do host |
| Uso típico | Microserviços, CI, dev envs | Tenants hostis, compliance, legacy |
A arquitetura real do Docker
Quando você digita docker run, não é um binário mágico. Tem uma cadeia de processos colaborando:
docker buildcontinuam funcionando — elas são OCI, não “Docker”.Os 4 objetos que você precisa dominar
| Objeto | O que é | Ciclo de vida |
|---|---|---|
| Image | Template imutável de um filesystem + metadata (cmd, env, ports) | Build → push → pull → run |
| Container | Instância em execução (ou parada) de uma imagem + camada R/W | create → start → stop → rm |
| Volume | Armazenamento persistente gerenciado pelo Docker (fora da imagem) | create → mount em N containers → rm |
| Network | Rede virtual que conecta containers (com DNS embutido) | create → attach → disconnect → rm |
Uma forma útil de pensar: imagem é uma classe, container é um objeto instanciado. Volume é estado persistente injetado; network é o “barramento” que permite containers conversarem por nome.
Imagens: o truque das camadas (UnionFS)
Uma imagem Docker não é um blob único. É uma pilha de camadas read-only, cada uma com um diff do filesystem, montadas por cima via UnionFS (overlay2 é o driver padrão hoje). Quando o container inicia, Docker coloca uma camada read-writeno topo (“container layer”). Mudanças no runtime ficam lá.
CMD ["nginx","-g","daemon off;"]COPY nginx.conf /etc/nginx/RUN apt-get install nginxFROM debian:12-slimdocker pullsó baixa camadas que você ainda não tem localmente. Isso é o que faz “rebuild + push” ser rápido quando você mudou só a última camada.Dockerfile — o DNA da imagem
Dockerfile é um script declarativo que descreve como construir a imagem. Cada instrução de topo de linha gera uma camada (exceto FROM, LABEL, ARG, etc., que afetam metadata). As instruções mais usadas:
# ❌ RUIM — 3 camadas, 3 passos de cache invalidados por coisas que não deveriam FROM ubuntu:24.04 RUN apt-get update RUN apt-get install -y curl RUN apt-get install -y git # ✅ BOM — 1 camada, limpeza de cache do apt no final, menor FROM ubuntu:24.04 RUN apt-get update \ && apt-get install -y --no-install-recommends curl git \ && rm -rf /var/lib/apt/lists/*
Layer caching — a diferença entre build de 3s e 3min
Docker tenta reusar cache de cada instrução. Regra de ouro: coisas que mudam pouco primeiro; coisas que mudam sempre por último. Um Dockerfile comum de Node mal escrito reinstala node_modules a cada commit porque o cache do RUN npm ci é invalidado antes.
# ❌ Reinstala dependências a cada mudança de código FROM node:20-alpine WORKDIR /app COPY . . RUN npm ci CMD ["node", "server.js"] # ✅ Cache-friendly — deps só reinstalam quando package.json muda FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY . . CMD ["node", "server.js"]
docker build --progress=plain . — o BuildKit (default hoje) mostra CACHED vs executado para cada step.Multi-stage builds — imagens pequenas de verdade
A ideia: um stage para compilar, outro só para rodar. O stage final recebe apenas os artefatos, sem toolchain, cache do gerenciador de pacotes ou fontes. Exemplos canônicos:
# ─── Node.js / TypeScript ────────────────────────────────────── FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production COPY package*.json ./ RUN npm ci --omit=dev COPY --from=builder /app/dist ./dist USER node EXPOSE 3000 CMD ["node", "dist/server.js"]
# ─── Go (imagem final "scratch" = ~6 MB) ──────────────────────── FROM golang:1.23-alpine AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /bin/app ./cmd/api FROM scratch COPY --from=builder /bin/app /app COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ EXPOSE 8080 ENTRYPOINT ["/app"]
# ─── Python (com distroless — sem shell, sem apt, ~40 MB) ─────── FROM python:3.12-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --user --no-cache-dir -r requirements.txt FROM gcr.io/distroless/python3-debian12 WORKDIR /app COPY --from=builder /root/.local /root/.local COPY . . ENV PATH=/root/.local/bin:$PATH CMD ["server.py"]
Escolhendo a imagem base
| Base | Tamanho | Tem shell? | Quando usar |
|---|---|---|---|
| ubuntu / debian | ~80 MB | Sim (bash, apt) | Dev/debug, apps que exigem libs glibc completas |
| *-slim | ~30-50 MB | Sim, enxuto | Default seguro pra prod quando precisa de glibc |
| alpine | ~5-10 MB | Sim (ash, apk) | Prod, mas cuidado: usa musl libc (quebra libs com binários glibc) |
| distroless | ~20-50 MB | Não | Prod — maior segurança, sem shell pra atacante explorar |
| scratch | 0 MB | Não | Binários estáticos (Go, Rust) — o menor possível |
psycopg2, pillow, numpy) e algumas libs Node (sharp, bcrypt) podem precisar recompilar ou até quebrar. Em produção Node/Python, -slim costuma ser mais previsível que -alpine.CMD vs ENTRYPOINT — o ponto que 90% erra
| Instrução | Propósito | Sobrescrito por docker run? |
|---|---|---|
| CMD | Comando default | Sim — docker run imagem <novo-cmd> substitui |
| ENTRYPOINT | Binário fixo da imagem | Não (exceto com --entrypoint) |
| ENTRYPOINT + CMD | Binário + args default | Args sobrescritos, binário mantido |
# Padrão profissional: ENTRYPOINT é o binário, CMD são flags default ENTRYPOINT ["./myapp"] CMD ["--port=8080", "--log-level=info"] # docker run img → roda: ./myapp --port=8080 --log-level=info # docker run img --log=debug → roda: ./myapp --log=debug # docker run --entrypoint=sh img → abre shell (útil pra debug)
CMD node server.js (sem colchetes) roda /bin/sh -c "node server.js". O sh vira PID 1 e não repassa SIGTERM ao node — seu container demora 10s pra morrer no docker stop. Sempre use a forma exec (JSON array): CMD ["node", "server.js"].Docker Compose — multi-container sem sofrimento
Quando a aplicação tem mais de um container (app + db + cache), escrever docker run à mão vira tortura. Compose descreve tudo num YAML declarativo e sobe com docker compose up.
# compose.yaml — app Node + Postgres + Redis com healthchecks e volumes
services:
web:
build: .
ports: ["3000:3000"]
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
retries: 5
cache:
image: redis:7-alpine
volumes:
- redisdata:/data
volumes:
pgdata:
redisdata:db:5432 — não precisa de IP. O Docker mantém um DNS embutido na rede bridge que resolve nomes de serviço.Redes — como containers se falam (e se isolam)
| Driver | Uso | Quando escolher |
|---|---|---|
| bridge | Default. Rede virtual isolada no host. | Dev local, Compose, single-host |
| host | Container usa a stack de rede do host. | Quando precisa de perf máxima ou monitorar rede do host |
| none | Sem rede. | Batch jobs isolados, compliance |
| overlay | Rede distribuída entre múltiplos hosts. | Swarm ou multi-host (hoje quase sempre K8s resolve isso) |
| macvlan | Container com IP/MAC próprios na rede física. | Integrar container como se fosse um host físico |
# Criar rede bridge customizada (com DNS entre containers por nome) docker network create backend # Subir containers nela docker run -d --name db --network backend postgres:16-alpine docker run -d --name api --network backend -p 3000:3000 my-api:1.0 # Dentro do container api, conectar em "db:5432" funciona por DNS. # O bridge default (sem --network) NÃO tem DNS por nome — sempre crie uma rede.
-p bypassa o ufw/iptables do host porque Docker mexe direto no netfilter.Volumes — onde o estado persiste
| Tipo | Origem | Quando usar |
|---|---|---|
| Named volume | Gerenciado pelo Docker em /var/lib/docker/volumes/ | Prod — dados do db, cache, uploads |
| Bind mount | Caminho explícito do host | Dev — montar código-fonte pra hot reload |
| tmpfs | Memória (não persiste) | Segredos temporários, caches de sessão |
# Named volume (recomendado em prod) docker run -d --name db -v pgdata:/var/lib/postgresql/data postgres:16-alpine # Bind mount (dev — código refletindo no container) docker run -d --name dev -v "$(pwd):/app" -w /app node:20 npm run dev # tmpfs (dados sensíveis que não devem tocar o disco) docker run -d --tmpfs /run/secrets:size=10M my-app:1.0 # Backup de um named volume docker run --rm -v pgdata:/data -v "$(pwd):/backup" alpine \ tar czf /backup/pgdata-$(date +%F).tgz -C /data .
docker rm. Sobrescrever volume com bind mount por acidente em produção é um dos desastres mais comuns — cuidado com caminhos relativos em compose.yaml.Registries — onde as imagens vivem
| Registry | Hospedeiro | Quando usar |
|---|---|---|
| Docker Hub | docker.io | Images públicas, OSS, prototipagem |
| GHCR | ghcr.io (GitHub) | Projetos hospedados no GitHub, integra com Actions |
| ECR | AWS | Prod na AWS — IAM, scanning, regional |
| GCR / Artifact Registry | Google Cloud | Prod no GCP |
| ACR | Azure | Prod no Azure |
| Harbor / self-hosted | Seu cluster | Air-gapped, compliance, custo |
# Tag + push pro GHCR docker build -t ghcr.io/me/app:1.2.0 -t ghcr.io/me/app:latest . echo "$GH_TOKEN" | docker login ghcr.io -u me --password-stdin docker push ghcr.io/me/app:1.2.0 docker push ghcr.io/me/app:latest
:latest — é uma referência móvel. Prod deve apontar para tags versionadas (:1.2.0) ou digests (@sha256:abc...), que são criptograficamente imutáveis.Segurança — checklist mínimo de produção
# Padrão production-hardened (Node)
FROM node:20-alpine AS runner
WORKDIR /app
# Usuário não-root
RUN addgroup -S app && adduser -S app -G app
COPY --chown=app:app . .
RUN npm ci --omit=dev
USER app
EXPOSE 3000
# Healthcheck pra orquestrador reiniciar container travado
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
ENTRYPOINT ["node"]
CMD ["server.js"]CLI essencial — os comandos que você usa toda semana
# Imagens docker build -t app:1.0 . # build do diretório atual docker images # lista imagens locais docker tag app:1.0 ghcr.io/me/app:1.0 docker push ghcr.io/me/app:1.0 docker pull nginx:1.27-alpine docker rmi app:0.9 # remove imagem # Containers docker run -d --name web -p 80:80 nginx:1.27-alpine docker ps # containers em execução docker ps -a # inclui parados docker logs -f --tail 100 web # logs ao vivo docker exec -it web sh # shell dentro docker stop web && docker rm web # para e remove docker restart web # Inspeção / debug docker inspect web # JSON com tudo (rede, mounts, env) docker stats # top de CPU/mem por container docker top web # processos dentro do container docker diff web # o que mudou na camada R/W docker port web # mapa de portas # Limpeza (cuidado em prod — apaga de verdade) docker system df # quanto disco está sendo usado docker system prune -a --volumes # apaga imagens, containers, volumes não usados docker builder prune # só o cache do BuildKit
BuildKit — o build moderno
Desde o Docker 23, BuildKit é o builder default. Habilita recursos que o build antigo não tinha:
# Cache mount: npm ci fica MUITO mais rápido em rebuilds # syntax=docker/dockerfile:1.7 FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev COPY . . CMD ["node", "server.js"]
Armadilhas comuns (que você vai encontrar mais cedo ou mais tarde)
CMD bash script.sh (shell form), o bash vira PID 1 e não propaga SIGTERM. Container demora 10s pra morrer. Use forma exec ou adicione um init como tini (flag --init do docker run).wait() em filhos órfãos. Rode com --init ou use dumb-init como ENTRYPOINT.-v $(pwd):/app sobrescreve /app/node_modules com o do Mac (que pode ter binários macOS). Solução: volume anônimo em cima — -v /app/node_modules.node_modules, .git, .env e dumps de db pro contexto de build. Lentidão + potencial vazamento de segredo.http://localhost:5432 está chamando ele mesmo. Pra acessar o host, use host.docker.internal (Docker Desktop) ou --add-host.linux/amd64 roda em Mac M via emulação Rosetta — lenta. Use buildx com --platform e publique multi-arch.Cenários de decisão
📋 API Node 20 para produção: qual base usar?
Slim tem glibc (compatível com toda lib nativa do npm), é ~60 MB, e num multi-stage o stage final só recebe dist + node_modules --omit=dev. Prod roda em ~180 MB, previsível.
Alt: node:20-alpine — menor (~30 MB), mas musl libc quebra bcrypt/sharp/node-gyp em casos comuns — investir em troubleshooting.
Alt: distroless — mais seguro (sem shell), mas dificulta debug. Use quando maturidade de ops for alta.
📋 Preciso de hot reload no dev, mas o código está no Mac e o container em Linux
-v $(pwd):/app monta o código (hot reload funciona). -v /app/node_modules cria volume anônimo em cima, preservando os módulos instalados no build (evita conflito de binários Mac vs Linux).
Alt: Sem bind, rebuild a cada mudança — lento demais pra DX — inviável.
📋 Meu CI demora 8 min no build de uma API Python
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt reusa cache entre builds. Com COPY requirements.txt antes do COPY . ., deps só reinstalam quando requirements muda.
Alt: Só multi-stage — reduz imagem final mas não reduz o tempo de build — ataca problema diferente.
Perguntas típicas
❓ Docker vai morrer porque Kubernetes “removeu” o Docker?
dockershim (a camada que falava com o dockerd); agora usa containerd direto. Mas imagens Docker continuam rodando — elas são OCI. Pra desenvolvedor, docker build e docker push seguem sendo o fluxo padrão.❓ Qual a diferença entre COPY e ADD?
❓ Por que minha imagem está 1.5 GB se meu binário tem 20 MB?
❓ É seguro rodar docker em prod?
--privilegedem produção porque “funcionou no dev”.❓ Preciso aprender Docker antes de Kubernetes?
Quiz rápido
4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito