Container FastAPI parando sozinho na VPS? Como o processo principal e o CMD definem o ciclo de vida

Ilustração de um container FastAPI representado como caixa conectada a um processo principal dentro de um servidor VPS

Fala aí, beleza? Tu sobe a API FastAPI em Docker na VPS, testa, funciona, e pouco depois (ou no primeiro reboot do servidor) ela simplesmente não está mais lá 😛

Este post é sobre diagnóstico: container que encerra sozinho ou que não volta quando a VPS reinicia. A instalação do Docker e o deploy completo estão no guia de subir a API com Docker e HTTPS, então aqui vamos direto ao que decide o ciclo de vida

O diagnóstico precisa conferir tanto o comando de inicialização e a política do Docker quanto os logs e os erros da aplicação

Por que o container FastAPI para sozinho?

O container só vive enquanto o processo principal dele estiver rodando

A documentação oficial do FastAPI sobre Docker diz isso com todas as letras: normalmente é um único processo (o comando ou programa que você definiu), e quando não sobra nenhum processo rodando dentro dele, o container para

O sintoma típico é esse: o container sobe e logo some da lista

Aqui costuma aparecer uma confusão comum, que é tratar imagem e container como a mesma coisa

E qual é a diferença?

  • Imagem: a versão estática, com arquivos, variáveis de ambiente e o comando padrão, sem nada em execução
  • Container: a instância em execução dessa imagem

Se você conhece orientação a objetos, a ideia é bem semelhante a classe e objeto: a imagem é a receita, o container é a coisa rodando 🙂

Por isso reconstruir a imagem não mantém nada no ar

O que mantém o container vivo é o processo que ele executa

Solução: garantir que o comando padrão da imagem seja o servidor da API rodando em primeiro plano. Se esse comando termina, o container termina junto

Como prevenir: trate o CMD como a linha que decide o ciclo de vida, e não como um detalhe esquecido no fim do Dockerfile

Como ver containers parados e descobrir o estado de cada um:

Rode docker ps -a

O sintoma é clássico: você roda docker ps e a API não aparece. A causa é simples: segundo a referência do docker container ls, por padrão o docker ps mostra só os containers em execução

A flag -a (ou --all) lista todos, inclusive os parados:

docker ps -a

# equivalente
docker container ls --all

Na listagem, cada container aparece com um estado, e cada um conta uma parte da história:

  • running: está em execução, então o problema provavelmente está em outro lugar (porta, proxy e tal)
  • exited: não está mais rodando, porque o processo terminou ou porque alguém rodou docker stop
  • created: o container foi criado, mas nunca foi iniciado

Checagem: procure o container da API e veja se ele está como exited ou created

Se está exited e ninguém deu docker stop, o processo principal terminou, e aí vale olhar o CMD (próxima seção)

Se está created, ele nem chegou a ser iniciado

Erro comum: concluir que o container foi apagado só porque sumiu do docker ps. Na maioria das vezes ele está ali, parado, esperando você olhar com o -a

CMD na forma exec ou shell: qual usar no Dockerfile do FastAPI?

Use a forma exec, com colchetes

O sintoma aqui é mais sutil: você para o container e o encerramento é brusco, e o código que deveria rodar no desligamento (os eventos de lifespan) não dispara

Que lifespan? É o mecanismo do FastAPI pra executar lógica quando a aplicação inicia e quando ela encerra, tipo abrir e fechar conexões

A documentação do FastAPI recomenda a forma exec justamente para o FastAPI encerrar de forma graciosa e os eventos de lifespan serem disparados

Lado a lado fica assim:

# Forma exec (recomendada)
CMD ["fastapi", "run", "app/main.py", "--port", "80"]

# Forma shell (evitar)
CMD fastapi run app/main.py --port 80

Parece só estética, né? Não é haha

Checagem: troque o CMD para a forma com colchetes, faça o rebuild da imagem e crie um container novo a partir dela. Lembra que a imagem é estática: o container antigo continua com o comando antigo

Como prevenir: antes de mexer em qualquer outra coisa, revise o CMD. É a primeira linha que eu olharia num container que se comporta estranho ao parar

Container não volta depois que a VPS reinicia: política de reinício

O padrão do Docker é não reiniciar, então você precisa definir a política com --restart

O sintoma: o servidor reinicia e a API fica fora do ar até alguém entrar e subir na mão

