Solução de Problemas: Contêineres Docker Saindo Imediatamente

DockerBeginner
Pratique Agora

Introdução

Neste laboratório prático, você aprenderá a identificar e resolver um problema comum do Docker: contêineres que saem imediatamente após a inicialização. Este problema frequentemente confunde iniciantes e pode ocorrer por várias razões, desde erros de configuração até problemas de aplicação.

Ao completar este laboratório, você entenderá o ciclo de vida de um contêiner Docker, aprenderá a diagnosticar saídas imediatas de contêineres, dominará técnicas de depuração e implementará as melhores práticas para garantir que seus contêineres sejam executados de forma confiável. Essas habilidades são essenciais para qualquer pessoa que trabalhe com Docker em ambientes de desenvolvimento ou produção.

Compreendendo os Fundamentos de Contêineres Docker

Vamos começar explorando os fundamentos dos contêineres Docker e nos familiarizar com os comandos básicos usados para gerenciar contêineres.

O que é um Contêiner Docker?

Um contêiner Docker é um pacote de software leve, autônomo e executável que inclui tudo o que é necessário para executar uma aplicação:

  • Código
  • Runtime (Tempo de Execução)
  • Ferramentas do sistema
  • Bibliotecas
  • Configurações

Os contêineres são executados em isolamento do sistema host e de outros contêineres, proporcionando consistência em diferentes ambientes.

Explorando o Docker em Seu Sistema

Primeiro, vamos verificar se o Docker está corretamente instalado e em execução em seu sistema:

docker --version

Você deve ver uma saída semelhante a:

Docker version 20.10.21, build baeda1f

Em seguida, vamos verificar se algum contêiner está atualmente em execução:

docker ps

Este comando lista os contêineres em execução. Como ainda não iniciamos nenhum, você deve ver apenas os cabeçalhos das colunas:

CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Para visualizar todos os contêineres, incluindo os parados:

docker ps -a

Compreendendo o Ciclo de Vida do Contêiner

O ciclo de vida do contêiner Docker consiste em vários estados:

  1. Created (Criado): Um contêiner é criado, mas ainda não foi iniciado
  2. Running (Em Execução): O contêiner está executando seus processos definidos
  3. Paused (Pausado): Os processos do contêiner são temporariamente suspensos
  4. Stopped (Parado): O contêiner saiu ou foi parado
  5. Deleted (Excluído): O contêiner foi removido do sistema

Vamos executar um contêiner simples e observar seu ciclo de vida:

docker run hello-world

Você deve ver uma saída semelhante a:

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.
...

Observe que este contêiner foi executado e saiu imediatamente após exibir a mensagem. Este é, na verdade, um comportamento normal para o contêiner hello-world, pois ele foi projetado para simplesmente exibir uma mensagem e sair.

Verifique o contêiner na lista de todos os contêineres:

docker ps -a

Você deve ver seu contêiner hello-world na lista com o status "Exited (Saiu)":

CONTAINER ID   IMAGE         COMMAND    CREATED          STATUS                      PORTS     NAMES
a1b2c3d4e5f6   hello-world   "/hello"   30 seconds ago   Exited (0) 30 seconds ago             peaceful_hopper

O código de saída (0) indica que o contêiner saiu com sucesso, sem erros.

Comandos Docker Chave para Gerenciamento de Contêineres

Aqui estão alguns comandos Docker essenciais que você usará ao longo deste laboratório:

  • docker run [OPTIONS] IMAGE [COMMAND]: Cria e inicia um contêiner
  • docker ps: Lista os contêineres em execução
  • docker ps -a: Lista todos os contêineres (incluindo os parados)
  • docker logs [CONTAINER_ID]: Visualiza os logs do contêiner
  • docker inspect [CONTAINER_ID]: Obtém informações detalhadas do contêiner
  • docker exec -it [CONTAINER_ID] [COMMAND]: Executa um comando em um contêiner em execução
  • docker stop [CONTAINER_ID]: Para um contêiner em execução
  • docker rm [CONTAINER_ID]: Remove um contêiner

Agora que você entende os fundamentos dos contêineres Docker e seu ciclo de vida, passaremos para a solução de problemas de contêineres que saem inesperadamente.

Identificando Problemas de Saída de Contêineres

