Como fazer o deploy de FastAPI com Docker e banco de dados, sem administrar um servidor

Uma API de tarefas em FastAPI com Dockerfile, no ar e salvando dados em 86 segundos, sem servidor nem banco para operar. Medido passo a passo, incluindo o bug de validação que o Pydantic deixou passar e o backend barrou.

Uma requisição, dois validadores: um POST /tasks com um título de três espaços passa pelo min_length=1 do Pydantic dentro do container FastAPI, é rejeitado pelo hook beforeSave do backend Back4app com o código 142 e volta ao cliente como 422

Tradução do original em inglês, testado em 16 de setembro de 2026. Código, números e datas são os do original. Read in English

Sua aplicação FastAPI roda em localhost:8000. Tem um Dockerfile. E no instante em que você procura um lugar para colocá-la, a pergunta muda de “onde meu código roda?” para “onde meus dados ficam, e quem está aplicando os patches naquele Postgres?”

Este post é sobre uma API em Python: o FastAPI validando o formato de cada requisição, um backend guardando as linhas, e a regra sobre o que é uma tarefa válida vivendo em exatamente um lugar. Uma plataforma de containers roda o uvicorn. Um backend entrega o banco de dados e uma API que você não escreveu. É no segundo que os servidores se escondem.

As outras três formas têm posts próprios: Node.js com Docker, Next.js com SSR, um worker sempre ligado.

Construímos uma API de tarefas — 80 linhas de FastAPI, um Dockerfile de sete linhas —, fizemos o push e a tivemos salvando dados atrás de HTTPS sem servidor para administrar. O primeiro deploy levou 86 segundos do clique ao DEPLOYMENT READY. Tudo abaixo vem dessa execução, em 15 e 16 de setembro de 2026, incluindo a parte que o Pydantic errou.

Pegue o código funcionando: o repositório companion está em github.com/templates-back4app/fastapi-tasks — main.py, Dockerfile, requirements.txt, cloud/main.js e deploy-check.sh, que executa criar, ler, atualizar, validar e excluir contra qualquer URL de deploy.

O que “sem administrar um servidor” inclui para uma API em Python?

Cinco coisas, e um Dockerfile só cobre a primeira. Uma plataforma só pode usar a expressão com honestidade se cobrir a lista inteira:

O que você precisaPor que não é opcionalOnde fica neste post
uvicorn rodando sua aplicaçãoA parte óbviaContainer, seção 03
HTTPS em uma URL públicaClientes e navegadores recusam HTTP puroContainer, automático
Um banco de dados que você não operaOs dados sobrevivem ao containerBackend, seção 04
Validação fora do containerO container é descartável; as regras, nãoBackend, seção 05
Redeploy a cada mudança, sem downtimeSenão você volta ao SSHSeção 06

A maioria dos hosts de containers para nos dois primeiros: rodam o Dockerfile e entregam uma URL. O banco de dados é um serviço separado, cobrado à parte, com connection string, e o pool, as migrations e os backups são seus.

A Back4app foi a que testamos porque a segunda metade é um produto da mesma conta: um app de backend com banco de dados gerenciado e REST API, que um app de container chama por HTTPS. Sem driver, sem pool, sem connection string — dois headers.

O container é a única peça para a qual você constrói uma imagem. O banco de dados é uma URL e dois headers.

O que a API de tarefas em FastAPI contém?

Uma API de tarefas — a menor aplicação que exercita as duas metades. Seis rotas: POST /tasks, GET /tasks (com ?done=true), GET, PATCH e DELETE em /tasks/{id}, e GET /healthz. O FastAPI valida o formato, o backend guarda os dados e é dono das regras.

Arquitetura: clientes chamam o container FastAPI; o container transforma cada requisição em uma chamada REST ao backend Back4app, onde um hook de Cloud Code valida cada Task antes de ela chegar ao banco de dados gerenciado

O Dockerfile inteiro. EXPOSE 8080 é a linha que a plataforma lê para descobrir a porta:

# Stack: Docker | Python 3.12 (slim) | File: Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

O container lê a configuração das variáveis de ambiente e fala com o backend por um único cliente compartilhado:

