Ir para o conteúdo

FastAPI em contêineres - Docker

Ao fazer o deploy de aplicações FastAPI uma abordagem comum é construir uma imagem de contêiner Linux. Isso normalmente é feito usando o Docker. Você pode a partir disso fazer o deploy dessa imagem de algumas maneiras.

Usando contêineres Linux você tem diversas vantagens incluindo segurança, replicabilidade, simplicidade, entre outras.

Dica

Está com pressa e já sabe dessas coisas? Pode ir direto para Dockerfile abaixo 👇.

Visualização do Dockerfile 👀
FROM python:3.9

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

COPY ./app /code/app

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]

# If running behind a proxy like Nginx or Traefik add --proxy-headers
# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"]

O que é um Contêiner

Contêineres (especificamente contêineres Linux) são um jeito muito leve de empacotar aplicações contendo todas as dependências e arquivos necessários enquanto os mantém isolados de outros contêineres (outras aplicações ou componentes) no mesmo sistema.

Contêineres Linux rodam usando o mesmo kernel Linux do hospedeiro (máquina, máquina virtual, servidor na nuvem, etc). Isso simplesmente significa que eles são muito leves (comparados com máquinas virtuais emulando um sistema operacional completo).

Dessa forma, contêineres consomem poucos recursos, uma quantidade comparável com rodar os processos diretamente (uma máquina virtual consumiria muito mais).

Contêineres também possuem seus próprios processos (comumente um único processo), sistema de arquivos e rede isolados simplificando deploy, segurança, desenvolvimento, etc.

O que é uma Imagem de Contêiner

Um contêiner roda a partir de uma imagem de contêiner.

Uma imagem de contêiner é uma versão estática de todos os arquivos, variáveis de ambiente e do comando/programa padrão que deve estar presente num contêiner. Estática aqui significa que a imagem de contêiner não está rodando, não está sendo executada, somente contém os arquivos e metadados empacotados.

Em contraste com a "imagem de contêiner" que contém os conteúdos estáticos armazenados, um "contêiner" normalmente se refere à instância rodando, a coisa que está sendo executada.

Quando o contêiner é iniciado e está rodando (iniciado a partir de uma imagem de contêiner), ele pode criar ou modificar arquivos, variáveis de ambiente, etc. Essas mudanças vão existir somente nesse contêiner, mas não persistirão na imagem subjacente do container (não serão salvas no disco).

Uma imagem de contêiner é comparável ao arquivo de programa e seus conteúdos, ex.: python e algum arquivo main.py.

E o contêiner em si (em contraste à imagem de contêiner) é a própria instância da imagem rodando, comparável a um processo. Na verdade, um contêiner está rodando somente quando há um processo rodando (e normalmente é somente um processo). O contêiner finaliza quando não há um processo rodando nele.

Imagens de contêiner

Docker tem sido uma das principais ferramentas para criar e gerenciar imagens de contêiner e contêineres.

E existe um Docker Hub público com imagens de contêiner oficiais pré-prontas para diversas ferramentas, ambientes, bancos de dados e aplicações.

Por exemplo, há uma Imagem Python oficial.

E existe muitas outras imagens para diferentes coisas, como bancos de dados, por exemplo:

Usando imagens de contêiner pré-prontas é muito fácil combinar e usar diferentes ferramentas. Por exemplo, para testar um novo banco de dados. Em muitos casos, você pode usar as imagens oficiais precisando somente de variáveis de ambiente para configurá-las.

Dessa forma, em muitos casos você pode aprender sobre contêineres e Docker e re-usar essa experiência com diversos componentes e ferramentas.

Então, você rodaria vários contêineres com coisas diferentes, como um banco de dados, uma aplicação Python, um servidor web com uma aplicação frontend React, e conectá-los juntos via sua rede interna.

Todos os sistemas de gerenciamento de contêineres (como Docker ou Kubernetes) possuem essas funcionalidades de rede integradas a eles.

Contêineres e Processos