Nesta etapa, criaremos um contêiner Docker que sai imediatamente e aprenderemos a diagnosticar o problema.

Executando um Contêiner que Sai Imediatamente

Primeiro, vamos tentar executar um contêiner Ubuntu:

docker run ubuntu

Você notará algo interessante - o comando é concluído instantaneamente e retorna ao seu prompt. Onde está nosso contêiner Ubuntu? Vamos verificar:

docker ps

Nenhum contêiner está em execução. Agora, verifique todos os contêineres, incluindo os parados:

docker ps -a

Você verá uma saída semelhante a:

CONTAINER ID   IMAGE         COMMAND       CREATED          STATUS                      PORTS     NAMES
f7d9e7f6543d   ubuntu        "/bin/bash"   10 seconds ago   Exited (0) 10 seconds ago             focused_galileo
a1b2c3d4e5f6   hello-world   "/hello"      10 minutes ago   Exited (0) 10 minutes ago             peaceful_hopper

O contêiner Ubuntu foi iniciado e, em seguida, saiu imediatamente com um código de status 0, indicando que saiu sem erros. Este é o comportamento esperado, mas pode ser confuso para iniciantes.

Entendendo Por Que os Contêineres Saem

Os contêineres são projetados para executar um comando ou processo específico. Quando esse processo é concluído ou sai, o contêiner para. Este é um princípio fundamental do design do Docker.

O contêiner Ubuntu saiu imediatamente porque:

  1. O comando padrão para a imagem Ubuntu é /bin/bash
  2. Quando executado sem as flags -it (interativo, terminal), não há entrada para o shell bash
  3. Sem entrada e sem um comando específico para executar, o bash sai imediatamente
  4. Quando o processo principal (bash) sai, o contêiner para

Visualizando Logs e Informações do Contêiner

Vamos examinar os logs do nosso contêiner Ubuntu que saiu para entender o que aconteceu. Primeiro, encontre o ID do contêiner na saída do seu docker ps -a, então:

docker logs CONTAINER_ID

Substitua CONTAINER_ID pelo ID real do seu contêiner. Você provavelmente não verá nenhuma saída porque o contêiner não produziu nenhum log antes de sair.

Para obter informações mais detalhadas sobre o contêiner:

docker inspect CONTAINER_ID

Isso exibirá um grande objeto JSON com a configuração do contêiner e informações de estado.

Vamos nos concentrar no código de saída:

docker inspect CONTAINER_ID --format='{{.State.ExitCode}}'

Você deve ver:

0

Isso confirma que o contêiner saiu normalmente, não devido a um erro.

Mantendo um Contêiner em Execução

Para manter o contêiner Ubuntu em execução, precisamos:

  1. Fornecer uma sessão interativa, ou
  2. Substituir o comando padrão por um processo de longa duração

Vamos tentar a abordagem interativa:

docker run -it ubuntu

Agora você está dentro do contêiner com um prompt bash:

root@3a4b5c6d7e8f:/#

O contêiner permanece em execução enquanto esta sessão bash estiver ativa. Digite exit ou pressione Ctrl+D para sair do contêiner.

exit

Alternativamente, podemos manter um contêiner em execução fornecendo um comando que não seja concluído imediatamente:

docker run -d ubuntu sleep 300

Isso executa o contêiner Ubuntu e executa o comando sleep 300, que manterá o contêiner em execução por 300 segundos (5 minutos).

Verifique se o contêiner está em execução:

docker ps

Você deve ver seu contêiner no estado em execução:

CONTAINER ID   IMAGE     COMMAND       CREATED          STATUS          PORTS     NAMES
9a8b7c6d5e4f   ubuntu    "sleep 300"   10 seconds ago   Up 10 seconds             hopeful_hopper

Ao diagnosticar contêineres que saem imediatamente, lembre-se destes pontos-chave:

  1. Os contêineres saem quando seu processo principal é concluído
  2. Verifique os logs e códigos de saída para entender por que eles pararam
  3. Se um contêiner deve continuar em execução, certifique-se de que seu processo principal não saia

Solucionando Problemas Comuns de Saída de Contêineres

Agora que entendemos por que os contêineres saem imediatamente, vamos explorar as causas comuns de saídas inesperadas de contêineres e como solucionar esses problemas.

