Todos os artigos
56 artigos · atualizado semanalmente Veja nossas Ferramentas
Todos os artigos
Tutoriais

Docker explicado para quem nunca usou containers

Imagem vs container, Dockerfile básico e por que Docker resolve o clássico "na minha máquina funciona" — guia direto para devs iniciando.

COVER · Tutoriais

O dev novo no time acabou de clonar o repositório. Seguiu o README passo a passo. Rodou. Não funcionou. Versão diferente do Node. Biblioteca de sistema faltando. Variável de ambiente que ninguém documentou. Esse ciclo tem nome: "na minha máquina funciona" — e é irritante de resolver sem Docker.

Docker não nasceu pra ser a ferramenta favorita de todo mundo em entrevista técnica. Nasceu pra resolver esse problema específico: fazer o ambiente de execução viajar junto com o código.

O problema real que o Docker resolve

Quando você instala uma aplicação no seu computador, ela depende de dezenas de coisas além do código: versão do runtime, bibliotecas do sistema operacional, variáveis de ambiente, arquivos de configuração. Tudo isso existe na sua máquina mas não necessariamente em produção — ou na máquina do colega de equipe.

Antes do Docker, a solução era documentar tudo e torcer. Ou usar máquinas virtuais completas — o que funciona, mas custa tempo de setup e usa memória de forma ineficiente. Uma VM carrega um sistema operacional inteiro só pra rodar uma aplicação.

O Docker resolve isso de forma diferente: em vez de virtualizar o hardware inteiro, ele isola processos usando recursos do próprio kernel Linux (namespaces e cgroups). O resultado é um container: um processo isolado que enxerga apenas o que você definiu, mas compartilha o kernel da máquina host. Mais leve que uma VM, mais previsível que instalar na máquina nua.

Se você ainda está entendendo o cenário maior — como deploy, automação e CI/CD se encaixam nisso — vale a leitura de O que é DevOps além das ferramentas, que cobre a cultura e os processos por trás das ferramentas.

Imagem vs container: a distinção que todo mundo confunde no começo

Essa é a dúvida que aparece sempre. A analogia mais direta:

  • Imagem é a receita. É estática, imutável, descrita em arquivo.
  • Container é o que nasce quando você executa a receita. É dinâmico, tem estado, tem processo rodando.

Uma imagem pode gerar dez containers simultâneos. Cada container é uma instância daquela receita, isolada das outras. Você pode rodar a mesma imagem no seu Mac, na máquina do colega com Linux, e no servidor em nuvem — o container vai se comportar igual nos três lugares, porque o ambiente está definido dentro da imagem.

A imagem em si não roda nada. Ela é só um template. O container é onde a vida acontece.

Dockerfile: onde você descreve o ambiente

O Dockerfile é um arquivo de texto com instruções sequenciais. Cada instrução vira uma camada da imagem. Camadas são cacheadas — se você mudar só o código da aplicação, o Docker não precisa reinstalar as dependências, só aplica a diff da última camada.

Um Dockerfile básico para uma aplicação Node:

FROM node:20-alpine

WORKDIR /app

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

COPY . .

EXPOSE 3000
CMD ["node", "src/index.js"]

Linha por linha:

  • FROM node:20-alpine — parte de uma imagem base. O Alpine Linux é minimalista, boa escolha para produção.
  • WORKDIR /app — define o diretório de trabalho dentro do container.
  • COPY package*.json ./ seguido de RUN npm ci — copia só o manifesto de dependências e instala. Isso aproveita o cache de camadas: se o package.json não mudou, o Docker pula esse passo no próximo build.
  • COPY . . — copia o restante do código depois das dependências. Intencionalmente depois, pra não invalidar o cache de npm ci a cada alteração de código.
  • EXPOSE 3000 — documentação de qual porta o processo vai usar. Não faz binding por si só.
  • CMD — o comando que executa quando o container sobe.

Para construir a imagem a partir desse Dockerfile:

docker build -t minha-api:1.0 .

Para rodar um container a partir dela:

docker run -p 3000:3000 minha-api:1.0

O -p 3000:3000 mapeia a porta 3000 do seu host para a porta 3000 do container. Sem esse mapeamento, o processo está rodando mas inacessível de fora.

O que acontece "por baixo" quando você faz docker run

Quando você executa docker run, o Docker:

  1. Pega a imagem (baixa do registry se não tiver local)
  2. Cria uma camada de escrita em cima das camadas somente-leitura da imagem
  3. Configura a rede virtual do container
  4. Executa o processo definido no CMD

O container vive enquanto o processo principal estiver ativo. Quando o processo encerra, o container para. Isso é diferente de uma VM, que fica "ligada" independentemente de ter processos ativos.

