🧠FFVAcademy
🐳

Docker Completo: do zero ao production-ready

22 min de leitura·+100 XP

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:

1979
chroot (Unix V7)
Isola filesystem de um processo.
2000
FreeBSD Jails
Isola rede, filesystem e usuários.
2005
Solaris Zones
VMs leves no Solaris.
2006
cgroups (Google)
Limita CPU e memória por grupo de processos.
2008
LXC
Namespaces + cgroups = "container Linux" completo.
2013
Docker 0.1
CLI amigável em cima do LXC — mudou o jogo.
2015
OCI / runc
Padrão aberto para imagens e runtime.
2016
containerd
Runtime de alto nível extraído do Docker.
2018
Kubernetes domina
Docker deixa de ser obrigatório em K8s.

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

🗺️ Virtual Machine
apps
App A · App B · App C
libs
Libs A · Libs B · Libs C
guest OS
Kernel completo × N (Linux/Win)
overhead real por VM
hypervisor
ESXi · KVM · Hyper-V · Xen
host OS
Sistema operacional do bare-metal
hardware
CPU · RAM · Disco · Rede
🗺️ Container
apps
App A · App B · App C · ...
libs
Libs por container (camadas)
engine
Docker Engine · containerd · runc
host OS
Kernel Linux único
namespaces + cgroups isolam tudo
hardware
CPU · RAM · Disco · Rede
CritérioContainerVM
KernelCompartilhado com o hostPróprio (duplica kernel)
Boot50-500 ms30 s - vários minutos
Tamanho~10 MB - 500 MB~500 MB - 20 GB
Overhead de CPU/RAMQuase zeroSignificativo (5-15%)
DensidadeCentenas por hostDezenas por host
IsolamentoNamespaces + cgroups (software)Hardware (ring -1)
Multi-OSSó mesmo kernel (Linux/Win separados)Qualquer OS em cima do host
Uso típicoMicroserviços, CI, dev envsTenants hostis, compliance, legacy
💡
Resumo: container é isolamento feito em software pelo kernel do Linux (namespaces isolam o que o processo vê; cgroups limitam o que ele consome). VM é isolamento feito em hardware pelo hypervisor. Por isso container é leve e VM é forte — use VM quando precisar rodar código de terceiros que você não confia; use container pra tudo mais.

A arquitetura real do Docker

Quando você digita docker run, não é um binário mágico. Tem uma cadeia de processos colaborando:

🗺️ O stack completo
🖥️docker CLIcliente
Só um cliente HTTP. Roda como seu usuário. Nada de mágico.
REST · /var/run/docker.sock
⚙️dockerddaemon
Roda como root. Gerencia images, networks e volumes. Fala com containerd.
gRPC
📦containerdruntime CNCF
Runtime de alto nível. Pull/push de imagens, snapshotting, lifecycle.
exec
🧬runcruntime OCI
Runtime de baixo nível. clone() + namespaces + cgroups + pivot_root.
syscalls
🐧Kernel Linuxhost
Namespaces, cgroups, seccomp, LSM — o isolamento real acontece aqui.
docker CLISó um cliente HTTP. Roda como seu usuário. Não tem nada de mágico.
dockerdDaemon. Recebe comandos do CLI. Historicamente monolítico, foi sendo dividido.
containerdRuntime de alto nível. Cuida do ciclo de vida dos containers. É usado também por Kubernetes sem Docker.
runcRuntime de baixo nível. Implementa o padrão OCI. Faz as chamadas de sistema que materializam o container.
OCIOpen Container Initiative: padrão aberto de formato de imagem e runtime. Docker doou o runc pra OCI em 2015.
⚠️
Por que isso importa:quando Kubernetes “removeu o Docker” em 2022, o que ele removeu foi o dockerd. Containers continuam rodando via containerd + runc. Imagens feitas com docker buildcontinuam funcionando — elas são OCI, não “Docker”.

Os 4 objetos que você precisa dominar

ObjetoO que éCiclo de vida
ImageTemplate imutável de um filesystem + metadata (cmd, env, ports)Build → push → pull → run
ContainerInstância em execução (ou parada) de uma imagem + camada R/Wcreate → start → stop → rm
VolumeArmazenamento persistente gerenciado pelo Docker (fora da imagem)create → mount em N containers → rm
NetworkRede 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á.