Criando um Contêiner com um Problema

Vamos criar uma situação simples em que um contêiner sai inesperadamente. Primeiro, crie um diretório para nossos arquivos de teste:

mkdir -p ~/project/docker-exit-test
cd ~/project/docker-exit-test

Agora, crie um script Python simples com um erro:

nano app.py

Adicione o seguinte código:

import os

## Attempt to read a required environment variable
database_url = os.environ['DATABASE_URL']

print(f"Connecting to database: {database_url}")
print("Application running...")

## Rest of the application code would go here

Salve e saia do arquivo (pressione Ctrl+O, Enter, depois Ctrl+X).

Agora, crie um Dockerfile para construir uma imagem com esta aplicação:

nano Dockerfile

Adicione o seguinte conteúdo:

FROM python:3.9-slim

WORKDIR /app

COPY app.py .

CMD ["python", "app.py"]

Salve e saia do arquivo.

Construa a imagem Docker:

docker build -t exit-test-app .

Você deve ver uma saída indicando que a imagem foi construída com sucesso:

Successfully built a1b2c3d4e5f6
Successfully tagged exit-test-app:latest

Agora, execute o contêiner:

docker run exit-test-app

Você deve ver o contêiner sair imediatamente com um erro:

Traceback (most recent call last):
  File "/app/app.py", line 4, in <module>
    database_url = os.environ['DATABASE_URL']
  File "/usr/local/lib/python3.9/os.py", line 679, in __getitem__
    raise KeyError(key) from None
KeyError: 'DATABASE_URL'

O contêiner saiu porque nosso script Python esperava uma variável de ambiente que não foi fornecida.

Diagnosticando o Problema

Quando um contêiner sai inesperadamente, siga estas etapas de solução de problemas:

  1. Verifique o código de saída para determinar se é um erro:
docker ps -a

Procure seu contêiner e observe o código de saída. Ele deve ser diferente de zero, indicando um erro:

CONTAINER ID   IMAGE           COMMAND            CREATED          STATUS                     PORTS     NAMES
b1c2d3e4f5g6   exit-test-app   "python app.py"   20 seconds ago   Exited (1) 19 seconds ago           vigilant_galileo
  1. Examine os logs do contêiner:
docker logs $(docker ps -a -q --filter ancestor=exit-test-app --latest)

Isso recupera os logs do contêiner mais recente criado a partir de nossa imagem.

A mensagem de erro mostra claramente o problema: o script Python está tentando acessar uma variável de ambiente chamada DATABASE_URL que não existe.

Corrigindo o Problema

Agora, vamos corrigir o problema fornecendo a variável de ambiente ausente:

docker run -e DATABASE_URL=postgresql://user:password@db:5432/mydatabase exit-test-app

Você deve ver o contêiner ser executado com sucesso:

Connecting to database: postgresql://user:password@db:5432/mydatabase
Application running...

O contêiner ainda sai, mas desta vez é porque nosso script chega ao fim e termina normalmente. Se quiséssemos que o contêiner continuasse em execução, precisaríamos modificar nosso script para incluir um loop infinito ou um processo de longa duração.

Causas Comuns de Saídas de Contêineres

Aqui estão várias razões comuns pelas quais os contêineres podem sair inesperadamente:

  1. Variáveis de ambiente ausentes: Como acabamos de demonstrar
  2. Dependências ausentes: Bibliotecas ou pacotes de sistema ausentes
  3. Falhas de conexão: Incapaz de alcançar um banco de dados ou outro serviço
  4. Problemas de permissão: Permissões insuficientes para acessar arquivos ou recursos
  5. Limitações de recursos: Contêiner fica sem memória ou CPU
  6. Falhas de aplicação: Bugs no código da aplicação

Técnicas de Solução de Problemas

Para cada um desses problemas, aqui estão abordagens eficazes de solução de problemas:

  1. Revisar logs: Sempre verifique os logs do contêiner primeiro com docker logs
  2. Substituir ponto de entrada (entrypoint): Use docker run --entrypoint /bin/sh -it my-image para entrar no contêiner e investigar
  3. Adicionar instruções de depuração: Modifique sua aplicação para adicionar mais logs
  4. Verificar o uso de recursos: Use docker stats para monitorar o uso de recursos do contêiner
  5. Inspecionar o ambiente: Execute docker exec -it CONTAINER_ID env para verificar as variáveis de ambiente

