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ê precisa | Por que não é opcional | Onde fica neste post |
|---|---|---|
| uvicorn rodando sua aplicação | A parte óbvia | Container, seção 03 |
| HTTPS em uma URL pública | Clientes e navegadores recusam HTTP puro | Container, automático |
| Um banco de dados que você não opera | Os dados sobrevivem ao container | Backend, seção 04 |
| Validação fora do container | O container é descartável; as regras, não | Backend, seção 05 |
| Redeploy a cada mudança, sem downtime | Senão você volta ao SSH | Seçã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.
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()
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"}# Stack: bash | Endpoint: POST /classes/Task
curl -X POST \
-H "X-Parse-Application-Id: $APP_ID" \
-H "X-Parse-REST-API-Key: $REST_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Write the tutorial", "done": false}' \
"$BASE/classes/Task"
# {"objectId":"…","createdAt":"…"} ← HTTP 201Mesma 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.

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.
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:

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.

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.

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.

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.

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
Userdo 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
/healthzexecutarGET /classes/Task?limit=0contra 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.