A causa está na documentação de início automático do Docker: a política é definida com a flag --restart no docker run, e o valor padrão é no

As opções são:

  • no: padrão, não reinicia
  • always: sempre reinicia se o container parar
  • unless-stopped: igual ao always, mas se você parou o container manualmente, ele não volta nem quando o daemon reinicia
  • on-failure[:max-retries]: reinicia só quando o processo sai com código diferente de zero, com limite opcional de tentativas

O exemplo oficial com unless-stopped segue este formato:

docker run -d --name <nome> --restart unless-stopped <imagem>

Qual a diferença entre always e unless-stopped?

A diferença aparece depois de um docker stop manual

Com always, a política fica ignorada até o daemon Docker reiniciar ou até você reiniciar o container manualmente. Ou seja, num reboot do servidor ele volta

Com unless-stopped, o container continua parado mesmo depois que o daemon reinicia

Erro comum: dar docker stop pra testar e esperar que o unless-stopped traga o container de volta. Ele não vai trazer, e isso é o comportamento esperado, não um bug!

Por que o container em loop de falha demora pra voltar?

Porque o Docker aplica um atraso crescente entre as tentativas, pra não sobrecarregar o servidor

Segundo a referência do docker run, o atraso começa em 100 ms e dobra a cada tentativa, respeitando o limite de tentativas configurado para on-failure e as ações manuais de parada

Se o container consegue rodar por pelo menos 10 segundos, o atraso volta pra 100 ms

Então se a API quebra logo depois de subir, cada nova tentativa demora mais… e parece que o Docker desistiu. Não desistiu, só está esperando

Nesse caso, a política de reinício não resolve nada: o problema é o processo principal terminando, volta lá no CMD e nos logs da aplicação

O Docker inicia junto com o sistema?

No Debian e no Ubuntu, sim, o serviço Docker já inicia no boot por padrão

Em outras distribuições Linux com systemd, a documentação de pós-instalação do Docker indica habilitar os serviços:

sudo systemctl enable docker.service
sudo systemctl enable containerd.service

Sem o daemon subindo no boot, nenhuma política de reinício funciona, porque não tem quem reinicie o container

Como prevenir: defina a política já no primeiro docker run, e não depois que a API caiu em produção

Que servidor eu preciso pra controlar isso?

Um servidor em que você tenha acesso root, porque mexer em systemctl e no daemon do Docker exige isso

A recomendação da Hora de Codar pra esse cenário é a VPS da Hostinger (este artigo contém links de afiliado da Hostinger). Os planos de VPS KVM da Hostinger incluem acesso root completo, SSD NVMe, processadores AMD EPYC e backups semanais gratuitos que podem ser restaurados pelo painel, com opção de servidor no Brasil

Mas se liga: container que não volta é erro de configuração

Trocar de servidor não corrige CMD na forma errada nem política de reinício faltando. O servidor só te dá o controle pra aplicar a correção

Como conferir os headers quando a API está atrás de um proxy

A seção oficial sobre proxy de terminação TLS orienta adicionar --proxy-headers nesse cenário. Confira também quais endereços podem enviar esses headers, em vez de tratar qualquer remetente como confiável

O sintoma: a API está atrás de Nginx ou Traefik com HTTPS, mas enxerga a requisição como HTTP, ou não recebe o host e o IP originais do cliente

A causa, segundo a documentação do FastAPI sobre rodar atrás de proxy, é que por segurança o servidor iniciado pelo comando fastapi run não interpreta esses headers, porque não sabe que está atrás de um proxy confiável

E que headers são esses? Eles preservam dados da requisição original:

  • X-Forwarded-For: o IP do cliente
  • X-Forwarded-Proto: o protocolo, por exemplo https
  • X-Forwarded-Host: o host original

A mesma documentação trata a flag --forwarded-allow-ips como a opção que define quais IPs são considerados confiáveis para que esses headers sejam lidos. Com --forwarded-allow-ips="*" o servidor confia em qualquer IP, cenário que a documentação associa a situações em que só o proxy confiável fala com o servidor

Separadamente, o Dockerfile oficial do FastAPI traz um comentário indicando adicionar --proxy-headers quando a aplicação roda atrás de um proxy como Nginx ou Traefik:

# If running behind a proxy like Nginx or Traefik add --proxy-headers
CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"]