Uma imagem de contêiner normalmente inclui em seus metadados o programa padrão ou comando que deve ser executado quando o contêiner é iniciado e os parâmetros a serem passados para esse programa. Muito similar ao que seria se estivesse na linha de comando.

Quando um contêiner é iniciado, ele irá rodar esse comando/programa (embora você possa sobrescrevê-lo e fazer com que ele rode um comando/programa diferente).

Um contêiner está rodando enquanto o processo principal (comando ou programa) estiver rodando.

Um contêiner normalmente tem um único processo, mas também é possível iniciar sub-processos a partir do processo principal, e dessa forma você terá vários processos no mesmo contêiner.

Mas não é possível ter um contêiner rodando sem pelo menos um processo rodando. Se o processo principal parar, o contêiner também para.

Construindo uma Imagem Docker para FastAPI

Okay, vamos construir algo agora! 🚀

Eu vou mostrar como construir uma imagem Docker para FastAPI do zero, baseado na imagem oficial do Python.

Isso é o que você quer fazer na maioria dos casos, por exemplo:

  • Usando Kubernetes ou ferramentas similares
  • Quando rodando em uma Raspberry Pi
  • Usando um serviço em nuvem que irá rodar uma imagem de contêiner para você, etc.

O Pacote Requirements

Você normalmente teria os requisitos do pacote para sua aplicação em algum arquivo.

Isso pode depender principalmente da ferramenta que você usa para instalar esses requisitos.

O caminho mais comum de fazer isso é ter um arquivo requirements.txt com os nomes dos pacotes e suas versões, um por linha.

Você, naturalmente, usaria as mesmas ideias que você leu em Sobre Versões do FastAPI para definir os intervalos de versões.

Por exemplo, seu requirements.txt poderia parecer com:

fastapi>=0.68.0,<0.69.0
pydantic>=1.8.0,<2.0.0
uvicorn>=0.15.0,<0.16.0

E você normalmente instalaria essas dependências de pacote com pip, por exemplo:

$ pip install -r requirements.txt
---> 100%
Successfully installed fastapi pydantic uvicorn

Info

Há outros formatos e ferramentas para definir e instalar dependências de pacote.

Eu vou mostrar um exemplo depois usando Poetry em uma seção abaixo. 👇

Criando o Código do FastAPI

  • Crie um diretório app e entre nele.
  • Crie um arquivo vazio __init__.py.
  • Crie um arquivo main.py com:
from typing import Optional

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

Dockerfile

Agora, no mesmo diretório do projeto, crie um arquivo Dockerfile com:

# (1)
FROM python:3.9

# (2)
WORKDIR /code

# (3)
COPY ./requirements.txt /code/requirements.txt

# (4)
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# (5)
COPY ./app /code/app

# (6)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
  1. Inicie a partir da imagem base oficial do Python.

  2. Defina o diretório de trabalho atual para /code.

    Esse é o diretório onde colocaremos o arquivo requirements.txt e o diretório app.

  3. Copie o arquivo com os requisitos para o diretório /code.

    Copie somente o arquivo com os requisitos primeiro, não o resto do código.

    Como esse arquivo não muda com frequência, o Docker irá detectá-lo e usar o cache para esse passo, habilitando o cache para o próximo passo também.

  4. Instale as dependências de pacote vindas do arquivo de requisitos.

    A opção --no-cache-dir diz ao pip para não salvar os pacotes baixados localmente, pois isso só aconteceria se pip fosse executado novamente para instalar os mesmos pacotes, mas esse não é o caso quando trabalhamos com contêineres.

    Note

    --no-cache-dir é apenas relacionado ao pip, não tem nada a ver com Docker ou contêineres.

    A opção --upgrade diz ao pip para atualizar os pacotes se eles já estiverem instalados.

    Por causa do passo anterior de copiar o arquivo, ele pode ser detectado pelo cache do Docker, esse passo também usará o cache do Docker quando disponível.

    Usando o cache nesse passo irá salvar muito tempo quando você for construir a imagem repetidas vezes durante o desenvolvimento, ao invés de baixar e instalar todas as dependências toda vez.

  5. Copie o diretório ./app dentro do diretório /code.

    Como isso tem todo o código contendo o que muda com mais frequência, o cache do Docker não será usado para esse passo ou para qualquer passo seguinte facilmente.

    Então, é importante colocar isso perto do final do Dockerfile, para otimizar o tempo de construção da imagem do contêiner.

  6. Defina o comando para rodar o servidor uvicorn.

    CMD recebe uma lista de strings, cada uma dessas strings é o que você digitaria na linha de comando separado por espaços.

    Esse comando será executado a partir do diretório de trabalho atual, o mesmo diretório /code que você definiu acima com WORKDIR /code.

    Porque o programa será iniciado em /code e dentro dele está o diretório ./app com seu código, o Uvicorn será capaz de ver e importar app de app.main.

