Aula 02 · Run

Containers, redes, volumes e Compose

Da imagem ao container em execução, na ordem do Docker: imagem → container → network → volume, com logs, Docker Compose e uma introdução ao Docker Swarm.

7 tópicos6 exercíciosdocker run · network · volume · Compose · SwarmGitHub Codespaces
Tópicos
    2.1

    Arquitetura do Docker

    Conceito

    O que o Docker gerencia

    docker CLI cliente
    ↓ REST API · /var/run/docker.sock
    Docker daemon dockerd
    Logs docker logs · log drivers
    Estatísticas docker stats · events
    Integrações Compose, Swarm, SDKs, IDEs
    →
    Objetos que o daemon gerencia
    1Imagempull, build e armazenamento localdocker image ls
    2Containercriar, iniciar, parar e removerdocker ps
    3Networkcomunicação entre containers e com o hostdocker network ls
    4Volumedados que sobrevivem ao containerdocker volume ls

    Esta aula segue a ordem do desenho: imagem → container → network → volume, e fecha com logs, Compose e Swarm.

    O daemon do Docker (dockerd)

    Processo principal do Docker. Atua como um servidor que escuta comandos da CLI ou de APIs e executa operações como:

    Gerenciar containersCriar, iniciar, parar e excluir
    Gerenciar imagensPull, build e armazenamento
    Gerenciar redesCriar e configurar redes para a comunicação entre containers
    Gerenciar volumesCriar e manter volumes para persistência de dados
    OrquestraçãoModo Swarm nativo e integração com outras ferramentas

    O daemon roda em background e precisa estar ativo para os comandos do Docker funcionarem.

    Do comando ao processo

    docker CLIcliente
    →
    REST API/var/run/docker.sock
    →
    dockerdbuild, redes, volumes, logs
    →
    containerdimagens, ciclo de vida
    →
    runccria namespaces e cgroups
    • O CLI é só um cliente HTTP. Qualquer ferramenta (Portainer, SDKs, VS Code) usa a mesma API.
    • O containerd-shim mantém cada container vivo mesmo se o dockerd reiniciar.
    • No Kubernetes, o kubelet fala direto com o containerd via CRI, sem dockerd. A imagem é a mesma porque é OCI.

    Ecossistema e licenciamento

    Docker Engine + CLI

    Open source e gratuitos. É o que usamos no Codespaces.

    Docker Desktop

    Pago para uso comercial em empresas com mais de 250 funcionários ou mais de US$ 10 milhões de receita anual.

    Alternativas

    Podman / Podman Desktop (daemonless, rootless), nerdctl + containerd, Rancher Desktop, Colima.

    Prática
    Exercício 2.1

    Conversando com o daemon

    ~15 min

    Comprovar que o CLI é apenas um cliente da REST API e enxergar a cadeia dockerd → containerd → shim → processo.

    1. Informações do daemon:

      terminal
      docker info --format 'Server {{.ServerVersion}} | Storage {{.Driver}} | Cgroup v{{.CgroupVersion}} | Logging {{.LoggingDriver}}'

      Uso de disco por tipo de objeto:

      terminal
      docker system df
    2. Chame a REST API diretamente pelo socket, sem o CLI:

      terminal
      curl -s --unix-socket /var/run/docker.sock http://localhost/version | python3 -m json.tool | head -12

      Agora a lista de imagens:

      terminal
      curl -s --unix-socket /var/run/docker.sock http://localhost/images/json | python3 -m json.tool | grep -m5 RepoTags -A1
    3. Acompanhe os eventos que o daemon emite enquanto um container nasce e morre. Primeiro, dispare o escutador de eventos em segundo plano:

      terminal
      timeout 8 docker events --format '{{.Type}} {{.Action}} {{.Actor.Attributes.name}}' &

      Depois, num mesmo terminal, dispare o evento e aguarde o escutador terminar:

      terminal
      sleep 1; docker run --rm --name evento alpine:3.22 echo "olá"; wait
    4. Suba um container de teste que fica rodando em segundo plano:

      terminal
      docker run -d --name arq alpine:3.22 sleep 3600
    5. Veja a cadeia de processos no host onde o Docker roda. O comando abre um container que enxerga os processos do host (--pid=host) e mostra a árvore com o ps do Linux, então funciona igual no Codespaces, no Linux e no Docker Desktop (Mac/Windows):

      terminal
      docker run --rm --pid=host alpine:3.22 sh -c "apk add -q procps && ps -e -o pid,ppid,args --forest" | grep -E 'dockerd|containerd|shim|sleep 3600' | grep -v grep
      Por que não direto no terminal do Mac?

      O ps do macOS não tem a opção --forest e, no Docker Desktop, o dockerd roda dentro de uma VM Linux, invisível para o macOS. No Codespaces (Linux) também dá para rodar direto: ps -ef --forest | grep -E 'dockerd|containerd|shim|sleep 3600' | grep -v grep.

    6. Remova o container de teste:

      terminal
      docker rm -f arq
    Resultado esperado
    • Logging json-file: o driver de log padrão (visto no tópico 2.5).
    • A API responde JSON com a mesma versão do docker version.
    • Os eventos mostram container create, start, die e destroy.
    • O ps --forest mostra containerd-shim-runc-v2 como pai do sleep.
    O que aprendemos com este exercício?
    • O docker é só um cliente da REST API do daemon: dá para falar com o socket direto.
    • Por baixo, a cadeia é dockerd → containerd → shim → processo do container.
    • Os eventos (create, start, die, destroy) mostram o ciclo de vida que as ferramentas de monitoramento consomem.
    2.2

    Containers: executar e operar

    Conceito

    docker run: flags essenciais

    docker run [OPÇÕES] IMAGEM[:TAG|@DIGEST] [COMANDO] [ARGS...]

    Flags mais usadas
    -dSegundo plano (detached)
    -itTerminal interativo
    --rmRemove ao terminar
    --name webNome fixo
    -p 8080:80Porta host:container
    -e K=v · --env-fileVariáveis de ambiente
    -v dados:/dataVolume ou bind mount
    --network redeRede do container
    --restart unless-stoppedPolítica de reinício
    --memory 256m --cpus 0.5Limites (cgroups)
    -w /app · -u 1000Diretório e usuário
    --entrypoint shTroca o entrypoint
    --platform linux/arm64Arquitetura da imagem
    --initPID 1 que repassa sinais
    Combinações do dia a dia
    Servidor web em segundo plano
    docker run -d --name web -p 8080:80 nginx:stable-alpine
    Shell descartável para testar
    docker run --rm -it alpine:3.22 sh
    Banco com variáveis e volume
    docker run -d --name db -e POSTGRES_PASSWORD=senha \
      -v pgdata:/var/lib/postgresql/data postgres:17
    Código local montado no container
    docker run --rm -it -v "$PWD":/app -w /app python:3.13-slim bash
    Serviço resiliente e limitado
    docker run -d --restart unless-stopped --memory 256m --cpus 0.5 minha/api:1.0.0
    Comando único e sair
    docker run --rm alpine:3.22 cat /etc/os-release

    Referência completa: docs.docker.com · docker container run. No terminal: docker run --help.

    Comandos do dia a dia

    Observar

    • docker ps -a
    • docker logs -f web
    • docker inspect web
    • docker stats / docker top web

    Interagir

    • docker exec -it web sh
    • docker cp web:/etc/x .
    • docker attach web

    Ciclo de vida

    • docker stop / start / restart
    • docker pause / unpause
    • docker rm -f, docker container prune

    Isolamento: namespaces e cgroups

    Namespaces: o que o processo enxerga

    • pid: a aplicação é o PID 1
    • net: IP, interfaces e portas próprias
    • mnt: sistema de arquivos da imagem
    • uts, ipc, user, cgroup

    cgroups: quanto o processo pode usar

    • --memory vira memory.max. Estourou: OOM kill, exit code 137
    • --cpus 0.5 vira cpu.max. CPU não mata: sofre throttling
    • Mesma base dos requests/limits do Kubernetes

    Sinais do Linux

    Sinais são o mecanismo que o kernel usa para notificar processos sobre eventos.

    SIGTERM (15)Pede que o processo termine de forma graciosa
    SIGKILL (9)Força a terminação imediata; não pode ser ignorado
    SIGINT (2)Interrompe o processo (Ctrl+C)
    SIGQUIT (3)Como o SIGINT, mas gera dump de memória
    SIGHUP (1)Hangup; usado para recarregar configuração
    SIGSTOP (19)Suspende a execução (docker pause usa o freezer do cgroup para o mesmo efeito)
    SIGCONT (18)Retoma um processo suspenso

    Ciclo de vida e sinais

    created
    →
    running
    →
    paused
    →
    exited
    →
    removed
    • docker stop envia SIGTERM ao PID 1 e, após 10 s, SIGKILL.
    • A aplicação precisa tratar SIGTERM para desligar de forma graciosa.
    • Na forma shell do CMD, o PID 1 é o sh, que não repassa o sinal. Use a forma exec ou --init.
    Prática
    Exercício 2.2

    Operando containers do início ao fim

    ~30 min

    Executar a imagem publicada na Aula 01, operar o container com a CLI, aplicar limites de recursos e observar o tratamento de sinais.

    1. Defina a imagem (sua do Docker Hub, ou nginx:stable-alpine):

      terminal
      export IMAGE="seu-usuario-dockerhub/container-technologies:1.0.1"

      Rode o container:

      terminal
      docker run -d --name web -p 8080:80 --restart unless-stopped "$IMAGE"

      Confira que está rodando:

      terminal
      docker ps

      Acesse a aplicação:

      terminal
      curl -s localhost:8080 | head -5

      Veja os logs:

      terminal
      docker logs web --tail 5

      Entre no container e rode um comando:

      terminal
      docker exec -it web sh -c 'hostname; ps'

      Inspecione o IP e a política de reinício:

      terminal
      docker inspect web --format 'IP={{.NetworkSettings.IPAddress}} Restart={{.HostConfig.RestartPolicy.Name}}'

      Pare e confira o estado:

      terminal
      docker stop web && docker ps -a --filter name=web

      Reinicie e remova:

      terminal
      docker start web && docker rm -f web
    2. Rode um container com limites de recursos:

      terminal
      docker run -d --name limitado --memory 128m --cpus 0.5 nginx:stable-alpine

      Confira os cgroups aplicados:

      terminal
      docker exec limitado cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max

      Compare com o uso em tempo real:

      terminal
      docker stats --no-stream limitado

      Remova:

      terminal
      docker rm -f limitado
    3. Force um OOM kill — um processo que dobra de tamanho até estourar 32 MB:

      terminal
      docker run --name oom --memory 32m alpine:3.22 sh -c 'x=a; while true; do x=$x$x; done'

      Confira a causa da morte:

      terminal
      docker inspect oom --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}'

      Remova:

      terminal
      docker rm oom
    4. Rode um processo que ignora SIGTERM:

      terminal
      docker run -d --name sem-trap alpine:3.22 sh -c 'while true; do sleep 1; done'

      Pare e cronometre:

      terminal
      time docker stop sem-trap

      Agora um processo que trata o sinal:

      terminal
      docker run -d --name com-trap alpine:3.22 sh -c 'trap "echo SIGTERM recebido; exit 0" TERM; while true; do sleep 1; done'

      Pare e cronometre de novo:

      terminal
      time docker stop com-trap

      Confira o log de encerramento e limpe:

      terminal
      docker logs com-trap; docker rm sem-trap com-trap
    Resultado esperado
    • A página da sua imagem responde na porta 8080 e a política de reinício aparece como unless-stopped.
    • memory.max = 134217728, cpu.max = 50000 100000 e o docker stats mostra limite de 128 MiB.
    • O container oom termina com OOMKilled=true ExitCode=137.
    • sem-trap leva cerca de 10 s para parar (SIGKILL); com-trap para em cerca de 1 s.
    O que aprendemos com este exercício?
    • Limites de recursos viram cgroups no kernel: memory.max e cpu.max.
    • Passou do limite de memória, o kernel mata o processo: OOMKilled, código de saída 137.
    • O docker stop manda SIGTERM e, depois do tempo de espera, SIGKILL: a aplicação precisa tratar o sinal para parar rápido e limpo.
    • A política de reinício decide o que acontece quando o container cai.
    Desafio

    Repita o sem-trap com docker run --init. O que muda e por quê?

    2.3

    Redes de containers

    Conceito

    Drivers de rede

    DriverComportamentoQuando usar
    bridge (padrão)Rede privada no host (docker0), NAT para fora. Sem resolução por nomeContainers avulsos
    bridge personalizadaIsolada por projeto, DNS interno (127.0.0.11) resolve nomes de containersPadrão para aplicações (o Compose cria uma)
    hostUsa a pilha de rede do host. Sem isolamento, sem -pPerformance extrema, agentes de monitoramento
    noneSó loopbackJobs isolados, processamento offline
    overlayRede virtual (VXLAN) entre vários hostsDocker Swarm

    Publicar porta × expor na rede

    Clientenavegador / curl
    →
    host:8080-p 8080:80 (iptables DNAT)
    →
    web:80rede lab-net
    →
    db:3306só acessível dentro da rede

    Só publique o que precisa ser acessado de fora. Entre containers da mesma rede, a comunicação é direta pelo nome do serviço, sem -p.

    Prática
    Exercício 2.3

    Testando cada driver de rede

    ~30 min

    Validar na prática uma hipótese por driver: bridge padrão, bridge personalizada, host e none. Em cada parte, rode os comandos e confira se o resultado confirma a hipótese.

    Parte 1 · bridge (padrão)

    Hipótese Containers na bridge padrão se falam pelo IP, mas não pelo nome, e só são acessados do host com -p.

    1. Suba o primeiro container, sem informar rede (cai na bridge padrão docker0):

      terminal
      docker run -d --name a1 alpine:3.22 sleep 3600

      Suba o segundo:

      terminal
      docker run -d --name a2 alpine:3.22 sleep 3600

      Confira as redes existentes:

      terminal
      docker network ls

      Veja o IP de cada um:

      terminal
      docker inspect -f '{{.NetworkSettings.IPAddress}}' a1 a2
    2. Teste a comunicação pelo IP:

      terminal
      docker exec a1 ping -c2 $(docker inspect -f '{{.NetworkSettings.Networks.bridge.IPAddress}}' a2)

      Agora pelo nome:

      terminal
      docker exec a1 ping -c2 a2
    3. Teste a saída para a internet:

      terminal
      docker exec a1 ping -c2 8.8.8.8

      Suba um nginx sem publicar porta:

      terminal
      docker run -d --name w1 nginx:stable-alpine

      Tente acessar pelo host:

      terminal
      curl -s --max-time 3 localhost:80 || echo "sem acesso pelo host"

      Remova e suba de novo, agora publicando a porta:

      terminal
      docker rm -f w1
      terminal
      docker run -d --name w1 -p 8080:80 nginx:stable-alpine

      Acesse de novo:

      terminal
      sleep 1; curl -sI localhost:8080 | head -1
    Resultado esperado

    Ping pelo IP e para 8.8.8.8 respondem; pelo nome dá ping: bad address 'a2'. O nginx só responde no host depois do -p 8080:80 (HTTP/1.1 200 OK).

    O que aprendemos com este exercício?
    • A bridge padrão dá IP e saída para a internet, mas não resolve nomes entre containers.
    • A bridge personalizada resolve por nome e isola de outras redes: é a escolha natural para aplicações com vários containers.
    • host usa a rede do host direto, sem -p; none deixa o container sem rede.
    • Na bridge, só com a porta publicada a aplicação é acessível de fora.

    Parte 2 · bridge personalizada

    Hipótese Numa rede criada pelo usuário, o Docker oferece DNS interno e isola os containers de outras redes.

    1. Crie a rede:

      terminal
      docker network create lab-net

      Suba dois containers nela:

      terminal
      docker run -d --name b1 --network lab-net alpine:3.22 sleep 3600
      terminal
      docker run -d --name b2 --network lab-net alpine:3.22 sleep 3600
    2. Teste a resolução de nomes:

      terminal
      docker exec b1 ping -c2 b2

      Veja quem responde o DNS:

      terminal
      docker exec b1 nslookup b2

      E para onde ele aponta:

      terminal
      docker exec b1 cat /etc/resolv.conf
    3. Teste o isolamento entre redes: a1 (bridge padrão) tentando alcançar b1 (lab-net):

      terminal
      docker exec a1 ping -c2 -W2 $(docker inspect -f '{{(index .NetworkSettings.Networks "lab-net").IPAddress}}' b1) || echo "isolado"

      Conecte a1 também à lab-net:

      terminal
      docker network connect lab-net a1

      Teste de novo:

      terminal
      docker exec a1 ping -c2 b1
    Resultado esperado

    Ping pelo nome funciona e o resolv.conf aponta para nameserver 127.0.0.11 (DNS embutido do Docker). a1 não alcança b1 até ser conectado à lab-net; depois, resolve até pelo nome.

    Parte 3 · host

    Hipótese Com --network host o container usa a pilha de rede do host: sem isolamento e sem NAT.

    1. Rode um nginx na rede do host, sem -p:

      terminal
      docker run -d --name h1 --network host nginx:stable-alpine

      Acesse direto:

      terminal
      sleep 1; curl -sI localhost:80 | head -1
    2. Veja as interfaces que o container enxerga:

      terminal
      docker run --rm --network host alpine:3.22 ip -brief addr

      Compare com as do host:

      terminal
      ip -brief addr
    Resultado esperado

    O curl na porta 80 responde sem nenhum -p, e o container lista as mesmas interfaces do codespace (eth0, docker0, veth…).

    Parte 4 · none

    Hipótese Com --network none o container fica totalmente isolado: só existe o loopback.

    1. Rode um container sem rede:

      terminal
      docker run --rm --network none alpine:3.22 ip addr

      Tente sair para a internet:

      terminal
      docker run --rm --network none alpine:3.22 ping -c2 -W2 8.8.8.8
    Resultado esperado

    Só aparece a interface lo, e o ping falha com Network unreachable.

    Limpeza

    1. Remova os containers:

      terminal
      docker rm -f a1 a2 b1 b2 w1 h1

      E a rede:

      terminal
      docker network rm lab-net
    Overlay

    O driver overlay liga containers em hosts diferentes e depende do Swarm. Ele aparece no tópico 2.7 (Docker Swarm).

    Docker Desktop (Mac/Windows)

    A Parte 3 deve ser feita no Codespaces: no Docker Desktop, o “host” é a VM interna do Docker, e o --network host não expõe as portas no seu computador por padrão.

    2.4

    Armazenamento e persistência

    Conceito

    Três formas de montar dados

    VolumeBind mounttmpfs
    Onde ficaÁrea gerenciada pelo Docker (/var/lib/docker/volumes)Qualquer diretório do hostMemória RAM do host
    Persiste?Sim, independente do containerSim (é o arquivo do host)Não
    Uso típicoBancos de dados, dados de aplicaçãoCódigo em desenvolvimento, configsCache, arquivos temporários, segredos em memória
    Sintaxe-v dados:/data-v /host/dir:/dir--tmpfs /cache

    Prefira --mount type=volume,src=dados,dst=/data em scripts: é mais explícito que -v. O conceito reaparece no Kubernetes como PersistentVolume, hostPath e emptyDir.

    Comandos de volume

    Gerenciar

    • docker volume create
    • docker volume ls
    • docker volume inspect
    • docker volume rm / prune

    Copiar arquivos

    • docker cp <container>:/caminho .
    • docker cp arquivo <container>:/caminho
    • Funciona mesmo com o container parado
    Prática
    Exercício 2.4

    Dados que sobrevivem ao container

    ~30 min

    Validar uma hipótese por tipo de armazenamento: camada de escrita, volume, bind mount e tmpfs. Em cada parte, confira se o dado sobrevive ao restart e à remoção do container.

    Parte 1 · sem montagem (camada de escrita)

    Hipótese A camada de escrita pertence ao container: os dados sobrevivem a stop/restart, mas somem quando o container é removido.

    1. Suba o container:

      terminal
      docker run -d --name c1 alpine:3.22 sleep 3600

      Grave um arquivo dentro dele:

      terminal
      docker exec c1 sh -c 'echo "dado importante" > /tmp/arquivo.txt'

      Reinicie e confira que o arquivo continua lá:

      terminal
      docker restart c1 && docker exec c1 cat /tmp/arquivo.txt

      Remova o container:

      terminal
      docker rm -f c1

      Suba um novo, com o mesmo nome:

      terminal
      docker run -d --name c1 alpine:3.22 sleep 3600

      Procure o arquivo, que não deveria estar mais lá:

      terminal
      docker exec c1 cat /tmp/arquivo.txt; docker rm -f c1
    Resultado esperado

    Depois do restart o arquivo continua lá; no container recriado, cat falha com No such file or directory.

    O que aprendemos com este exercício?
    • A camada de escrita sobrevive ao restart, mas morre junto com o container.
    • O volume é gerenciado pelo Docker, sobrevive à remoção e pode ser compartilhado entre containers.
    • O bind mount liga uma pasta do host, ótimo para desenvolvimento; com :ro, o container não escreve.
    • O tmpfs fica em memória e some no restart: bom para dados temporários e sensíveis.

    Parte 2 · volume nomeado

    Hipótese Um volume vive fora do ciclo de vida do container e é gerenciado pelo Docker: pode ser usado por vários containers.

    1. Crie um volume:

      terminal
      docker volume create dados

      Grave nele com um container descartável:

      terminal
      docker run --rm -v dados:/data alpine:3.22 sh -c 'echo "gravado em $(date)" >> /data/log.txt'
    2. Leia com outro container e grave de novo:

      terminal
      docker run --rm -v dados:/data alpine:3.22 sh -c 'echo "gravado em $(date)" >> /data/log.txt; cat /data/log.txt'
    3. Liste os volumes:

      terminal
      docker volume ls

      Veja onde este fica no host:

      terminal
      docker volume inspect dados --format '{{.Mountpoint}}'
    Resultado esperado

    log.txt mostra duas linhas, gravadas por containers diferentes que já foram removidos. O volume fica em /var/lib/docker/volumes/dados/_data.

    Parte 3 · bind mount

    Hipótese Bind mount liga uma pasta do host ao container: o que muda de um lado aparece do outro, e os arquivos são do host.

    1. Crie uma pasta no host com uma página:

      terminal
      mkdir -p ~/labs/bind && echo '<h1>v1 via bind mount</h1>' > ~/labs/bind/index.html

      Sirva-a com o nginx, somente leitura (:ro):

      terminal
      docker run -d --name bind -p 8082:80 -v ~/labs/bind:/usr/share/nginx/html:ro nginx:stable-alpine

      Acesse:

      terminal
      sleep 1; curl -s localhost:8082
    2. Edite o arquivo no host:

      terminal
      echo '<h1>v2 editado no host</h1>' > ~/labs/bind/index.html

      Acesse de novo, sem reiniciar nada:

      terminal
      curl -s localhost:8082
    3. Tente escrever pelo container:

      terminal
      docker exec bind sh -c 'echo teste > /usr/share/nginx/html/x.html'

      Remova o container:

      terminal
      docker rm -f bind

      Confira que o arquivo continua no host:

      terminal
      ls ~/labs/bind
    Resultado esperado

    O segundo curl já mostra “v2 editado no host”. A escrita pelo container falha com Read-only file system, e index.html continua no host depois do rm.

    Parte 4 · tmpfs

    Hipótese tmpfs vive na memória do host: é rápido, mas não persiste nem a um restart.

    1. Monte um tmpfs:

      terminal
      docker run -d --name tmp --tmpfs /cache:size=16m alpine:3.22 sleep 3600

      Grave nele e confira onde ele está:

      terminal
      docker exec tmp sh -c 'echo "temporario" > /cache/a.txt; df -h /cache; cat /cache/a.txt'
    2. Reinicie o container:

      terminal
      docker restart tmp

      Procure o arquivo:

      terminal
      docker exec tmp ls /cache

      Remova:

      terminal
      docker rm -f tmp
    Resultado esperado

    df mostra /cache como tmpfs de 16 MB; depois do restart, a pasta está vazia.

    Resumo e limpeza

    TipoSobrevive ao restart?Sobrevive ao rm?Onde fica
    Camada de escritaSimNãoDentro do container
    VolumeSimSimÁrea do Docker
    Bind mountSimSimPasta do host
    tmpfsNãoNãoMemória do host
    1. Remova o volume criado:

      terminal
      docker volume rm dados

      Confira que sumiu:

      terminal
      docker volume ls
    2.5

    Logs de containers

    Conceito

    Como o Docker captura logs

    Aplicaçãoescreve em stdout / stderr
    →
    Log driverconfigurado no dockerd ou por container
    →
    Destinoarquivo local, syslog, CloudWatch, Splunk…
    • Container deve logar em stdout/stderr, não em arquivo. A imagem oficial do NGINX, por exemplo, liga access.log ao stdout.
    • Prefira logs estruturados (JSON) para facilitar busca e correlação.
    • docker logs lê do driver local. Desde o Docker 20.10, o dual logging mantém uma cópia local mesmo com drivers remotos.

    Log drivers disponíveis

    DriverDestino
    json-fileArquivo JSON no host. Padrão do Docker
    localFormato binário compacto, com rotação por padrão. Recomendado para uso local
    noneDescarta os logs
    syslogDaemon syslog (local ou remoto)
    journaldsystemd journal
    gelfGraylog e coletores compatíveis (Logstash)
    fluentdFluentd / Fluent Bit
    awslogsAmazon CloudWatch Logs (padrão no ECS)
    gcplogsGoogle Cloud Logging
    splunkSplunk HTTP Event Collector
    etwlogsEvent Tracing for Windows

    Também existem plugins de terceiros (Loki, Elasticsearch, entre outros).

    Configuração e rotação

    Padrão do host

    /etc/docker/daemon.json:

    json
    {
      "log-driver": "local",
      "log-opts": { "max-size": "10m", "max-file": "3" }
    }

    Por container

    • --log-driver syslog
    • --log-opt max-size=10m
    • --log-opt mode=non-blocking: não trava a aplicação se o destino ficar lento

    Lendo logs

    • docker logs -f web
    • --tail 50, --since 10m
    • --timestamps
    • docker compose logs -f

    O json-file sem max-size cresce sem limite e pode encher o disco do host. No Kubernetes, o kubelet grava os logs em /var/log/pods e um DaemonSet (Fluent Bit, por exemplo) envia para CloudWatch, OpenSearch ou Loki.

    De comandos soltos a um arquivo

    Repare no padrão dos últimos quatro tópicos: cada peça da aplicação foi um comando docker separado, com flags para decorar e repetir toda vez que o ambiente precisa subir de novo.

    docker runTópico 2.2 · subir e operar o container
    →
    docker networkTópico 2.3 · conectar os containers entre si
    →
    docker volumeTópico 2.4 · persistir os dados
    →
    --log-driverTópico 2.5 · para onde vão os logs

    A seguir, Docker Compose: as mesmas peças — serviço, rede, volume, log — declaradas uma única vez, num arquivo versionado com o código. Um docker compose up sobe tudo, sempre do mesmo jeito, para qualquer pessoa do time.

    Sem exercício prático

    Tópico conceitual. Os comandos de leitura de logs já foram usados no exercício 2.2 e a prática com o Compose vem no tópico 2.6.

    2.6

    Docker Compose

    Conceito

    Docker Compose

    O que é: a ferramenta do próprio Docker para definir e rodar aplicações com vários containers a partir de um único arquivo (compose.yaml), com os serviços, as redes e os volumes da aplicação inteira.
    Problema que resolve: até aqui, cada container do WordPress (app + banco) foi subido na mão, repetindo rede, volume e ordem de inicialização toda vez. O Compose troca essa sequência de comandos decorados por uma definição declarativa, versionada com o código.

    Para que usar o Docker Compose?

    Ambiente de desenvolvimento

    App, banco, cache e outras dependências sobem juntos com um comando — sem instalar nada localmente.

    Testes automatizados

    O ambiente inteiro é criado e destruído em poucos comandos, isolado a cada execução do CI.

    Deploy em host único

    Mesma definição também serve para rodar em produção num único host Docker.

    Fonte: Docker docs · Compose: features and uses.

    Um compose.yaml de exemplo

    yaml
    services:
      web:
        image: nginx:alpine
        ports:
          - "8080:80"
        volumes:
          - html-data:/usr/share/nginx/html
        networks:
          - app-net
    
      db:
        image: postgres:16
        environment:
          - POSTGRES_PASSWORD=senha
        expose:
          - "5432"
        volumes:
          - db-data:/var/lib/postgresql/data
        networks:
          - app-net
    
    volumes:
      html-data:
      db-data:
    
    networks:
      app-net:
        driver: bridge
    Usuárionavegador
    ↓host:8080
    ⇄
    network: app-net (bridge)
    webnginx:alpine
    ↓
    html-data
    ⇄db:5432
    (expose)
    dbpostgres:16
    ↓
    db-data

    A aplicação descrita em um arquivo

    Docker Compose define serviços, redes e volumes de forma declarativa em compose.yaml. Um comando sobe o ambiente inteiro, igual para todo o time.

    services

    Cada container: image ou build, ports, environment, volumes, depends_on, healthcheck, restart.

    volumes

    Volumes nomeados, criados com o prefixo do projeto.

    networks

    Por padrão, uma rede <projeto>_default com DNS pelo nome do serviço.

    O campo version: é obsoleto na Compose Specification e gera aviso. Use docker compose (v2, plugin) em vez de docker-compose (v1, descontinuado).

    Comandos mais utilizados no dia a dia

    docker compose up -dCria e sobe tudo em segundo plano
    docker compose psEstado e saúde dos serviços
    docker compose logs -f wordpressAcompanha os logs de um serviço
    docker compose exec db shShell num serviço em execução
    docker compose configValida e mostra o YAML final (com variáveis resolvidas)
    docker compose down / down -vRemove containers e redes / também os volumes

    Ordem de inicialização com healthcheck

    depends_on simples só espera o container iniciar, não ficar pronto. Com condition: service_healthy, o WordPress só sobe depois que o banco responde ao healthcheck.

    restart: no · always · on-failure · unless-stopped.

    No Kubernetes, o equivalente é a readinessProbe, e a aplicação deve tolerar dependências indisponíveis com retry.

    Limites de recursos no Compose

    yaml
    services:
      api:
        image: minha/api:v1
        deploy:
          replicas: 3
          resources:
            limits:   { cpus: "0.50", memory: 256M }
            reservations: { memory: 128M }
    • Sem limite, um container pode consumir toda a CPU e memória do host e derrubar os vizinhos.
    • Escalar funciona para serviços stateless. Não escale um banco com --scale: cada réplica teria seus próprios dados.
    • Serviços com ports: "8080:80" fixo não escalam no mesmo host (conflito de porta).
    Prática
    Exercício 2.5

    WordPress + MariaDB com Compose

    ~30 min

    Provisionar um WordPress com banco de dados, volumes e rede próprios, e comprovar a persistência após destruir os containers. Base: containers/Lab-02.

    1. Crie a pasta e entre nela:

      terminal
      mkdir -p ~/labs/wordpress && cd ~/labs/wordpress

      Crie o compose.yaml:

      terminal
      cat > compose.yaml <<'EOF'
      services:
        db:
          image: mariadb:11.4
          restart: unless-stopped
          environment:
            MARIADB_ROOT_PASSWORD: senha1234
            MARIADB_DATABASE: wordpress
            MARIADB_USER: blog
            MARIADB_PASSWORD: blog1234
          volumes:
            - db-data:/var/lib/mysql
          networks: [backend]
          healthcheck:
            test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
            interval: 10s
            timeout: 5s
            retries: 5
            start_period: 20s
      
        wordpress:
          image: wordpress:6-apache
          restart: unless-stopped
          depends_on:
            db:
              condition: service_healthy
          environment:
            WORDPRESS_DB_HOST: db
            WORDPRESS_DB_USER: blog
            WORDPRESS_DB_PASSWORD: blog1234
            WORDPRESS_DB_NAME: wordpress
          ports:
            - "8080:80"
          volumes:
            - wp-data:/var/www/html
          networks: [backend]
      
      volumes:
        db-data:
        wp-data:
      
      networks:
        backend:
      EOF

      Valide o YAML:

      terminal
      docker compose config --quiet && echo "YAML válido"
    2. Suba os serviços:

      terminal
      docker compose up -d

      Acompanhe a ordem de inicialização:

      terminal
      docker compose ps

      Veja o log do banco:

      terminal
      docker compose logs db --tail 5
    3. Abra a porta 8080 pela aba Ports, conclua a instalação do WordPress e publique um post de teste.

    4. Destrua os containers, mantendo os volumes:

      terminal
      docker compose down

      Confira que os volumes continuam lá:

      terminal
      docker volume ls | grep wordpress

      Suba de novo:

      terminal
      docker compose up -d
    5. Ao terminar, remova tudo, inclusive volumes:

      terminal
      docker compose down -v
    Resultado esperado
    • docker compose ps mostra db como (healthy) antes de o wordpress iniciar.
    • Após down + up, o site volta com o post criado: os dados estão nos volumes wordpress_db-data e wordpress_wp-data.
    • Depois do down -v, os volumes somem e a instalação começa do zero.
    O que aprendemos com este exercício?
    • O Compose descreve serviços, redes e volumes num único arquivo declarativo.
    • depends_on com condition: service_healthy garante a ordem: o banco saudável antes do WordPress.
    • Volumes nomeados mantêm os dados entre down e up; o down -v apaga tudo.
    • O Compose não reconcilia: se um container cair, ninguém sobe outro. É o que o Swarm traz.
    Segurança

    Senhas no YAML servem só para laboratório. Em projetos reais use um arquivo .env fora do Git ou secrets: do Compose.

    2.7

    Docker Swarm

    Conceito

    Docker Swarm

    O que é: o orquestrador nativo do Docker — transforma vários hosts Docker (um cluster) em um único recurso, usando o mesmo formato do Compose (docker stack deploy -c compose.yaml).
    Problema que resolve: o Compose sobe uma aplicação muito bem, mas só num host só. Se esse host cai, ou se a demanda cresce além da capacidade de uma máquina, não há para onde crescer nem quem reponha o que caiu. O Swarm distribui os serviços por vários nós e os mantém no ar sozinho.

    Por que orquestrar containers?

    Estado desejado“3 réplicas de web:v2”
    →
    Observaestado atual do cluster
    →
    Comparadiferença entre os dois
    →
    Agecria, remove, move containers
    • Scheduling: decide em qual nó cada container roda.
    • Auto-reparo: container ou nó caiu, a réplica volta em outro lugar.
    • Rolling update e rollback, service discovery e balanceamento.

    Conceito: arquitetura do Docker Swarm

    Managers

    Guardam o estado do cluster e reconciliam o desejado com o real. Replicados via Raft: precisam de quórum (N/2 + 1). 3 managers toleram 1 falha; 5 toleram 2.

    Workers

    Executam as tasks (containers) de cada service.

    Rede

    2377/tcp gerência · 7946/tcp+udp descoberta · 4789/udp overlay (VXLAN). Routing mesh publica a porta em todos os nós.

    Portainer oferece uma interface gráfica para operar um cluster Swarm.

    Docker Swarm ainda é utilizado hoje?

    Onde ele ainda aparece

    • Times pequenos que já usam Docker e não querem a complexidade operacional do Kubernetes
    • Clusters simples, poucos nós, sem exigência de recursos avançados (CRDs, operators, autoscaling fino)
    • Ferramentas como Portainer mantêm suporte de primeira classe a Swarm

    Por que o mercado foi para o Kubernetes

    • Ecossistema CNCF: Helm, operators, service mesh, observabilidade — tudo pensado para K8s primeiro
    • Toda grande nuvem oferece Kubernetes gerenciado (EKS, GKE, AKS); Swarm gerenciado praticamente não existe
    • Mais vagas, mais comunidade, mais integrações de terceiros

    Resposta curta: sim, ainda é usado, mas é nicho. Para quem já domina Docker, continua sendo o caminho mais simples até o dia em que o cluster exigir o que só o Kubernetes oferece — e é exatamente aí que entram as Aulas 03 (Amazon ECS) e 04 (Kubernetes).

    Diagrama de baixo nível

    Clienterequisição HTTP
    ↓qualquer nó, porta 8080
    Routing meshbalanceia entre os nós
    ↓8080 → 80 (porta do container)
    cluster Swarm · overlay network VXLAN (4789/udp)
    manager 1raft: líder
    manager 2raft: seguidor
    manager 3raft: seguidor
    ↕2377/tcp · gerência · 7946/tcp+udp · descoberta
    worker 1
    web.1nginx:alpine
    worker 2
    web.2nginx:alpine
    worker 3
    web.3nginx:alpine
    Quórum (N/2)+1: com 3 managers, o cluster tolera a perda de 1 sem parar de decidir

    "Reproduzir requisição": a bolinha verde entra pelo Cliente, passa pela routing mesh e cai num container diferente a cada clique. "Reproduzir eleição Raft": simula o líder caindo (cinza, tracejado) — sem heartbeat, outro manager vira candidato (laranja) e pede voto ao único eleitor que resta; com o próprio voto do candidato, já são 2 de 3 (maioria), e ele assume como novo líder (verde). O nó caído volta como seguidor ao final, pronto para a próxima queda.

    Raft: por que esse algoritmo aparece tanto

    O diagrama anterior resolve um problema que não é só do Swarm: como vários processos, espalhados em máquinas diferentes, concordam sobre um único valor — "quem é o líder", "qual é o estado atual" — mesmo quando um deles cai a qualquer momento. Esse problema se chama consenso distribuído, e o Raft é hoje a solução mais usada para ele.

    Para que serve

    Mantém um log replicado idêntico entre várias réplicas, com um líder só por vez. É a peça que sustenta bancos de dados distribuídos, service discovery e o control plane de orquestradores.

    Por que importa

    Publicado em 2014 (Diego Ongaro e John Ousterhout, Stanford) como alternativa ao Paxos — correto, porém notoriamente difícil de entender e implementar. O Raft virou a escolha padrão de qualquer sistema distribuído novo.

    Quem usa Raft

    • Docker Swarm — eleição dos managers (este tópico)
    • Kubernetes — dentro do etcd, seu banco de estado (Aula 04)
    • HashiCorp — Consul, Nomad e Vault
    • Bancos distribuídos — CockroachDB, TiKV/TiDB, YugabyteDB
    • Filas e streaming — RabbitMQ (quorum queues), Kafka (modo KRaft, substituindo o ZooKeeper)

    Mesmo algoritmo, sempre o mesmo motivo: eleger um líder e replicar um log com segurança. O Raft do Swarm e o Raft dentro do etcd do Kubernetes resolvem exatamente o mesmo problema.

    Como o Raft decide quem manda

    Os três managers do diagrama anterior rodam o mesmo algoritmo de consenso entre si: só um é líder por vez, e nenhuma mudança vira estado oficial do cluster sem passar pela maioria.

    Raft · consenso entre os managers
    manager 1líder
    manager 2seguidor
    manager 3seguidor
    ↕ heartbeat constante do líder · se ele some, um seguidor vira candidato e pede voto aos outros

    1. Eleição

    Sem heartbeat do líder, um seguidor vira candidato e pede voto aos demais. Quem conseguir maioria (2 de 3) vence e assume como líder.

    2. Replicação do log

    Toda mudança (criar service, escalar, atualizar) é uma entrada de log que só o líder propõe e envia aos seguidores.

    3. Commit por quórum

    A entrada só vira estado oficial do cluster quando a maioria confirma tê-la gravado — não é preciso esperar todo mundo.

    Por isso o número de managers deve ser ímpar: com 3, o cluster tolera perder 1 sem parar de decidir (quórum 2/3); com 5, tolera perder 2 (quórum 3/5). Um número par não ganha tolerância extra e ainda arrisca empate na votação.

    O ciclo do Raft, em loop

    As mesmas três fases do slide anterior, animadas em sequência — eleição, liderança e replicação se repetindo continuamente.

    manager 1seguidor
    manager 2seguidor
    manager 3seguidor
    1 · Estado inicial: todos os managers são seguidores (followers)
    manager 1candidato
    manager 2seguidor
    manager 3seguidor
    2 · Sem heartbeat do líder, manager 1 vira candidato e pede voto aos outros
    manager 1líder
    manager 2seguidor
    manager 3seguidor
    3 · Com maioria dos votos (2 de 3), manager 1 vira líder e envia heartbeats
    manager 1líder → replica
    manager 2grava log
    manager 3grava log
    4 · O líder propõe a mudança; vira commit quando a maioria confirma

    Baseado na visualização interativa de The Secret Lives of Data · Raft — vale abrir e brincar com a eleição e as falhas de nó. Especificação completa em raft.github.io.

    Prática
    Exercício 2.6

    Swarm de um nó: auto-reparo e rolling update

    ~25 min

    Ver na prática o loop de reconciliação que o Compose não oferece: réplica que cai volta sozinha.

    1. Inicie o Swarm:

      terminal
      docker swarm init || docker swarm init --advertise-addr $(hostname -I | awk '{print $1}')

      Confira o nó:

      terminal
      docker node ls

      Crie um service com 3 réplicas:

      terminal
      docker service create --name web --replicas 3 -p 8083:80 nginx:1.27-alpine

      Veja as tasks:

      terminal
      docker service ps web
    2. Mate um dos containers:

      terminal
      docker rm -f $(docker ps -q --filter name=web. | head -1)

      Veja o Swarm repor:

      terminal
      sleep 5; docker service ps web
    3. Escale para 5 réplicas:

      terminal
      docker service scale web=5

      Faça um rolling update, uma réplica por vez:

      terminal
      docker service update --image nginx:1.28-alpine --update-parallelism 1 --update-delay 5s web

      Acompanhe o estado das tasks:

      terminal
      docker service ps web --filter desired-state=running

      Volte para a versão anterior:

      terminal
      docker service rollback web
    4. Crie a pasta e entre nela:

      terminal
      mkdir -p ~/labs/swarm && cd ~/labs/swarm

      Crie o arquivo da stack:

      terminal
      cat > stack.yaml <<'EOF'
      services:
        whoami:
          image: traefik/whoami
          ports: ["8084:80"]
          deploy:
            replicas: 3
            update_config: { parallelism: 1, delay: 5s }
            restart_policy: { condition: any }
      EOF

      Faça o deploy da stack:

      terminal
      docker stack deploy -c stack.yaml demo

      Confira os serviços:

      terminal
      docker stack services demo

      Chame o serviço várias vezes e veja o routing mesh alternar entre as réplicas:

      terminal
      for i in $(seq 6); do curl -s localhost:8084 | grep Hostname; done
    5. Limpe e saia do Swarm (o ECS vem na Aula 03 e o Kubernetes, na Aula 04):

      terminal
      docker stack rm demo; docker service rm web; docker swarm leave --force
    Resultado esperado
    • Após o rm -f, service ps mostra uma task Failed/Shutdown e uma nova Running: o Swarm reconciliou.
    • O update troca as réplicas uma a uma, com 5 s de intervalo; o rollback volta para 1.27.
    • As requisições na porta 8084 são respondidas por hostnames diferentes (routing mesh).
    O que aprendemos com este exercício?
    • Um orquestrador mantém o estado desejado: réplica que cai é reposta sozinha.
    • Escalar é mudar um número; o rolling update troca as réplicas aos poucos e permite rollback.
    • O routing mesh distribui as requisições entre as réplicas numa mesma porta.
    • É o mesmo loop de reconciliação que aparece no ECS e no Kubernetes.