DevOps Admin

Como Usar Docker Fundamentos: Imagens, Containers, Volumes e Redes em Produção Já leu

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

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/

Artigos relacionados