Tip

Revise o que cada linha faz clicando em cada bolha com o número no código. 👆

Agora você deve ter uma estrutura de diretório como:

.
├── app
│   ├── __init__.py
│   └── main.py
├── Dockerfile
└── requirements.txt

Por Trás de um Proxy de Terminação TLS

Se você está executando seu contêiner atrás de um Proxy de Terminação TLS (load balancer) como Nginx ou Traefik, adicione a opção --proxy-headers, isso fará com que o Uvicorn confie nos cabeçalhos enviados por esse proxy, informando que o aplicativo está sendo executado atrás do HTTPS, etc.

CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]

Cache Docker

Existe um truque importante nesse Dockerfile, primeiro copiamos o arquivo com as dependências sozinho, não o resto do código. Deixe-me te contar o porquê disso.

COPY ./requirements.txt /code/requirements.txt

Docker e outras ferramentas constróem essas imagens de contêiner incrementalmente, adicionando uma camada em cima da outra, começando do topo do Dockerfile e adicionando qualquer arquivo criado por cada uma das instruções do Dockerfile.

Docker e ferramentas similares também usam um cache interno ao construir a imagem, se um arquivo não mudou desde a última vez que a imagem do contêiner foi construída, então ele irá reutilizar a mesma camada criada na última vez, ao invés de copiar o arquivo novamente e criar uma nova camada do zero.

Somente evitar a cópia de arquivos não melhora muito as coisas, mas porque ele usou o cache para esse passo, ele pode usar o cache para o próximo passo. Por exemplo, ele pode usar o cache para a instrução que instala as dependências com:

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

O arquivo com os requisitos de pacote não muda com frequência. Então, ao copiar apenas esse arquivo, o Docker será capaz de usar o cache para esse passo.

E então, o Docker será capaz de usar o cache para o próximo passo que baixa e instala essas dependências. E é aqui que salvamos muito tempo. ✨ ...e evitamos tédio esperando. 😪😆

Baixar e instalar as dependências do pacote pode levar minutos, mas usando o cache leva segundos no máximo.

E como você estaria construindo a imagem do contêiner novamente e novamente durante o desenvolvimento para verificar se suas alterações de código estão funcionando, há muito tempo acumulado que isso economizaria.

A partir daí, perto do final do Dockerfile, copiamos todo o código. Como isso é o que muda com mais frequência, colocamos perto do final, porque quase sempre, qualquer coisa depois desse passo não será capaz de usar o cache.

COPY ./app /code/app

Construindo a Imagem Docker

Agora que todos os arquivos estão no lugar, vamos construir a imagem do contêiner.

  • Vá para o diretório do projeto (onde está o seu Dockerfile, contendo o diretório app).
  • Construa sua imagem FastAPI:
$ docker build -t myimage .

---> 100%

Tip

Note o . no final, é equivalente a ./, ele diz ao Docker o diretório a ser usado para construir a imagem do contêiner.

Nesse caso, é o mesmo diretório atual (.).