Containers são descartáveis por design. Se você precisa de estado persistente — banco de dados, uploads, logs — você usa volumes, que são mapeamentos de diretórios entre o host e o container.

Docker Compose: quando você tem mais de um container

Aplicação real geralmente tem pelo menos dois serviços: a aplicação e o banco de dados. O docker-compose.yml descreve todos eles e como se comunicam:

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/myapp
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Com isso, docker compose up sobe tudo junto. A API consegue acessar o banco pelo hostname db — que é o nome do serviço, resolvido automaticamente pela rede interna do Compose. Sem configurar nada de DNS.

O pgdata é um volume nomeado: os dados do Postgres persistem mesmo que você destrua e recrie o container do banco.

Por que isso resolve "na minha máquina funciona"

A resposta é direta: porque o ambiente deixa de ser implícito.

Antes do Docker, o ambiente estava documentado (quando estava) em READMEs, wikis, scripts de bootstrap que podiam estar desatualizados. Com o Dockerfile, o ambiente está no código. É versionado junto com a aplicação. Se funciona no CI, vai funcionar em produção — porque o CI e produção rodam a mesma imagem.

O container não conhece a versão do Node instalada no seu Mac. Ele usa a versão definida no FROM. A lib de sistema que faltava no servidor não falta mais — ela está na imagem. A variável de ambiente que ninguém documentou está no docker-compose.yml ou no guia de variáveis de ambiente do projeto, que agora é obrigatório pra subir localmente.

Isso não elimina todos os bugs de ambiente — diferenças de arquitetura (x86 vs ARM), secrets gerenciados fora do Compose, dependências externas como APIs de terceiros ainda podem causar divergências. Mas elimina a classe mais comum de problemas: versão errada de runtime e dependência de sistema faltando.

Para validar expressões de cron em containers agendados, o Gerador de Expressão Cron mostra as próximas execuções com fuso correto — útil quando o container roda em UTC mas o job foi escrito pensando em BRT.

Perguntas frequentes

Docker substitui máquina virtual?

Para a maioria dos casos de uso em desenvolvimento e deploy de aplicações, sim — e com vantagens. Containers sobem em segundos, consomem menos memória e são descartáveis. VMs ainda fazem sentido quando você precisa de isolamento completo de kernel (segurança de nível mais alto, diferentes SOs), ou quando a aplicação exige o sistema operacional completo por razões específicas. Para serviços web, APIs e bancos de dados, containers são a escolha padrão da indústria hoje.

Docker funciona igual no Windows, Mac e Linux?

No Linux, o Docker roda nativamente — containers compartilham o kernel da máquina. No Mac e Windows, o Docker Desktop roda uma VM Linux leve em segundo plano para prover o kernel necessário. Para desenvolvimento do dia a dia isso é transparente, mas pode haver pequenas diferenças de performance de I/O em alguns casos. Em produção, o ambiente é quase sempre Linux, então a imagem vai rodar sem camada extra.

O que é Docker Hub?

É o registry público oficial de imagens. Quando você escreve FROM node:20-alpine, o Docker baixa essa imagem do Docker Hub. É de lá que vêm as imagens base de linguagens, bancos de dados e ferramentas. Você também pode hospedar imagens privadas lá, ou usar registries alternativos como o GitHub Container Registry ou o Amazon ECR.

Devo usar Docker em desenvolvimento local?

Depende. Para equipes com mais de uma pessoa e projetos que têm backend + banco + outros serviços, Docker Compose economiza horas de onboarding. Para projetos pessoais ou ferramentas simples que você desenvolve sozinho, o overhead de aprender e manter o Dockerfile pode não valer no começo. A lógica inverte quando o projeto cresce ou quando outros devs precisam contribuir.

Dockerfile como documentação de ambiente

O maior benefício do Docker não é velocidade de deploy — é tornar o ambiente explícito e versionado. Um Dockerfile bem escrito responde a perguntas que antes ficavam na cabeça do dev que configurou o servidor original: qual versão do runtime? Quais dependências do sistema? Qual porta? Qual usuário executa o processo?

Isso tem valor direto em onboarding, em CI, em incidentes às 23h quando alguém precisa subir um container de emergência sem saber o histórico da máquina. O ambiente está no repositório, junto com o código que depende dele.

RD
Autor
Rafael Duarte
Desenvolvedor backend com passagem por fintech e SaaS B2B — trabalhou em times que escalaram APIs de zero a milhões de requisições. Carrega cicatrizes de produção suficientes para ter opiniões fortes sobre ferramentas, padrões e decisões de arquitetura. Não é acadêmico: leu a RFC do UUID quando precisou escolher entre v4 e v7 para uma tabela de alta escrita.
Ver perfil