# Stack: Python 3.12 | FastAPI 0.115 + httpx | File: main.py (setup)
APP_ID = os.environ["PARSE_APP_ID"]
REST_KEY = os.environ["PARSE_REST_KEY"]
BASE = os.environ.get("PARSE_SERVER_URL", "https://parseapi.back4app.com")
HEADERS = {"X-Parse-Application-Id": APP_ID, "X-Parse-REST-API-Key": REST_KEY, "Content-Type": "application/json"}

app = FastAPI(title="tasks")
backend = httpx.AsyncClient(base_url=BASE, headers=HEADERS, timeout=10)

class TaskIn(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    done: bool = False

Uma rota é um modelo Pydantic na entrada, uma chamada REST e um status code que o cliente entende. O backend responde com códigos de erro do Parse; parse_error faz o mapeamento (101 → 404, 142 → 422, 209 → 401, qualquer outro → 502):

# Stack: Python 3.12 | FastAPI 0.115 + httpx | File: main.py (create + read)
@app.post("/tasks", status_code=201)
async def create_task(task: TaskIn):
    r = await backend.post("/classes/Task", json=task.model_dump())
    if r.status_code != 201:
        parse_error(r)
    return {**task.model_dump(), **r.json()}

@app.get("/tasks/{task_id}")
async def get_task(task_id: str):
    r = await backend.get(f"/classes/Task/{task_id}")
    if r.status_code != 200:
        parse_error(r)
    return r.json()
Medido · fastapi-tasks · setembro de 2026
80linhas no container
→
9linhas de Cloud Code
0drivers de banco, pools ou migrations

Você precisa de uma conta Free na Back4app, uma conta no GitHub e curl. Nada de Docker na sua máquina: a plataforma constrói a imagem, e este post foi produzido em um Mac sem Docker instalado. Python só para rodar a aplicação localmente antes.

De onde vem o banco de dados?

De um app de backend que você cria na dashboard — New App → Build your Backend, dê um nome, Create. A página Overview mostra o App ID e, no dropdown Keys, a REST API key. Esses dois valores são toda a configuração de banco de dados do container.

Você não define a classe Task. O primeiro POST /classes/Task a criou, com title como String e done como Boolean, inferidos do JSON (Parse Server 7.5.2, setembro de 2026). Aqui está a mesma escrita de dois jeitos — a que o container faz, e a que você pode fazer do seu terminal com as mesmas chaves:

# Stack: Python 3.12 | httpx 0.28 | File: main.py (the write)
r = await backend.post("/classes/Task", json={"title": "Write the tutorial", "done": False})
r.status_code            # 201
r.json()                 # {"objectId": "OFTBmFAMs3", "createdAt": "2026-09-15T15:01:53.9Z"}

Mesma escrita, mesmo backend. O container não acrescenta nada que o curl não envie.

Filtrar é um parâmetro where com JSON dentro. GET /tasks?done=true no container vira GET /classes/Task?where={"done":true}&order=-createdAt no backend — sem query builder, sem ORM, e a mesma URL funciona no navegador com os dois headers:

# Stack: bash | Endpoint: GET /classes/Task with a where filter
curl -G "$BASE/classes/Task" \
  -H "X-Parse-Application-Id: $APP_ID" \
  -H "X-Parse-REST-API-Key: $REST_KEY" \
  --data-urlencode 'where={"done": true}' \
  --data-urlencode 'order=-createdAt'

Por que a regra de validação vive no backend, e não no Pydantic?

Porque é uma regra sobre os seus dados. O Pydantic confere o formato de uma requisição; ele não sabe o que é uma tarefa válida, e roda só neste container. Uma regra no backend vale para a aplicação Python, para um comando curl e para o próximo container que você escrever em outra linguagem.

cloud/main.js roda dentro do backend. Ele faz o trim do título, rejeita um título vazio e define o padrão de done:

// Stack: Node.js 22.x | Parse Server 8.x | File: cloud/main.js
Parse.Cloud.beforeSave("Task", (request) => {
  const t = request.object;
  const title = t.get("title");
  if (typeof title !== "string" || !title.trim())
    throw new Parse.Error(Parse.Error.VALIDATION_ERROR, "title is required.");
  t.set("title", title.trim());
  if (t.get("done") === undefined) t.set("done", false);
});

Cole em Cloud Code → cloud/main.js na dashboard do backend, Deploy, e prove com uma requisição. O nosso respondeu 400 {"code":142,"error":"title is required."} a um título de três espaços — depois do segundo deploy.

Como fazer o deploy de um Dockerfile FastAPI a partir do GitHub?

Faça o push do repositório para o GitHub e depois New App → Deploy from GitHub no lado de Containers. Na primeira vez, o GitHub pede para instalar o app Back4app Containers nos repositórios que você escolher; a lista na dashboard atualiza assim que você termina.

Configurar. Selecione o repositório. O formulário detectou o Dockerfile e ofereceu um Python buildpack como alternativa — ele leu a linguagem do repositório. Mantivemos o Dockerfile, deixamos a porta em auto e adicionamos PARSE_APP_ID e PARSE_REST_KEY.

Também mudamos o Health check de / para /healthz. No fim, não fez diferença para o deploy — veja a seção de erros — mas é a rota que o seu próprio monitoramento deve usar.

O formulário de deploy do fastapi-tasks: Dockerfile selecionado com a alternativa Python Buildpack, duas variáveis de ambiente com valores mascarados, health check /healthz

Deploy. Clicamos às 15:00:16. O botão mostrou “Deploying…” por 30 segundos antes de a página do app aparecer; depois PREPARING às 15:00:46, BUILDING IMAGE por 46 segundos (pip install em python:3.12-slim), LAUNCHING CONTAINER às 15:01:33 e DEPLOYMENT READY às 15:01:42.

Medido · primeiro deploy · 15 de setembro de 2026
86 sclique em Deploy → DEPLOYMENT READY
→
46 sdisso, pip install + build da imagem
17 sprimeiro 200 chegou antes da linha Ready

Como medimos: um cronômetro do clique em Deploy até a linha DEPLOYMENT READY da dashboard, plano Free, um deploy em 15 de setembro de 2026, com um loop consultando a URL pública a cada segundo, em paralelo, para capturar o primeiro 200. O pip install domina, então o seu tempo varia com o requirements.txt.

deploy-check.sh então fez a volta completa contra a URL ao vivo: health, criar (201, objectId de volta), ler, atualizar (PATCH {"done": true}), validar (título em branco → 422), excluir (204). Doze segundos depois do Ready, os seis passaram.

A página /docs do próprio FastAPI funciona sem alteração na URL, que é o jeito mais rápido de mostrar a API a alguém sem mandar um curl:

A Swagger UI do FastAPI em /docs na URL b4a.run listando as seis rotas e os schemas TaskIn, TaskPatch e de validação

Onde as tarefas vão parar? O backend por trás da API

Três telas na dashboard do backend, e cada linha nelas foi escrita pelo container acima — sem dados de seed, sem schema definido à mão.

Overview — de onde vêm os dois headers. Dashboard → New App → Build your Backend → Create. A página Overview mostra o App ID e, no dropdown Keys, a REST API key. Esses dois valores viram PARSE_APP_ID e PARSE_REST_KEY no formulário do container; não há connection string em lugar nenhum.

A tela Overview do backend fastapi-tasks: App ID, o dropdown Keys em Rest Key (valor borrado), Parse Server 7.5.2 e MongoDB 3.6 à direita

Cloud Code — onde a regra vive. cloud/main.js aberto no editor com o hook beforeSave("Task"), e o selo Files pending deploy (1) ao lado do botão Deploy. Em um backend novo, esse selo é o sinal: ele continua lá depois do primeiro Deploy, e é assim que você sabe que nada foi enviado.

O editor de Cloud Code com cloud/main.js aberto mostrando o hook beforeSave de Task, e o selo Files pending deploy (1) ao lado do botão Deploy

Database Browser — a classe Task. Seis linhas. As três mais novas vieram pelo POST /tasks do container enquanto escrevíamos esta seção; a de título Deploy the container foi enviada com espaços em volta e armazenada sem eles. As duas linhas sem título visível são três espaços cada — escritas em 15 de setembro, antes do deploy do hook, exatamente a brecha que o hook fecha.

O Database Browser mostrando a classe Task com seis objetos: colunas title e done inferidas pelo primeiro POST, três tarefas novas de 17 de setembro e duas linhas mais antigas com o título em branco

Quanto tempo a URL dura, e quanto custa torná-la permanente?

No plano Free, 60 minutos a partir do momento em que o deploy começa — depois o container é destruído e a dashboard mostra URL Expired. No plano Shared, US$ 5/mês por 0,5 vCPU e 512 MB, a URL é permanente, e a simples troca de plano refez o deploy do app no mesmo endereço (tudo em 16 de setembro de 2026).

A hora gratuita é real e é suficiente: cada passo deste post, incluindo os erros, cabe em uma janela. Não é suficiente para deixar a API rodando para um cliente, e a própria plataforma diz isso — o card do plano Free traz “Ideal for developing, learning, and prototyping”.

Medimos a expiração ao minuto no worker do nosso post sobre processos sempre ligados: deploy iniciado às 14:55:28, URL fora do ar às 15:55:27. Esta API mostrou o mesmo status URL Expired na manhã seguinte.

Na manhã seguinte trocamos o plano. Sem nenhum clique em Deploy, o log mostrou PREPARING DEPLOYMENT às 12:35:32 e DEPLOYMENT READY às 12:36:24 — 52 segundos — na mesma URL b4a.run, e o deploy-check.sh passou de novo.

A tela Overview do container no plano Shared: US$ 5,00 por mês, 0,5 vCPU e 512 MB, status Available, o redeploy automático Ready em 52 s, o log terminando no CMD do uvicorn e em Pushed image

Medido · plano Shared · 16 de setembro de 2026
52 stroca de plano → redeploy, mesma URL
→
US$ 5por mês, 0,5 vCPU · 512 MB
60 minvida útil por deploy no plano Free

O que 422, 404 e 502 significam nesta API, e como corrigir cada um?

Quebramos o deploy de propósito, e a plataforma o quebrou uma vez por nós. Cada entrada começa com o que você vai ver de fato.

KeyError: 'PARSE_APP_ID' no instante em que o uvicorn importa a aplicação. os.environ["PARSE_APP_ID"] lança a exceção na importação quando a variável não existe, então não há processo para o health check alcançar. Adicione as duas variáveis em Settings → Environment e faça o deploy de novo; uma variável salva vale no próximo deploy, não no que está rodando.

422 {"detail":{"code":142,"error":"title is required."}} em um título que o Pydantic aceitou. Funcionando como esperado: o hook do backend rejeitou. O formato desse 422 é diferente da lista de erros do Pydantic — se o seu cliente faz o parse de detail, trate tanto uma lista quanto um objeto.

404 {"detail":{"code":101,"error":"Object not found."}} em GET /tasks/{id}. O id não é o objectId de uma Task; o Parse responde 101 e o container mapeia para 404. Ids são strings de dez caracteres, sensíveis a maiúsculas e minúsculas, como OFTBmFAMs3.

502 {"detail":{"error":"{\"error\":\"unauthorized\"}"}} em todas as rotas, exceto /healthz. Um PARSE_REST_KEY errado. O backend responde unauthorized sem código do Parse, então parse_error cai no 502 com o corpo bruto como string. O health check passou porque /healthz não toca o backend — e a verificação da plataforma é um teste de porta, de qualquer forma. Faça o /healthz executar uma consulta barata para que o deploy-check.sh e o seu monitor peguem uma chave errada.

DEPLOYMENT READY em uma API cujas rotas falham todas. O passo de health da plataforma confere se a porta responde, não o status: no nosso worker, um caminho que devolvia 404 passou. Uma chave errada ou um handler que quebrou, portanto, não é pego na hora do deploy — rode o deploy-check.sh contra a URL depois de cada deploy.

404 not found na URL, dashboard diz URL Expired. A hora gratuita acabou e o container se foi. Faça o deploy de novo para ganhar mais uma hora, ou troque de plano — a troca de plano refaz o deploy sozinha.

Quando esta é a resposta errada?

Pelo nosso próprio manifesto, um post em que só a plataforma ganha não é publicado — então aqui vai a outra coluna.

  • Sua API é CRUD e nada mais. Então você não precisa do container: a camada REST do backend é a API, e um cliente Python pode chamá-la diretamente com os mesmos dois headers.
  • Você precisa de recursos do Postgres. Joins entre muitas tabelas, window functions, PostGIS, um schema que você migra com Alembic — o backend é Parse Server sobre MongoDB (Postgres nos planos pagos), e sua linguagem de consulta é JSON no where, não SQL.
  • Você precisa de um worker em segundo plano ao lado da API. Um container é um processo; um consumidor de fila pertence ao próprio container, como no nosso post sobre processos sempre ligados.
  • Você quer construir imagens localmente e enviá-las. A rota por imagem aceita apenas o Docker Hub.

Se nenhum desses casos descreve você, um container FastAPI sem estado na frente de um backend gerenciado é a forma que escolheríamos de novo — e a dupla Pydantic + hook é como validaríamos.

O que você adicionaria à API de tarefas em seguida?

Você tem uma API que refaz o deploy em menos de um minuto, um banco de dados que nunca provisionou e regras que sobrevivem à substituição do container. A partir daqui:

  • Dê donos às tarefas. A classe User do Parse adiciona endpoints de criação de conta e login sem código no container (medido, com redefinição de senha); um session token no header da requisição restringe cada consulta ao seu dono assim que a classe é fechada (a sondagem de doze requisições).
  • Ligue o autodeploy. Em um plano pago, Settings → Build & deploy → Autodeploy; medimos um push até o ar em 44 segundos no worker.
  • Torne o health check honesto. Faça o /healthz executar GET /classes/Task?limit=0 contra o backend, para que uma chave errada apareça no seu monitoramento, não no primeiro usuário.

Todo o código acima está completo e testado — o repositório companion inclui o deploy-check.sh, que executa health, criar, ler, atualizar, validar e excluir contra qualquer URL de deploy que você passar.

Tagspythonfastapidockercontainersrestparse-server

Perguntas frequentes

Dá para fazer o deploy de uma aplicação FastAPI com Docker sem administrar um servidor?

Sim. No Back4app Containers você conecta um repositório do GitHub que tem um Dockerfile; a plataforma constrói a imagem com o kaniko, roda o uvicorn, termina o HTTPS e entrega uma URL. Nosso primeiro deploy foi do clique em Deploy ao DEPLOYMENT READY em 86 segundos (15 de setembro de 2026), 46 deles no pip install.

De onde vem o banco de dados se o container não tem estado?

De um app de backend da Back4app: um Parse Server gerenciado com REST API. O container FastAPI o chama com httpx usando dois headers, o App ID e a REST key, e nunca abre uma conexão com banco de dados. A classe Task foi criada pelo primeiro POST; nenhum schema foi definido antes.

O FastAPI precisa de um Dockerfile nessa plataforma?

Não. O formulário de deploy detectou o repositório como Python e ofereceu um buildpack de Python como alternativa ao Dockerfile. Usamos o Dockerfile porque ele fixa o interpretador (python:3.12-slim) e o comando exato do uvicorn; o buildpack é o caminho quando você não quer ser dono disso.

Por que um título em branco passou pelo Pydantic mas não pelo backend?

Field(min_length=1) conta caracteres, e três espaços são três caracteres, então o Pydantic aceitou ' '. O hook beforeSave do backend faz o trim do título e rejeita um título vazio com o código de erro 142, que o container mapeia para HTTP 422. Mantenha a regra no backend para que um comando curl e a aplicação Python recebam a mesma resposta.

Compare com

Como fazer o deploy de um app Node.js com Docker e banco de dados, sem administrar um servidor →

Leitura complementar

Revisado e verificado por Back4app Engineering, Editorial review na Back4app

Testado em 16 de setembro de 2026 — Containers: Back4app, plano Shared, 0,5 vCPU, 512 MB (plano Free na primeira hora) · Backend: Back4app, USA East · Local: Python 3.12, FastAPI 0.115, httpx.

Mais da Back4app Engineering

Containers · tutorial

Como fazer o deploy de um app Node.js com Docker e banco de dados, sem administrar um servidor

Containers · tutorial

How to Run a Node.js Bot, Webhook or Cron Job 24/7 Without a VPS [Full Repo]

Backend · tutorial

Como adicionar autenticação a um app web ou Flutter sem escrever um servidor de auth