Inicie o contêiner Docker

  • Execute um contêiner baseado na sua imagem:
$ docker run -d --name mycontêiner -p 80:80 myimage

Verifique

Você deve ser capaz de verificar isso no URL do seu contêiner Docker, por exemplo: http://192.168.99.100/items/5?q=somequery ou http://127.0.0.1/items/5?q=somequery (ou equivalente, usando seu host Docker).

Você verá algo como:

{"item_id": 5, "q": "somequery"}

Documentação interativa da API

Agora você pode ir para http://192.168.99.100/docs ou http://127.0.0.1/docs (ou equivalente, usando seu host Docker).

Você verá a documentação interativa automática da API (fornecida pelo Swagger UI):

Swagger UI

Documentação alternativa da API

E você também pode ir para http://192.168.99.100/redoc ou http://127.0.0.1/redoc (ou equivalente, usando seu host Docker).

Você verá a documentação alternativa automática (fornecida pela ReDoc):

ReDoc

Construindo uma Imagem Docker com um Arquivo Único FastAPI

Se seu FastAPI for um único arquivo, por exemplo, main.py sem um diretório ./app, sua estrutura de arquivos poderia ser assim:

.
├── Dockerfile
├── main.py
└── requirements.txt

Então você só teria que alterar os caminhos correspondentes para copiar o arquivo dentro do Dockerfile:

FROM python:3.9

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# (1)
COPY ./main.py /code/

# (2)
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
  1. Copie o arquivo main.py para o diretório /code diretamente (sem nenhum diretório ./app).

  2. Execute o Uvicorn e diga a ele para importar o objeto app de main (em vez de importar de app.main).

Então ajuste o comando Uvicorn para usar o novo módulo main em vez de app.main para importar o objeto FastAPI app.

Conceitos de Implantação

Vamos falar novamente sobre alguns dos mesmos Conceitos de Implantação em termos de contêineres.

Contêineres são principalmente uma ferramenta para simplificar o processo de construção e implantação de um aplicativo, mas eles não impõem uma abordagem particular para lidar com esses conceitos de implantação e existem várias estratégias possíveis.

A boa notícia é que com cada estratégia diferente há uma maneira de cobrir todos os conceitos de implantação. 🎉

Vamos revisar esses conceitos de implantação em termos de contêineres:

  • HTTPS
  • Executando na inicialização
  • Reinicializações
  • Replicação (número de processos rodando)
  • Memória
  • Passos anteriores antes de começar

HTTPS

Se nos concentrarmos apenas na imagem do contêiner para um aplicativo FastAPI (e posteriormente no contêiner em execução), o HTTPS normalmente seria tratado externamente por outra ferramenta.

Isso poderia ser outro contêiner, por exemplo, com Traefik, lidando com HTTPS e aquisição automática de certificados.

Tip

Traefik tem integrações com Docker, Kubernetes e outros, portanto, é muito fácil configurar e configurar o HTTPS para seus contêineres com ele.

Alternativamente, o HTTPS poderia ser tratado por um provedor de nuvem como um de seus serviços (enquanto ainda executasse o aplicativo em um contêiner).

Executando na inicialização e reinicializações

Normalmente, outra ferramenta é responsável por iniciar e executar seu contêiner.

Ela poderia ser o Docker diretamente, Docker Compose, Kubernetes, um serviço de nuvem, etc.

Na maioria (ou em todos) os casos, há uma opção simples para habilitar a execução do contêiner na inicialização e habilitar reinicializações em falhas. Por exemplo, no Docker, é a opção de linha de comando --restart.

Sem usar contêineres, fazer aplicativos executarem na inicialização e com reinicializações pode ser trabalhoso e difícil. Mas quando trabalhando com contêineres em muitos casos essa funcionalidade é incluída por padrão. ✨

Replicação - Número de Processos

