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

URL: https://www.back4app.com/blog/pt/como-fazer-o-deploy-de-fastapi-com-docker-e-banco-de-dados
Publicado: 2026-10-14 · Testado: 2026-09-16
Testado em: 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
Publisher: Back4app Engineering

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](https://www.back4app.com/blog/deploy-node-app-dockerfile-back4app), [Next.js com SSR](https://www.back4app.com/blog/deploy-nextjs-app-github-backend), [um worker sempre ligado](https://www.back4app.com/blog/run-a-bot-webhook-or-cron-24-7-without-a-vps).

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](https://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 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](/blog/blog-assets/deploy-python-fastapi-dockerfile-database/architecture-diagram.svg)

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

```dockerfile
# 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:

```python
# 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):

```python
# 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:

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:

```bash
# 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`:

```javascript
// 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](/blog/blog-assets/deploy-python-fastapi-dockerfile-database/02-deploy-form-v2.jpg)

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

![A Swagger UI do FastAPI em /docs na URL b4a.run listando as seis rotas e os schemas TaskIn, TaskPatch e de validação](/blog/blog-assets/deploy-python-fastapi-dockerfile-database/05-swagger-docs-v2.jpg)

## 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](/blog/blog-assets/deploy-python-fastapi-dockerfile-database/06-backend-overview-v2.jpg)

**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](/blog/blog-assets/deploy-python-fastapi-dockerfile-database/08-cloud-code-pending.jpg)

**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](/blog/blog-assets/deploy-python-fastapi-dockerfile-database/07-database-task.jpg)

## 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](https://www.back4app.com/blog/run-a-bot-webhook-or-cron-24-7-without-a-vps): 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](/blog/blog-assets/deploy-python-fastapi-dockerfile-database/04-shared-plan-ready.jpg)

## 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](https://www.back4app.com/blog/run-a-bot-webhook-or-cron-24-7-without-a-vps).
- **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](https://www.back4app.com/blog/add-signup-login-password-reset-without-an-auth-server)); 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](https://www.back4app.com/blog/lock-down-a-backend-your-frontend-talks-to)).
- **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](https://github.com/templates-back4app/fastapi-tasks) inclui o `deploy-check.sh`, que executa health, criar, ler, atualizar, validar e excluir contra qualquer URL de deploy que você passar.
