Docker Fundamentos: Compreendendo o Ecossistema de Containerização
Docker é uma plataforma de containerização que permite empacotar uma aplicação com todas as suas dependências em uma unidade isolada chamada container. Diferente de máquinas virtuais, containers compartilham o kernel do sistema operacional, tornando-os muito mais leves e rápidos. Quando você trabalha com Docker, está utilizando quatro componentes principais: imagens (blueprints), containers (instâncias em execução), volumes (armazenamento persistente) e redes (comunicação entre containers). Compreender como esses quatro elementos funcionam juntos é essencial para dominar Docker e criar arquiteturas robustas.
A grande vantagem do Docker é a garantia de que sua aplicação funcionará da mesma forma em qualquer ambiente — no seu computador local, em um servidor de produção ou na nuvem. Isso é possível porque a imagem Docker encapsula tudo: código-fonte, dependências, variáveis de ambiente e configurações. Neste artigo, vou guiá-lo através de cada componente com exemplos práticos que você pode executar imediatamente.
Imagens Docker: O Blueprint da Sua Aplicação
O Conceito por Trás das Imagens
Uma imagem Docker é um template imutável que contém tudo necessário para executar uma aplicação: sistema operacional, runtime, bibliotecas, código e configurações. É como um snapshot do seu ambiente. Quando você executa uma imagem, ela se torna um container — uma instância viva e isolada. A relação é simples: imagem é para container assim como classe é para objeto em programação orientada a objetos.
Para criar uma imagem, você escreve um arquivo chamado Dockerfile. Esse arquivo é um conjunto de instruções que descrevem como construir a imagem passo a passo. Cada instrução cria uma camada na imagem, e essas camadas são reutilizáveis, o que torna Docker muito eficiente em termos de armazenamento.
Criando Sua Primeira Imagem
Vou mostrar um exemplo prático com uma aplicação Python simples. Primeiro, crie um arquivo chamado app.py:
from flask import Flask
import os
app = Flask(__name__)
@app.route('/')
def hello():
return f"Olá! Hostname: {os.getenv('HOSTNAME', 'desconhecido')}"
@app.route('/api/status')
def status():
return {"status": "OK", "service": "WebApp"}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
Agora, crie um arquivo requirements.txt:
Flask==3.0.0
E finalmente, o Dockerfile:
# Usar imagem base oficial do Python
FROM python:3.11-slim
# Definir diretório de trabalho dentro do container
WORKDIR /app
# Copiar dependências
COPY requirements.txt .
# Instalar dependências
RUN pip install --no-cache-dir -r requirements.txt
# Copiar código da aplicação
COPY app.py .
# Expor porta
EXPOSE 5000
# Comando padrão
CMD ["python", "app.py"]
Para construir a imagem, execute:
docker build -t meu-app:1.0 .
O flag -t define o nome e tag da imagem. Após execução, você terá uma imagem chamada meu-app com a tag 1.0. Você pode verificar as imagens disponíveis com:
docker images
Boas Práticas em Imagens
Use imagens base oficiais e específicas (como python:3.11-slim) em vez de latest. A tag latest é um risco de segurança porque você nunca sabe exatamente qual versão está usando. Além disso, minimize camadas usando instruções multi-line com && e limpe caches desnecessários com --no-cache-dir. Quanto menor a imagem, mais rápida será para fazer download e executar.
Containers: Executando Suas Imagens
Do Blueprint para Instância em Execução
Um container é a execução real de uma imagem. Você pode ter múltiplas instâncias do mesmo container rodando simultaneamente, cada uma com seu próprio sistema de arquivos isolado, variáveis de ambiente e processos. Containers são efêmeros por padrão — quando você os para, tudo que foi modificado (exceto dados em volumes) é perdido.
Para executar um container a partir da imagem que criamos anteriormente:
docker run -d -p 8080:5000 --name meu-app-container meu-app:1.0
Vamos entender cada flag:
- -d: executa em background (detached mode)
- -p 8080:5000: mapeia a porta 5000 do container para 8080 da máquina host
- --name meu-app-container: dá um nome descritivo ao container
- meu-app:1.0: imagem que será executada
Para verificar se está rodando:
docker ps
Para acessar sua aplicação:
curl http://localhost:8080/
Gerenciando Containers
Para ver todos os containers (inclusive os parados):
docker ps -a
Para visualizar logs:
docker logs meu-app-container
Para ver logs em tempo real:
docker logs -f meu-app-container
Para acessar o shell dentro do container:
docker exec -it meu-app-container /bin/bash
O flag -it permite interação com o terminal do container. Isso é extremamente útil para debugging.
Para parar um container:
docker stop meu-app-container
Para removê-lo:
docker rm meu-app-container
Variáveis de Ambiente e Configuração
Containers podem receber variáveis de ambiente em tempo de execução sem precisar reconstruir a imagem. Isso é fundamental para adaptar uma mesma imagem a diferentes ambientes. Exemplo:
docker run -d \
-p 8080:5000 \
-e FLASK_ENV=production \
-e LOG_LEVEL=INFO \
--name meu-app-prod \
meu-app:1.0
Dentro do seu código Python, acesse com os.getenv('FLASK_ENV').
Volumes: Persistindo Dados
Por Que Volumes são Essenciais
Containers são efêmeros por design. Se você remover um container, todos os dados criados dentro dele são perdidos. Volumes resolvem esse problema ao permitir que você compartilhe diretórios entre o host e o container, ou crie armazenamento persistente gerenciado pelo Docker. Sem volumes, toda aplicação com banco de dados seria inútil.
Existem três tipos de mounts em Docker: volumes nomeados (gerenciados pelo Docker), bind mounts (pontos diretos do host) e tmpfs (em memória). Para aplicações em produção, volumes nomeados são a escolha mais segura.
Usando Volumes Nomeados
Primeiro, crie um volume:
docker volume create dados-app
Agora execute um container com esse volume:
docker run -d \
-p 8080:5000 \
--name meu-app-com-dados \
-v dados-app:/app/dados \
meu-app:1.0
Modifique seu app.py para criar um arquivo dentro do volume:
from flask import Flask
import os
import json
from datetime import datetime
app = Flask(__name__)
DATA_DIR = '/app/dados'
@app.route('/')
def hello():
return f"Olá! Container: {os.getenv('HOSTNAME', 'desconhecido')}"
@app.route('/api/salvar/<chave>/<valor>')
def salvar(chave, valor):
os.makedirs(DATA_DIR, exist_ok=True)
arquivo = os.path.join(DATA_DIR, 'dados.json')
dados = {}
if os.path.exists(arquivo):
with open(arquivo, 'r') as f:
dados = json.load(f)
dados[chave] = {
'valor': valor,
'timestamp': datetime.now().isoformat()
}
with open(arquivo, 'w') as f:
json.dump(dados, f, indent=2)
return {"status": "salvo", "chave": chave}
@app.route('/api/dados')
def obter_dados():
arquivo = os.path.join(DATA_DIR, 'dados.json')
if not os.path.exists(arquivo):
return {"dados": {}}
with open(arquivo, 'r') as f:
dados = json.load(f)
return {"dados": dados}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
Reconstrua a imagem:
docker build -t meu-app:2.0 .
Execute com o novo container:
docker run -d \
-p 8080:5000 \
--name app-persistente \
-v dados-app:/app/dados \
meu-app:2.0
Agora teste a persistência:
curl http://localhost:8080/api/salvar/usuario/joao
curl http://localhost:8080/api/dados
Você verá os dados salvos. Mesmo que você remova o container, os dados permanecerão:
docker rm -f app-persistente
docker volume ls # volume dados-app ainda existe
Bind Mounts para Desenvolvimento
Para desenvolvimento, bind mounts são convenientes. Você mapeia um diretório do host diretamente para o container:
docker run -d \
-p 8080:5000 \
-v /caminho/local:/app/dados \
--name app-dev \
meu-app:2.0
Mudanças em /caminho/local no seu computador refletem imediatamente no container.
Redes Docker: Comunicação Entre Containers
Isolamento e Conectividade
Por padrão, cada container é isolado. Eles não conseguem se comunicar uns com os outros apenas usando localhost. Docker oferece redes que permitem comunicação segura entre containers. Há três tipos principais: bridge (padrão, para containers no mesmo host), host (container compartilha rede do host) e overlay (para Docker Swarm).
Para a maioria dos casos, você usará redes bridge. Quando você cria uma rede bridge customizada, Docker fornece um DNS interno que permite resolver nomes de containers.
Criando uma Rede e Conectando Containers
Crie uma rede customizada:
docker network create minha-rede
Vamos criar uma segunda aplicação que funciona como um serviço de API. Crie um arquivo api.py:
from flask import Flask
import os
app = Flask(__name__)
@app.route('/api/info')
def info():
return {
"servico": "API",
"versao": "1.0",
"hostname": os.getenv('HOSTNAME', 'desconhecido')
}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=3000, debug=False)
Crie um Dockerfile.api:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY api.py .
EXPOSE 3000
CMD ["python", "api.py"]
Construa a imagem:
docker build -f Dockerfile.api -t meu-api:1.0 .
Agora execute ambos os containers na mesma rede:
docker run -d \
--name api-service \
--network minha-rede \
meu-api:1.0
docker run -d \
--name web-service \
--network minha-rede \
-p 8080:5000 \
meu-app:2.0
Modifique app.py para fazer requisições ao serviço de API:
from flask import Flask
import os
import requests
import json
from datetime import datetime
app = Flask(__name__)
DATA_DIR = '/app/dados'
API_URL = 'http://api-service:3000/api/info'
@app.route('/')
def hello():
return "Olá! Web Service rodando."
@app.route('/api/info')
def info():
try:
resp = requests.get(API_URL, timeout=2)
api_data = resp.json()
except Exception as e:
api_data = {"erro": str(e)}
return {
"web_hostname": os.getenv('HOSTNAME', 'desconhecido'),
"api_resposta": api_data
}
@app.route('/api/salvar/<chave>/<valor>')
def salvar(chave, valor):
os.makedirs(DATA_DIR, exist_ok=True)
arquivo = os.path.join(DATA_DIR, 'dados.json')
dados = {}
if os.path.exists(arquivo):
with open(arquivo, 'r') as f:
dados = json.load(f)
dados[chave] = {
'valor': valor,
'timestamp': datetime.now().isoformat()
}
with open(arquivo, 'w') as f:
json.dump(dados, f, indent=2)
return {"status": "salvo", "chave": chave}
@app.route('/api/dados')
def obter_dados():
arquivo = os.path.join(DATA_DIR, 'dados.json')
if not os.path.exists(arquivo):
return {"dados": {}}
with open(arquivo, 'r') as f:
dados = json.load(f)
return {"dados": dados}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
Atualize requirements.txt:
Flask==3.0.0
requests==2.31.0
Reconstrua e execute:
docker build -t meu-app:3.0 .
docker run -d \
--name web-service \
--network minha-rede \
-p 8080:5000 \
-v dados-app:/app/dados \
meu-app:3.0
Agora teste a comunicação:
curl http://localhost:8080/api/info
Você verá que o web-service conseguiu se conectar ao api-service usando apenas o nome api-service. Essa resolução automática é um super poder do Docker.
Inspecionando Redes
Para listar redes:
docker network ls
Para ver detalhes de uma rede:
docker network inspect minha-rede
Você verá quais containers estão conectados e seus endereços IP internos.
Conclusão
Dominar Docker significa entender como esses quatro componentes trabalham em harmonia. Imagens são seus blueprints imutáveis, construídas com Dockerfiles que descrevem exatamente o que sua aplicação precisa. Containers são as instâncias vivas dessas imagens, isoladas e escaláveis. Volumes garantem que seus dados sobrevivam além da vida do container, permitindo persistência real. Redes conectam containers de forma segura e automática, habilitando arquiteturas multi-container profissionais. Quando você consegue orquestrar esses quatro elementos, você tem a base sólida para trabalhar com ferramentas como Docker Compose e Kubernetes.
A prática é fundamental. Execute todos os exemplos neste artigo, experimente modificar os códigos, quebrantem erros propositalmente para entender as mensagens. Docker tem uma curva de aprendizado suave no início, mas profundidade infinita conforme você avança.
Referências
- Documentação Oficial do Docker: https://docs.docker.com/
- Docker Network Documentation: https://docs.docker.com/network/
- Best practices for writing Dockerfiles: https://docs.docker.com/develop/dev-best-practices/dockerfile_best-practices/
- Docker Volumes Documentation: https://docs.docker.com/storage/volumes/
- The Docker Book by James Turnbull: https://www.dockerbook.com/