Se você tiver um cluster de máquinas com Kubernetes, Docker Swarm Mode, Nomad ou outro sistema complexo semelhante para gerenciar contêineres distribuídos em várias máquinas, então provavelmente desejará lidar com a replicação no nível do cluster em vez de usar um gerenciador de processos (como o Gunicorn com workers) em cada contêiner.

Um desses sistemas de gerenciamento de contêineres distribuídos como o Kubernetes normalmente tem alguma maneira integrada de lidar com a replicação de contêineres enquanto ainda oferece balanceamento de carga para as solicitações recebidas. Tudo no nível do cluster.

Nesses casos, você provavelmente desejará criar uma imagem do contêiner do zero como explicado acima, instalando suas dependências e executando um único processo Uvicorn em vez de executar algo como Gunicorn com trabalhadores Uvicorn.

Balanceamento de Carga

Quando usando contêineres, normalmente você terá algum componente escutando na porta principal. Poderia ser outro contêiner que também é um Proxy de Terminação TLS para lidar com HTTPS ou alguma ferramenta semelhante.

Como esse componente assumiria a carga de solicitações e distribuiria isso entre os trabalhadores de uma maneira (esperançosamente) balanceada, ele também é comumente chamado de Balanceador de Carga.

Tip

O mesmo componente Proxy de Terminação TLS usado para HTTPS provavelmente também seria um Balanceador de Carga.

E quando trabalhar com contêineres, o mesmo sistema que você usa para iniciar e gerenciá-los já terá ferramentas internas para transmitir a comunicação de rede (por exemplo, solicitações HTTP) do balanceador de carga (que também pode ser um Proxy de Terminação TLS) para o(s) contêiner(es) com seu aplicativo.

Um Balanceador de Carga - Múltiplos Contêineres de Workers

Quando trabalhando com Kubernetes ou sistemas similares de gerenciamento de contêiner distribuído, usando seus mecanismos de rede internos permitiria que o único balanceador de carga que estivesse escutando na porta principal transmitisse comunicação (solicitações) para possivelmente múltiplos contêineres executando seu aplicativo.

Cada um desses contêineres executando seu aplicativo normalmente teria apenas um processo (ex.: um processo Uvicorn executando seu aplicativo FastAPI). Todos seriam contêineres idênticos, executando a mesma coisa, mas cada um com seu próprio processo, memória, etc. Dessa forma, você aproveitaria a paralelização em núcleos diferentes da CPU, ou até mesmo em máquinas diferentes.

E o sistema de contêiner com o balanceador de carga iria distribuir as solicitações para cada um dos contêineres com seu aplicativo em turnos. Portanto, cada solicitação poderia ser tratada por um dos múltiplos contêineres replicados executando seu aplicativo.

E normalmente esse balanceador de carga seria capaz de lidar com solicitações que vão para outros aplicativos em seu cluster (por exemplo, para um domínio diferente, ou sob um prefixo de URL diferente), e transmitiria essa comunicação para os contêineres certos para esse outro aplicativo em execução em seu cluster.

Um Processo por Contêiner

Nesse tipo de cenário, provavelmente você desejará ter um único processo (Uvicorn) por contêiner, pois já estaria lidando com a replicação no nível do cluster.

Então, nesse caso, você não desejará ter um gerenciador de processos como o Gunicorn com trabalhadores Uvicorn, ou o Uvicorn usando seus próprios trabalhadores Uvicorn. Você desejará ter apenas um único processo Uvicorn por contêiner (mas provavelmente vários contêineres).

Tendo outro gerenciador de processos dentro do contêiner (como seria com o Gunicorn ou o Uvicorn gerenciando trabalhadores Uvicorn) só adicionaria complexidade desnecessária que você provavelmente já está cuidando com seu sistema de cluster.

Contêineres com Múltiplos Processos e Casos Especiais

Claro, existem casos especiais em que você pode querer ter um contêiner com um gerenciador de processos Gunicorn iniciando vários processos trabalhadores Uvicorn dentro.

