InfraIniciante7 min de leitura6 seções2013, Data conhecida

Docker & containers

Empacota o app com tudo que ele precisa numa imagem imutável que roda igual em qualquer máquina. Container é essa imagem em execução — um processo isolado pelo kernel, não uma máquina virtual.

Em 10 segundos

Imagem = app + dependências + SO base, congelados num artefato imutável e versionado. Container = essa imagem rodando como processo isolado pelo kernel. O 'funciona na minha máquina' morre porque a máquina inteira (menos o kernel) viaja junto.

Antes de ter nome

Os `cgroups` e `namespaces` do Linux já existiam há anos, e o LXC empacotava os dois. O Docker não inventou o isolamento — inventou a imagem em camadas e o jeito de compartilhá-la.

  • #containers
  • #imagens
  • #deploy
  • #isolamento
  • #dockerfile
Analogia

O contêiner de navio

Antes do contêiner padronizado, cada carga era um problema: sacas, barris, caixotes — cada navio estivava de um jeito. O contêiner inverteu a lógica: a carga se adapta à caixa, e navio, guindaste e caminhão só precisam saber mover a caixa. Docker fez isso com software: o servidor não sabe se dentro roda Node, Python ou Java — só sabe rodar containers.

01Conceito

O problema: o ambiente que não viaja com o código

O código vai para produção, mas o ambiente fica para trás: a versão do runtime, as bibliotecas do sistema, as variáveis, o fuso do servidor. Cada máquina acumula diferenças invisíveis — e o bug que só acontece lá é quase sempre uma dessas diferenças.

VMs resolvem o isolamento, mas carregam um SO inteiro por app: gigabytes, minutos de boot. Container isola com namespaces e cgroups do próprio kernel — megabytes, milissegundos — porque não virtualiza hardware, só separa processos.

A imagem é feita de camadas: cada instrução do Dockerfile gera uma, e camadas são cacheadas e compartilhadas — dez serviços sobre a mesma base de Node compartilham essa base no disco e no registry. É por isso que a ordem do Dockerfile importa: dependências (que mudam pouco) antes do código (que muda sempre), para o cache aproveitar.

Container é descartável por design: o filesystem dele morre com ele. Estado que importa (banco, uploads) vive fora — em volumes ou serviços gerenciados. Essa é a inversão mental que dói no começo e sustenta todo o resto: você para de tratar servidor como lugar e passa a tratar como processo.

Como se organiza
  • Imagem (imutável, no registry)

    • camada base: SO mínimo

    • camada: dependências

    • camada: seu código

  • Container (processo em execução)

    • filesystem descartável

    • env + rede próprias

A imagem é o artefato — camadas empilhadas, cacheáveis, versionadas. O container é uma execução dela: nasce, roda, morre — e nada do que gravou dentro sobrevive.
02Anatomia

Da receita ao processo

Camada 1/3Dockerfile

Camada 1

Dockerfile

a receita, versionada junto do código

Declara base, dependências, build e comando de início. Multi-stage separa o estágio de build (compiladores, devDependencies) da imagem final (só o necessário para rodar) — a diferença entre 1,2 GB e 150 MB.

No código

FROM node:22 AS build
...
FROM node:22-slim  # imagem final magra

Se violar: imagem única com toolchain de build inteira = imagem gorda, deploy lento e superfície de ataque cheia de ferramentas que produção nunca usa.

03Implementação

Código

// O container não pede um framework — pede que seu app se comporte:
// config por ambiente, log em stdout e morte digna. O resto é Dockerfile.
import http from "node:http";

// 1. Config vem do ambiente — a MESMA imagem roda em dev, staging e prod
const porta = Number(process.env.PORT ?? 3000);
const urlBanco = process.env.DATABASE_URL; // nunca hardcoded na imagem

const servidor = http.createServer((req, res) => {
  // 2. Log em stdout: o runtime coleta; o app não gerencia arquivo de log
  console.log(JSON.stringify({ metodo: req.method, rota: req.url }));
  res.writeHead(200).end("ok");
});

servidor.listen(porta, () => console.log(`ouvindo em :${porta}`));

// 3. Morte digna: o orquestrador manda SIGTERM antes de matar.
//    Quem ignora, derruba requests no meio a cada deploy.
process.on("SIGTERM", () => {
  console.log("SIGTERM: parando de aceitar conexões...");
  servidor.close(() => process.exit(0)); // termina as requests em voo e sai
});