Vamos tentar a técnica de substituição do ponto de entrada com nossa imagem:

docker run --entrypoint /bin/sh -it exit-test-app

Agora você está dentro do contêiner com um shell. Você pode explorar o ambiente:

ls -la
cat app.py
echo $DATABASE_URL

Você verá que DATABASE_URL não está definido. Saia do contêiner quando terminar:

exit

Ao entender por que os contêineres saem e aplicar essas técnicas de solução de problemas, você pode diagnosticar e resolver rapidamente a maioria dos problemas de saída de contêineres.

Implementando as Melhores Práticas para a Confiabilidade de Contêineres

Agora que entendemos como solucionar problemas de saída de contêineres, vamos implementar as melhores práticas para tornar nossos contêineres mais confiáveis e robustos. Vamos aprimorar nosso aplicativo de exemplo com tratamento de erros adequado, verificações de integridade e registro (logging).

1. Melhorando o Tratamento de Erros

Vamos modificar nosso aplicativo Python para lidar com variáveis de ambiente ausentes de forma adequada:

cd ~/project/docker-exit-test
nano app.py

Atualize o conteúdo para:

import os
import time
import sys

## Get environment variables with defaults
database_url = os.environ.get('DATABASE_URL', 'sqlite:///default.db')

print(f"Connecting to database: {database_url}")

## Simulate a long-running process
try:
    print("Application running... Press Ctrl+C to exit")
    counter = 0
    while True:
        counter += 1
        print(f"Application heartbeat: {counter}")
        time.sleep(10)
except KeyboardInterrupt:
    print("Application shutting down gracefully...")
    sys.exit(0)

Salve e saia do arquivo.

Esta versão aprimorada:

  • Usa os.environ.get() com um valor padrão em vez de gerar uma exceção
  • Implementa um loop de longa duração para manter o contêiner ativo
  • Lida com o desligamento adequado quando é terminado

Vamos reconstruir a imagem:

docker build -t exit-test-app:v2 .

E execute o contêiner aprimorado:

docker run -d --name improved-app exit-test-app:v2

Verifique se o contêiner está em execução:

docker ps

Você deve ver seu contêiner em execução:

CONTAINER ID   IMAGE              COMMAND            CREATED          STATUS          PORTS     NAMES
c1d2e3f4g5h6   exit-test-app:v2   "python app.py"   10 seconds ago   Up 10 seconds             improved-app

Visualize os logs para confirmar que está funcionando:

docker logs improved-app

Você deve ver uma saída como:

Connecting to database: sqlite:///default.db
Application running... Press Ctrl+C to exit
Application heartbeat: 1
Application heartbeat: 2

2. Implementando uma Verificação de Integridade no Dockerfile

As verificações de integridade permitem que o Docker monitore a integridade do seu contêiner. Vamos atualizar nosso Dockerfile para incluir uma verificação de integridade:

nano Dockerfile

Atualize-o para:

FROM python:3.9-slim

WORKDIR /app

## Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

COPY app.py .

## Add a health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

## Expose port for the health check endpoint
EXPOSE 8080

CMD ["python", "app.py"]

Agora precisamos adicionar um endpoint de verificação de integridade ao nosso aplicativo:

nano app.py

Substitua o conteúdo por:

import os
import time
import sys
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler

## Get environment variables with defaults
database_url = os.environ.get('DATABASE_URL', 'sqlite:///default.db')

## Simple HTTP server for health checks
class HealthRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/health':
            self.send_response(200)
            self.send_header('Content-type', 'text/plain')
            self.end_headers()
            self.wfile.write(b'OK')
        else:
            self.send_response(404)
            self.end_headers()

def run_health_server():
    server = HTTPServer(('0.0.0.0', 8080), HealthRequestHandler)
    print("Starting health check server on port 8080")
    server.serve_forever()

## Start health check server in a separate thread
health_thread = threading.Thread(target=run_health_server, daemon=True)
health_thread.start()

print(f"Connecting to database: {database_url}")