Nesses casos, você pode usar a imagem oficial do Docker que inclui o Gunicorn como um gerenciador de processos executando vários processos trabalhadores Uvicorn, e algumas configurações padrão para ajustar o número de trabalhadores com base nos atuais núcleos da CPU automaticamente. Eu vou te contar mais sobre isso abaixo em Imagem Oficial do Docker com Gunicorn - Uvicorn.

Aqui estão alguns exemplos de quando isso pode fazer sentido:

Um Aplicativo Simples

Você pode querer um gerenciador de processos no contêiner se seu aplicativo for simples o suficiente para que você não precise (pelo menos não agora) ajustar muito o número de processos, e você pode simplesmente usar um padrão automatizado (com a imagem oficial do Docker), e você está executando em um único servidor, não em um cluster.

Docker Compose

Você pode estar implantando em um único servidor (não em um cluster) com o Docker Compose, então você não teria uma maneira fácil de gerenciar a replicação de contêineres (com o Docker Compose) enquanto preserva a rede compartilhada e o balanceamento de carga.

Então você pode querer ter um único contêiner com um gerenciador de processos iniciando vários processos trabalhadores dentro.

Prometheus and Outros Motivos

Você também pode ter outros motivos que tornariam mais fácil ter um único contêiner com múltiplos processos em vez de ter múltiplos contêineres com um único processo em cada um deles.

Por exemplo (dependendo de sua configuração), você poderia ter alguma ferramenta como um exportador do Prometheus no mesmo contêiner que deve ter acesso a cada uma das solicitações que chegam.

Nesse caso, se você tivesse múltiplos contêineres, por padrão, quando o Prometheus fosse ler as métricas, ele receberia as métricas de um único contêiner cada vez (para o contêiner que tratou essa solicitação específica), em vez de receber as métricas acumuladas de todos os contêineres replicados.

Então, nesse caso, poderia ser mais simples ter um único contêiner com múltiplos processos, e uma ferramenta local (por exemplo, um exportador do Prometheus) no mesmo contêiner coletando métricas do Prometheus para todos os processos internos e expor essas métricas no único contêiner.


O ponto principal é que nenhum desses são regras escritas em pedra que você deve seguir cegamente. Você pode usar essas idéias para avaliar seu próprio caso de uso e decidir qual é a melhor abordagem para seu sistema, verificando como gerenciar os conceitos de:

  • Segurança - HTTPS
  • Executando na inicialização
  • Reinicializações
  • Replicação (o número de processos em execução)
  • Memória
  • Passos anteriores antes de inicializar

Memória

Se você executar um único processo por contêiner, terá uma quantidade mais ou menos bem definida, estável e limitada de memória consumida por cada um desses contêineres (mais de um se eles forem replicados).

E então você pode definir esses mesmos limites e requisitos de memória em suas configurações para seu sistema de gerenciamento de contêineres (por exemplo, no Kubernetes). Dessa forma, ele poderá replicar os contêineres nas máquinas disponíveis levando em consideração a quantidade de memória necessária por eles e a quantidade disponível nas máquinas no cluster.

Se sua aplicação for simples, isso provavelmente não será um problema, e você pode não precisar especificar limites de memória rígidos. Mas se você estiver usando muita memória (por exemplo, com modelos de aprendizado de máquina), deve verificar quanta memória está consumindo e ajustar o número de contêineres que executa em cada máquina (e talvez adicionar mais máquinas ao seu cluster).

Se você executar múltiplos processos por contêiner (por exemplo, com a imagem oficial do Docker), deve garantir que o número de processos iniciados não consuma mais memória do que o disponível.

Passos anteriores antes de inicializar e contêineres

Se você estiver usando contêineres (por exemplo, Docker, Kubernetes), existem duas abordagens principais que você pode usar.

Contêineres Múltiplos

Se você tiver múltiplos contêineres, provavelmente cada um executando um único processo (por exemplo, em um cluster do Kubernetes), então provavelmente você gostaria de ter um contêiner separado fazendo o trabalho dos passos anteriores em um único contêiner, executando um único processo, antes de executar os contêineres trabalhadores replicados.