/* O Dockerfile correspondente (multi-stage: build pesado, imagem final magra):
   FROM node:22 AS build
   COPY . . && RUN npm ci && npm run build
   FROM node:22-slim
   COPY --from=build /app/dist ./dist
   USER node                # nunca root
   CMD ["node", "dist/main.js"]
*/
04No mundo real

Casos de uso

Onboarding que era uma semana, virou um comando

Cenário

O sistema tem API, worker, Postgres, Redis e um gateway — e o guia de setup do novo dev tinha 40 passos desatualizados. Cada notebook era um ambiente diferente, com bugs próprios.

Como o padrão entra

Um docker-compose.yml declara os cinco serviços com versões pinadas. O novo dev roda 'docker compose up' e tem o sistema inteiro, idêntico ao dos colegas, em minutos — e o guia de 40 passos vira um arquivo versionado que o CI também usa.

Trade-off: O time precisa de fluência mínima em Docker para depurar (logs, exec, volumes), e em máquinas modestas os cinco containers pesam mais que os serviços nativos.

A imagem testada é a imagem implantada

Cenário

O deploy antigo era 'git pull + npm install' no servidor: o install de produção às vezes resolvia versões diferentes das testadas, e o rollback era um exercício de arqueologia.

Como o padrão entra

O CI builda a imagem uma vez, roda os testes contra ela e a publica com tag imutável. Produção só troca a tag em execução; rollback é apontar para a tag anterior — o artefato antigo continua no registry, intacto.

Trade-off: Surge uma peça nova para operar (o registry) e uma disciplina nova: limpar imagens velhas, escanear vulnerabilidades da base, renovar a base quando o SO ganha CVE.

05Cuidado

Armadilhas

  1. Estado dentro do container

    SQLite no filesystem do container, uploads em ./uploads, sessão em memória: tudo isso morre no próximo deploy. Container é processo, não lugar — estado que importa vive em volume, banco ou storage externo, decidido antes do primeiro restart doer.

  2. Rodar como root

    O padrão das imagens é root, e um escape de container com root dentro é um problema de verdade. USER node (ou equivalente) no final do Dockerfile custa uma linha e corta a classe inteira de risco.

  3. :latest em produção

    Deploy com :latest é deploy de conteúdo desconhecido: o que sobe hoje não é o que subiu ontem, e rollback vira adivinhação. Tags imutáveis (versão ou SHA do commit) tornam cada deploy nomeável, auditável e reversível.

  4. Ignorar o SIGTERM

    No deploy, o orquestrador manda SIGTERM e espera; quem não trata é morto com as requests em voo — erros 502 a cada release. Fechar o servidor com graça são cinco linhas, e é a diferença entre deploy invisível e deploy que derruba usuários.

06Decisão

Quando usar × quando evitar

Use quando

3
  • Serviços de rede em produção — o caso padrão hoje.
  • Ambiente de desenvolvimento e CI reprodutíveis.
  • Sistemas com serviços em linguagens e runtimes diferentes.

Evite quando

3
  • Script pontual numa máquina só — o empacotamento não se paga.
  • Apps que dependem de kernel, driver ou GUI específicos.
  • Times sem ninguém para operar registry e ciclo de imagens — um PaaS esconde isso por você.

Testar o que ficou

4 armadilhas de Docker & containers viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • Dockerfile e Docker HubA receita da imagem e o registro de onde ela é baixada: você empacota app e dependências uma vez e roda igual em qualquer máquina.
  • runners de CICada job do GitHub Actions ou GitLab CI roda dentro de um contêiner limpo, garantindo o mesmo ambiente a cada execução do pipeline.
  • docker compose no dev localSubir banco, cache e app juntos com um comando dá a todo o time o mesmo ambiente, sem instalar serviço por serviço na máquina.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Mesma imagem do laptop ao cluster.
// docker build -t api:1 . && docker run api:1

O que ele cobra

1 salto a mais entre a chamada e o efeito.

  • Uma imagem a construir, versionar e manter, mais um registro de onde ela vive
  • A camada de contêiner adiciona um degrau entre o processo e a máquina para depurar

Não vale se: o ambiente é único, estável e já reproduzível sem esforço — empacotar aí adiciona um passo de build sem ganho de portabilidade.

Onde isto aparece

Continue por aqui