
Container na prática — do docker run ao docker-compose
Aprendi Docker em três fases distintas, cada uma revelando um nível diferente de controle sobre o ambiente. O salto mais importante foi do docker run para o Dockerfile. O mais útil no dia a dia foi o docker-compose.
Aprendi Docker em partes, não de uma vez.
A primeira parte foi docker run. Baixar uma imagem do registry, rodar um container, ter um Postgres rodando na porta 5432 sem instalar nada na máquina. Isso pareceu mágica. E resolveu um problema imediato: dependências de desenvolvimento sem poluir o sistema operacional.
Por um tempo, ficou nisso. Docker era uma forma de rodar coisas que eu não queria instalar.
A segunda parte foi o Dockerfile.
O Dockerfile define uma imagem — uma descrição reproduzível do ambiente onde seu código vai rodar. Em vez de dizer "instale Python 3.9, depois instale essas dependências, depois configure essa variável de ambiente", você escreve um arquivo que faz tudo isso de forma declarativa e versionável.
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
Esse Dockerfile cria uma imagem que qualquer máquina com Docker pode executar e obter o mesmo resultado. A imagem é o artefato. Você para de entregar "código que roda na minha máquina" e passa a entregar "uma unidade de execução definida".
O Dockerfile tem uma propriedade importante: cada instrução cria uma camada, e camadas são cacheadas. Se você não mudou o requirements.txt, o pip install não vai rodar de novo. Isso torna o processo de build incremental e muito mais rápido.
A terceira parte — e a mais útil no dia a dia — foi o docker-compose.
Aplicações reais raramente são um container só. Você tem a aplicação, o banco de dados, talvez um cache Redis, talvez uma fila de mensagens. Orquestrar isso manualmente — rodar cada container com os parâmetros corretos, na ordem certa, com a rede configurada para que se comuniquem — é trabalhoso e propenso a erro.
docker-compose.yml descreve todos esses serviços em um arquivo:
services:
app:
build: .
ports:
- "8080:8080"
environment:
DATABASE_URL: postgres://user:pass@db:5432/mydb
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: pass
POSTGRES_USER: user
POSTGRES_DB: mydb
docker compose up sobe tudo. docker compose down derruba tudo. O ambiente inteiro, descrito em código, versionado junto com o projeto.
Tem armadilhas que aprendi do jeito difícil.
depends_on não espera o serviço ficar pronto — só espera o container iniciar. Seu banco de dados pode estar subindo ainda quando sua aplicação tenta conectar. A solução correta é a aplicação ter retry na conexão, ou usar um healthcheck no depends_on.
Volumes e dados persistentes: por padrão, quando você derruba e sobe um container de banco de dados, os dados somem. Se você quer persistência, precisa declarar um volume. Se não quer dados acumulando entre testes, não declare — o comportamento padrão de apagar tudo é o que você quer.
Build context: o COPY . . no Dockerfile copia todo o diretório. Se você tem node_modules ou .git no diretório, eles vão para a imagem. Use .dockerignore para excluir o que não deveria estar lá.
O que Docker não é: uma solução para todos os problemas de infraestrutura. Para orquestração de múltiplos hosts, alta disponibilidade, scaling automático — você vai precisar de Kubernetes ou equivalente. Docker e docker-compose são para desenvolvimento local e deployments simples.
Mas para o problema que resolvem — tornar o ambiente reproduzível e descrito em código — eles resolvem muito bem.
Essa semana: se você tem um projeto com dependências externas (banco de dados, Redis, qualquer serviço), escreva um docker-compose.yml com esses serviços. O objetivo é conseguir rodar docker compose up e ter tudo funcionando em qualquer máquina, sem setup manual.