Info

Se você estiver usando o Kubernetes, provavelmente será um Init Container.

Se no seu caso de uso não houver problema em executar esses passos anteriores em paralelo várias vezes (por exemplo, se você não estiver executando migrações de banco de dados, mas apenas verificando se o banco de dados está pronto), então você também pode colocá-los em cada contêiner logo antes de iniciar o processo principal.

Contêiner Único

Se você tiver uma configuração simples, com um único contêiner que então inicia vários processos trabalhadores (ou também apenas um processo), então poderia executar esses passos anteriores no mesmo contêiner, logo antes de iniciar o processo com o aplicativo. A imagem oficial do Docker suporta isso internamente.

Imagem Oficial do Docker com Gunicorn - Uvicorn

Há uma imagem oficial do Docker que inclui o Gunicorn executando com trabalhadores Uvicorn, conforme detalhado em um capítulo anterior: Server Workers - Gunicorn com Uvicorn.

Essa imagem seria útil principalmente nas situações descritas acima em: Contêineres com Múltiplos Processos e Casos Especiais.

Warning

Existe uma grande chance de que você não precise dessa imagem base ou de qualquer outra semelhante, e seria melhor construir a imagem do zero, como descrito acima em: Construa uma Imagem Docker para o FastAPI.

Essa imagem tem um mecanismo de auto-ajuste incluído para definir o número de processos trabalhadores com base nos núcleos de CPU disponíveis.

Isso tem padrões sensíveis, mas você ainda pode alterar e atualizar todas as configurações com variáveis de ambiente ou arquivos de configuração.

Há também suporte para executar passos anteriores antes de iniciar com um script.

Tip

Para ver todas as configurações e opções, vá para a página da imagem Docker: tiangolo/uvicorn-gunicorn-fastapi.

Número de Processos na Imagem Oficial do Docker

O número de processos nesta imagem é calculado automaticamente a partir dos núcleos de CPU disponíveis.

Isso significa que ele tentará aproveitar o máximo de desempenho da CPU possível.

Você também pode ajustá-lo com as configurações usando variáveis de ambiente, etc.

Mas isso também significa que, como o número de processos depende da CPU do contêiner em execução, a quantidade de memória consumida também dependerá disso.

Então, se seu aplicativo consumir muito memória (por exemplo, com modelos de aprendizado de máquina), e seu servidor tiver muitos núcleos de CPU mas pouca memória, então seu contêiner pode acabar tentando usar mais memória do que está disponível e degradar o desempenho muito (ou até mesmo travar). 🚨

Criando um Dockerfile

Aqui está como você criaria um Dockerfile baseado nessa imagem:

FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9

COPY ./requirements.txt /app/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt

COPY ./app /app

Aplicações Maiores

Se você seguiu a seção sobre a criação de Aplicações Maiores com Múltiplos Arquivos, seu Dockerfile pode parecer com isso:

```Dockerfile hl_lines="7"
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9

COPY ./requirements.txt /app/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt

COPY ./app /app/app

Quando Usar

Você provavelmente não deve usar essa imagem base oficial (ou qualquer outra semelhante) se estiver usando Kubernetes (ou outros) e já estiver definindo replicação no nível do cluster, com vários contêineres. Nesses casos, é melhor construir uma imagem do zero conforme descrito acima: Construindo uma Imagem Docker para FastAPI.

Essa imagem seria útil principalmente nos casos especiais descritos acima em Contêineres com Múltiplos Processos e Casos Especiais. Por exemplo, se sua aplicação for simples o suficiente para que a configuração padrão de número de processos com base na CPU funcione bem, você não quer se preocupar com a configuração manual da replicação no nível do cluster e não está executando mais de um contêiner com seu aplicativo. Ou se você estiver implantando com Docker Compose, executando em um único servidor, etc.

Deploy da Imagem do Contêiner

Depois de ter uma imagem de contêiner (Docker), existem várias maneiras de implantá-la.

Por exemplo:

  • Com Docker Compose em um único servidor
  • Com um cluster Kubernetes
  • Com um cluster Docker Swarm Mode
  • Com outra ferramenta como o Nomad
  • Com um serviço de nuvem que pega sua imagem de contêiner e a implanta

Imagem Docker com Poetry

Se você usa Poetry para gerenciar as dependências do seu projeto, pode usar a construção multi-estágio do Docker:

# (1)
FROM python:3.9 as requirements-stage

# (2)
WORKDIR /tmp

# (3)
RUN pip install poetry

# (4)
COPY ./pyproject.toml ./poetry.lock* /tmp/

# (5)
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes

# (6)
FROM python:3.9

# (7)
WORKDIR /code

# (8)
COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt

# (9)
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# (10)
COPY ./app /code/app

# (11)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
  1. Esse é o primeiro estágio, ele é chamado requirements-stage.

  2. Defina /tmp como o diretório de trabalho atual.

    Aqui é onde geraremos o arquivo requirements.txt

  3. Instale o Poetry nesse estágio do Docker.

  4. Copie os arquivos pyproject.toml e poetry.lock para o diretório /tmp.

    Porque está usando ./poetry.lock* (terminando com um *), não irá falhar se esse arquivo ainda não estiver disponível.

  5. Gere o arquivo requirements.txt.

  6. Este é o estágio final, tudo aqui será preservado na imagem final do contêiner.

  7. Defina o diretório de trabalho atual como /code.

  8. Copie o arquivo requirements.txt para o diretório /code.

    Essse arquivo só existe no estágio anterior do Docker, é por isso que usamos --from-requirements-stage para copiá-lo.

  9. Instale as dependências de pacote do arquivo requirements.txt gerado.

  10. Copie o diretório app para o diretório /code.

  11. Execute o comando uvicorn, informando-o para usar o objeto app importado de app.main.

Tip

Clique nos números das bolhas para ver o que cada linha faz.

Um estágio do Docker é uma parte de um Dockerfile que funciona como uma imagem temporária do contêiner que só é usada para gerar alguns arquivos para serem usados posteriormente.

O primeiro estágio será usado apenas para instalar Poetry e para gerar o requirements.txt com as dependências do seu projeto a partir do arquivo pyproject.toml do Poetry.

Esse arquivo requirements.txt será usado com pip mais tarde no próximo estágio.

Na imagem final do contêiner, somente o estágio final é preservado. Os estágios anteriores serão descartados.

Quando usar Poetry, faz sentido usar construções multi-estágio do Docker porque você realmente não precisa ter o Poetry e suas dependências instaladas na imagem final do contêiner, você apenas precisa ter o arquivo requirements.txt gerado para instalar as dependências do seu projeto.

Então, no próximo (e último) estágio, você construiria a imagem mais ou menos da mesma maneira descrita anteriormente.

Por trás de um proxy de terminação TLS - Poetry

Novamente, se você estiver executando seu contêiner atrás de um proxy de terminação TLS (balanceador de carga) como Nginx ou Traefik, adicione a opção --proxy-headers ao comando:

CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]

Recapitulando

Usando sistemas de contêiner (por exemplo, com Docker e Kubernetes), torna-se bastante simples lidar com todos os conceitos de implantação:

  • HTTPS
  • Executando na inicialização
  • Reinícios
  • Replicação (o número de processos rodando)
  • Memória
  • Passos anteriores antes de inicializar

Na maioria dos casos, você provavelmente não desejará usar nenhuma imagem base e, em vez disso, construir uma imagem de contêiner do zero baseada na imagem oficial do Docker Python.

Tendo cuidado com a ordem das instruções no Dockerfile e o cache do Docker, você pode minimizar os tempos de construção, para maximizar sua produtividade (e evitar a tédio). 😎

Em alguns casos especiais, você pode querer usar a imagem oficial do Docker para o FastAPI. 🤓