## Main application loop
try:
    print("Application running... Press Ctrl+C to exit")
    counter = 0
    while True:
        counter += 1
        print(f"Application heartbeat: {counter}")
        time.sleep(10)
except KeyboardInterrupt:
    print("Application shutting down gracefully...")
    sys.exit(0)

Salve e saia do arquivo.

Reconstrua a imagem com nossa verificação de integridade:

docker build -t exit-test-app:v3 .

Execute o contêiner com a nova versão:

docker run -d --name healthcheck-app -p 8080:8080 exit-test-app:v3

Após cerca de 30 segundos, verifique o status da integridade:

docker inspect --format='{{.State.Health.Status}}' healthcheck-app

Você deve ver:

healthy

Você também pode testar o endpoint de integridade diretamente:

curl http://localhost:8080/health

Isso deve retornar OK.

3. Usando as Políticas de Reinicialização do Docker

O Docker fornece políticas de reinicialização para reiniciar automaticamente os contêineres quando eles saem ou encontram erros:

docker run -d --restart=on-failure:5 --name restart-app exit-test-app:v3

Esta política reiniciará o contêiner até 5 vezes se ele sair com um código diferente de zero.

Políticas de reinicialização disponíveis:

  • no: Nunca reiniciar (padrão)
  • always: Sempre reiniciar, independentemente do status de saída
  • unless-stopped: Sempre reiniciar, a menos que seja parado manualmente
  • on-failure[:max-retries]: Reiniciar apenas em saída diferente de zero

4. Definindo Limites de Recursos

Para evitar falhas de contêineres devido ao esgotamento de recursos, defina limites de recursos apropriados:

docker run -d --name resource-limited-app \
  --memory=256m \
  --cpus=0.5 \
  exit-test-app:v3

Isso limita o contêiner a 256MB de memória e metade de um núcleo de CPU.

5. Configuração de Registro (Logging) Adequada

Para uma melhor depuração, configure seu aplicativo para gerar logs estruturados:

docker run -d --name logging-app \
  --log-driver=json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  exit-test-app:v3

Isso configura o contêiner para usar o driver de log JSON com rotação de log (máximo de 3 arquivos de 10MB cada).

Resumo das Melhores Práticas

Ao implementar essas melhores práticas, você aprimorou significativamente a confiabilidade de seus contêineres Docker:

  1. Tratamento de erros adequado com valores padrão
  2. Verificações de integridade do contêiner para monitoramento
  3. Políticas de reinicialização apropriadas para recuperação automática
  4. Limites de recursos para evitar o esgotamento de recursos
  5. Configuração de registro (logging) adequada para facilitar a solução de problemas

Essas técnicas o ajudarão a criar contêineres resilientes que podem se recuperar de falhas e fornecer melhor observabilidade quando ocorrem problemas.

Resumo

Neste laboratório, você aprendeu habilidades essenciais para solucionar problemas e resolver questões de saída de contêineres Docker:

  • Compreensão do ciclo de vida do contêiner Docker e por que os contêineres saem quando seu processo principal é concluído
  • Identificação das causas comuns de saídas imediatas de contêineres por meio de logs e códigos de saída
  • Implementação de tratamento de erros robusto em aplicações em contêineres
  • Adição de verificações de integridade do contêiner para monitorar o status da aplicação
  • Configuração de políticas de reinicialização para recuperação automática de falhas
  • Definição de limites de recursos apropriados para evitar falhas de contêineres
  • Implementação de estratégias de registro (logging) adequadas para facilitar a depuração

Essas habilidades formam a base de implantações confiáveis de contêineres Docker. À medida que você continua trabalhando com o Docker em cenários do mundo real, você achará essas técnicas de solução de problemas inestimáveis para manter aplicações em contêineres estáveis e resilientes.

Lembre-se de que as causas mais comuns de saídas imediatas de contêineres são:

  1. O processo principal concluindo sua tarefa (por design)
  2. Variáveis de ambiente ou configuração ausentes
  3. Erros ou exceções de aplicação
  4. Restrições de recursos ou problemas de conectividade

Ao aplicar as técnicas de diagnóstico e as melhores práticas abordadas neste laboratório, você pode identificar e resolver rapidamente esses problemas, garantindo que seus contêineres Docker sejam executados de forma confiável em qualquer ambiente.