Como prevenir: antes de copiar uma linha de CMD pronta da internet juntando as duas flags, confira na documentação "Behind a Proxy" qual IP (ou faixa) representa o seu proxy, em vez de liberar tudo com "*" por padrão sempre que for viável. E repare que o CMD continua em colchetes, não vale ganhar o proxy e perder o encerramento gracioso xD

Build lento a cada mudança: cache de camadas e pip –no-cache-dir

A ordem das instruções no Dockerfile é o que faz o Docker reaproveitar a instalação das dependências

O sintoma: você muda uma linha da API e o build baixa e instala todas as dependências de novo

A causa: copiar o código antes do requirements.txt invalida o cache do passo de instalação, já que qualquer mudança no código muda aquela camada e tudo que vem depois

A documentação do FastAPI orienta copiar primeiro só o requirements.txt, instalar as dependências e só depois copiar o código. Como o requirements muda pouco, o Docker reaproveita o cache desse passo

O trecho abaixo é um exemplo didático que mostra só a ordem (o Dockerfile completo, com imagem base e diretório de trabalho, está na documentação):

# 1. primeiro, só o arquivo de dependências
COPY ./requirements.txt ./requirements.txt

# 2. instalação: camada reaproveitada enquanto o requirements não muda
RUN pip install --no-cache-dir --upgrade -r requirements.txt

# 3. só depois, o código da aplicação
COPY ./app ./app

Erro comum: achar que --no-cache-dir desliga o cache do Docker

Não desliga! Segundo a documentação do FastAPI, essa opção é do pip: ela faz o pip não guardar localmente os pacotes baixados depois de instalados

Checagem: altere só um arquivo dentro de app, rode o build de novo e confirme que o passo do pip install não baixa e instala tudo outra vez

Um processo por container ou vários workers: o que muda no ciclo de vida?

Depende de onde o container roda, mas a regra do processo principal vale em qualquer caso

Em cluster, tipo Kubernetes, a documentação do FastAPI sobre workers recomenda não usar --workers e rodar um único processo Uvicorn por container, deixando a replicação pro nível do cluster

Fora desse cenário, quando você usa --workers, a própria documentação explica que o processo pai passa a funcionar como gerenciador de processos dos workers

E isso muda o que é o processo principal: o processo que o Docker observa deixa de ser o servidor atendendo requisições e passa a ser esse gerenciador. Então quem decide se o container continua rodando é ele: se o processo pai (gerenciador) termina, o container para, não importa quantos workers ele mantinha vivos

Ou seja, a regra do início deste artigo, o container vive enquanto o processo principal vive, não muda com workers. Só muda quem é esse processo principal

Quantos workers cabem no seu servidor é outra conversa, e fica pro guia sobre workers do FastAPI com pouca RAM

Conteúdo do canal Hora de Codar

No canal Hora de Codar tem um vídeo mostrando o plugin OMO (All My Open Agent) pro Open Code, com agentes especialistas pra planejamento e execução

Fica aqui pra quem quer conhecer o canal e usa o Open Code no dia a dia 🙂

Conteúdo complementar do Hora de Codar para conhecer o canal; não é uma demonstração específica desta notícia ou versão.

Próximo passo: deixar a API resiliente na VPS

Como sugestão de validação em um ambiente de teste ou numa janela de manutenção, confira o comportamento após reiniciar o servidor:

  1. Rode docker ps -a e anote o estado do container da API (running, exited ou created)
  2. Abra o Dockerfile e confirme que o CMD está na forma exec, com colchetes. Se não estiver, corrija e faça o rebuild da imagem
  3. Antes de substituir um container, preserve os dados e volumes necessários. Em um ambiente de teste preparado para a substituição, crie o novo container com a política: docker run -d --name <nome> --restart unless-stopped <imagem>. Erro comum deste passo: esquecer que um container parado com docker stop não volta sozinho com unless-stopped
  4. Reinicie a VPS e, quando ela voltar, rode docker ps de novo. A API precisa aparecer em execução sem você subir nada na mão

Se ainda não tem um servidor com acesso root pra fazer esse controle, dá pra conferir a VPS para este projeto e já começar com a política de reinício certa desde o primeiro container

Até o próximo post! 😀

Quer aprender mais sobre inteligência artificial e programação na prática? Conheça o canal Hora de Codar no YouTube e acompanhe os próximos vídeos.

Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Formações

Formação Claude Code

Formação Claude Code

Domine Claude Code do absoluto zero até o avançado

  • 120 aulas
  • 4 projetos
  • 9h 48min

Blog | Mais populares