🗺️ Um container nginx na prática
camada R/Wdescartada ao dar rm
/var/log/nginx/*.log
UnionFS · overlay2
camada 4
CMD ["nginx","-g","daemon off;"]
camada 3
COPY nginx.conf /etc/nginx/
camada 2
RUN apt-get install nginx
camada 1
FROM debian:12-slim
base OSread-only · compartilhada
Debian 12 · ~80 MB
Consequência prática: se 10 containers usam a mesma imagem base, o Docker só armazena a base uma vez no disco. A mesma lógica vale para o registry — um docker 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:

FROMImagem base. Sempre a primeira linha (ou depois de ARG). Ex.: FROM node:20-alpine
WORKDIRMuda o diretório (equivale a cd + mkdir -p). Use SEMPRE ao invés de RUN cd ...
COPY / ADDCopia arquivos do contexto de build. Prefira COPY (ADD tem mágica de URL e tar que raramente você quer).
RUNExecuta comando no build. Cada RUN gera camada — agrupe com && para reduzir tamanho.
ENVVariável de ambiente persistida na imagem final.
ARGVariável só no build (não persiste no runtime).
EXPOSEDocumenta a porta que o processo usa. NÃO abre porta — só documenta (docker run -p faz o port mapping).
USERDefine o usuário do processo. Sempre use um não-root em produção.
CMDComando default. Pode ser sobrescrito por docker run imagem <novo-cmd>.
ENTRYPOINTBinário fixo. docker run imagem arg1 passa arg1 como argumento, não substitui o binário.
HEALTHCHECKComando que o Docker roda periodicamente para saber se o container está saudável.
dockerfile
# ❌ 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.

dockerfile
# ❌ 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"]
💡
Como ver o que invalidou o cache: rode 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:

dockerfile
# ─── 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"]
dockerfile
# ─── 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"]
dockerfile
# ─── 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"]
Ganhos reais: uma API Node típica sai de ~1.2 GB (node + devDeps + source) para 150-250 MB (node:alpine) ou ~100 MB (distroless). Uma API Go sai de 900 MB para 6-12 MB (scratch). Menor imagem = pull mais rápido no deploy, superfície de ataque menor, menos CVEs pra triar.

Escolhendo a imagem base

BaseTamanhoTem shell?Quando usar
ubuntu / debian~80 MBSim (bash, apt)Dev/debug, apps que exigem libs glibc completas
*-slim~30-50 MBSim, enxutoDefault seguro pra prod quando precisa de glibc
alpine~5-10 MBSim (ash, apk)Prod, mas cuidado: usa musl libc (quebra libs com binários glibc)
distroless~20-50 MBNãoProd — maior segurança, sem shell pra atacante explorar
scratch0 MBNãoBinários estáticos (Go, Rust) — o menor possível
⚠️
Alpine + Python/Node nativo = dor. Alpine usa musl libc, não glibc. Libs Python com bindings C (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çãoPropósitoSobrescrito por docker run?
CMDComando defaultSim — docker run imagem <novo-cmd> substitui
ENTRYPOINTBinário fixo da imagemNão (exceto com --entrypoint)
ENTRYPOINT + CMDBinário + args defaultArgs sobrescritos, binário mantido
dockerfile
# 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)
🚨
Gotcha do shell form: 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.

yaml
# 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:
docker compose up -dSobe tudo em background. Cria rede compose_default automaticamente.
docker compose logs -f webSegue logs de um serviço. Use -t pra timestamps.
docker compose exec web shShell dentro de um container em execução.
docker compose down -vPara tudo e apaga volumes. Cuidado — perde dados do db.
depends_on + conditionEspera db ficar healthy antes de subir web. Evita a corrida clássica "db not ready yet".
💡
DNS interno: no Compose, cada serviço é um hostname. A app Node conecta em 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)

DriverUsoQuando escolher
bridgeDefault. Rede virtual isolada no host.Dev local, Compose, single-host
hostContainer usa a stack de rede do host.Quando precisa de perf máxima ou monitorar rede do host
noneSem rede.Batch jobs isolados, compliance
overlayRede distribuída entre múltiplos hosts.Swarm ou multi-host (hoje quase sempre K8s resolve isso)
macvlanContainer com IP/MAC próprios na rede física.Integrar container como se fosse um host físico
bash
# 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 host:container não “abre porta na rede” — ele cria um NAT no kernel do host. O container continua só na rede bridge interna. Isso importa quando você tem firewall: em algumas configs, -p bypassa o ufw/iptables do host porque Docker mexe direto no netfilter.

Volumes — onde o estado persiste

TipoOrigemQuando usar
Named volumeGerenciado pelo Docker em /var/lib/docker/volumes/Prod — dados do db, cache, uploads
Bind mountCaminho explícito do hostDev — montar código-fonte pra hot reload
tmpfsMemória (não persiste)Segredos temporários, caches de sessão
bash
# 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 .
🚨
Regra de ouro: qualquer coisa que você não pode perder fica em volume. A camada R/W do container é descartada no 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

RegistryHospedeiroQuando usar
Docker Hubdocker.ioImages públicas, OSS, prototipagem
GHCRghcr.io (GitHub)Projetos hospedados no GitHub, integra com Actions
ECRAWSProd na AWS — IAM, scanning, regional
GCR / Artifact RegistryGoogle CloudProd no GCP
ACRAzureProd no Azure
Harbor / self-hostedSeu clusterAir-gapped, compliance, custo
bash
# 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
💡
Tag imutável: evite fazer deploy baseado em :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

Não rode como rootCrie um user no Dockerfile (USER appuser) ou --user 1000:1000 no run. Container root = root no kernel do host em muitas situações.
Filesystem read-onlydocker run --read-only + --tmpfs /tmp. App não consegue escrever em /etc, /usr, /bin.
Drop capabilities--cap-drop=ALL --cap-add=NET_BIND_SERVICE. Container sem capabilities desnecessárias é muito mais difícil de escalar.
Seccomp profileDocker tem um profile default que bloqueia ~40 syscalls perigosas. Não desabilite com --security-opt seccomp=unconfined em prod.
Sem --privileged--privileged desliga quase todo o isolamento. Use --cap-add específico se precisar de permissão fina.
Scan de imagemdocker scout cves minha-img:1.0 ou Trivy. Rode no CI e falhe builds com CVE High/Critical.
Não copie segredos pra imagemNunca COPY .env. Use --secret do BuildKit ou secrets do Compose. Segredo na imagem vaza no registry.
Rootless DockerAlternativa: rodar dockerd como usuário não-root. Mitiga boa parte do risco de escape.
dockerfile
# 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

bash
# 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:

Build paraleloStages independentes compilam em paralelo.
Cache mountsRUN --mount=type=cache,target=/root/.npm npm ci — persiste o cache do npm entre builds.
Secret mountsRUN --mount=type=secret,id=npmrc npm ci — segredo some após o RUN, não fica na camada.
SSH mountRUN --mount=type=ssh git clone ... — usa sua chave sem copiá-la pra imagem.
Build multi-archdocker buildx build --platform linux/amd64,linux/arm64 — uma imagem pra Mac M-series + servers x86.
dockerfile
# 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)

🪤
1. PID 1 e sinais. Se você roda 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).
🪤
2. Zombies em Node/Python. Mesma causa do PID 1. Node não faz wait() em filhos órfãos. Rode com --init ou use dumb-init como ENTRYPOINT.
🪤
3. node_modules do host vazando pro container. Num bind mount de dev, -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.
🪤
4. .dockerignore esquecido. Sem ele, o Docker manda node_modules, .git, .env e dumps de db pro contexto de build. Lentidão + potencial vazamento de segredo.
🪤
5. localhost dentro do container não é localhost do host. App dentro do container que faz fetch em http://localhost:5432 está chamando ele mesmo. Pra acessar o host, use host.docker.internal (Docker Desktop) ou --add-host.
🪤
6. Imagens multi-arch e Mac M-series. Uma imagem só para 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?

node:20-slim + multi-stage

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-alpinemenor (~30 MB), mas musl libc quebra bcrypt/sharp/node-gyp em casos comuns — investir em troubleshooting.

Alt: distrolessmais 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

Bind mount + volume anônimo em node_modules

-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çalento demais pra DX — inviável.

📋 Meu CI demora 8 min no build de uma API Python

Cache mount do pip via BuildKit + ordem correta de COPY

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-stagereduz imagem final mas não reduz o tempo de build — ataca problema diferente.

Perguntas típicas

Docker vai morrer porque Kubernetes “removeu” o Docker?

Não. O K8s deixou de usar 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?

COPY só copia arquivos. ADD também extrai tar.gz automaticamente e aceita URL — comportamento mágico que quase ninguém quer. Regra prática: use COPY, use ADD só se precisar do comportamento extra de propósito.

Por que minha imagem está 1.5 GB se meu binário tem 20 MB?

Quase sempre é (1) base grande (ubuntu puro vs slim) + (2) toolchain no stage final (gcc, make, headers) + (3) cache de package manager não limpo (/var/lib/apt/lists/, /root/.npm). Solução: base slim/alpine + multi-stage + limpar cache no mesmo RUN do install.

É seguro rodar docker em prod?

Sim, desde que você trate container como “processo privilegiado”, não como VM. Use usuário não-root, drop de capabilities, filesystem read-only, scan de CVE, registry privado. O maior risco não é o Docker em si — é desenvolvedor rodando --privilegedem produção porque “funcionou no dev”.

Preciso aprender Docker antes de Kubernetes?

Sim, com folga. K8s orquestra containers — você precisa entender imagem, Dockerfile, volume e rede primeiro. Pular direto pra K8s é aprender a dirigir antes de saber o que é um carro.
Take-aways. (1) Container é processo isolado por namespaces+cgroups, não VM. (2) Imagem é pilha de camadas — cacheie agressivamente (muda-raro primeiro, muda-sempre depois). (3) Multi-stage é obrigatório em prod — separa builder do runner. (4) Use imagem base certa (slim/alpine/distroless/scratch) pro seu caso; alpine quebra libs nativas com frequência. (5) ENTRYPOINT pro binário, CMD pros args default; sempre forma exec. (6) Volume pra tudo que não pode perder; bind mount só pra dev. (7) Rede bridge customizada dá DNS entre containers; porta com -p é NAT, não exposição real. (8) Segurança = não-root, read-only FS, drop caps, scan de CVE, secrets via BuildKit. O próximo módulo (Kubernetes) orquestra tudo isso em escala.
🧩

Quiz rápido

4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito

Continue lendo