Cómo desplegar FastAPI con Docker y una base de datos sin administrar un servidor

Una API de tareas en FastAPI con un Dockerfile, en producción y guardando datos en 86 segundos, sin servidor ni base de datos que administrar. Medido paso a paso, incluido el bug de validación que Pydantic dejó pasar y el backend atrapó.

Una solicitud, dos validadores: un POST /tasks con un título de tres espacios pasa el min_length=1 de Pydantic dentro del container de FastAPI, el hook beforeSave del backend de Back4app lo rechaza con el código 142 y vuelve al cliente como un 422

Traducción del original en inglés, probado el 16 de septiembre de 2026. El código, los números y las fechas son los del original. Read in English

Tu app FastAPI corre en localhost:8000. Tiene un Dockerfile. Y en cuanto buscas dónde ponerla, la pregunta cambia de “¿dónde corre mi código?” a “¿dónde viven mis datos y quién parchea ese Postgres?”.

Este post trata de una API en Python: FastAPI validando la forma de cada solicitud, un backend guardando las filas y la regla sobre qué es una tarea válida viviendo en un único sitio. Una plataforma de containers ejecuta uvicorn. Un backend te da la base de datos y una API que no escribiste. En el segundo es donde se esconden los servidores.

Las otras tres formas tienen sus propios posts: Node.js con Docker, Next.js con SSR, un worker siempre activo.

Construimos una API de tareas (80 líneas de FastAPI, un Dockerfile de siete líneas), la subimos y la tuvimos guardando datos detrás de HTTPS sin un servidor que administrar. El primer deploy tardó 86 segundos del clic a DEPLOYMENT READY. Todo lo que sigue sale de esa ejecución, el 15 y 16 de septiembre de 2026, incluida la parte en la que Pydantic se equivocó.

Consigue el código funcionando: el repositorio companion está en github.com/templates-back4app/fastapi-tasks — main.py, Dockerfile, requirements.txt, cloud/main.js y deploy-check.sh, que ejecuta create, read, update, validate y delete contra cualquier URL de deploy.

¿Qué incluye “sin administrar un servidor” para una API en Python?

Cinco cosas, y un Dockerfile solo cubre la primera. Una plataforma solo puede usar esa frase con honestidad si cubre la lista entera:

Lo que necesitasPor qué no es opcionalDónde vive en este post
uvicorn ejecutando tu appLa parte obviaContainer, sección 03
HTTPS en una URL públicaLos clientes y los navegadores rechazan HTTP planoContainer, automático
Una base de datos que no administrasLos datos sobreviven al containerBackend, sección 04
Validación fuera del containerEl container es desechable; las reglas, noBackend, sección 05
Redeploy en cada cambio, sin caídaSi no, vuelves a SSHSección 06

La mayoría de los hosts de containers se quedan en las dos primeras: ejecutan el Dockerfile y te dan una URL. La base de datos es un servicio aparte, facturado aparte, con una cadena de conexión, y el pool, las migraciones y las copias de seguridad son tuyos.

Back4app es el que probamos porque la segunda mitad es un producto de la misma cuenta: una app de backend con una base de datos gestionada y una REST API, a la que una app de container llama por HTTPS. Sin driver, sin pool, sin cadena de conexión: dos headers.

El container es la única pieza para la que construyes una imagen. La base de datos es una URL y dos headers.

¿Qué contiene la API de tareas en FastAPI?

Una API de tareas: la app más pequeña que ejercita las dos mitades. Seis rutas: POST /tasks, GET /tasks (con ?done=true), GET, PATCH y DELETE en /tasks/{id}, y GET /healthz. FastAPI valida la forma, el backend guarda los datos y es dueño de las reglas.

Arquitectura: los clientes llaman al container de FastAPI; el container convierte cada solicitud en una llamada REST al backend de Back4app, donde un hook de Cloud Code valida cada Task antes de que llegue a la base de datos gestionada

El Dockerfile completo. EXPOSE 8080 es la línea que la plataforma lee para el puerto:

# 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"]

El container lee su configuración del entorno y habla con el backend a través de un único cliente compartido:

# 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

Una ruta es un modelo de Pydantic de entrada, una llamada REST y un código de estado que el cliente entiende. El backend responde con códigos de error de Parse; parse_error los mapea (101 → 404, 142 → 422, 209 → 401, cualquier otro → 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 · septiembre de 2026
80líneas en el container
→
9líneas de Cloud Code
0drivers de base de datos, pools o migraciones

Necesitas una cuenta gratuita de Back4app, una cuenta de GitHub y curl. Docker en tu máquina, no: la plataforma construye la imagen, y este post se produjo en un Mac sin Docker instalado. Python solo para ejecutar la app en local primero.

¿De dónde sale la base de datos?

De una app de backend que creas en la dashboard: New App → Build your Backend, ponle nombre, Create. La página Overview muestra el App ID y, en el desplegable Keys, la REST API key. Esos dos valores son toda la configuración de base de datos del container.

La clase Task no la defines tú. La creó el primer POST /classes/Task, con title como String y done como Boolean, inferidos del JSON (Parse Server 7.5.2, septiembre de 2026). Aquí está la misma escritura de dos formas: la que hace el container y la que puedes hacer desde tu terminal con las mismas claves:

# 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"}

La misma escritura, el mismo backend. El container no añade nada que el curl no envíe.

Filtrar es un parámetro where con JSON dentro. GET /tasks?done=true en el container se convierte en GET /classes/Task?where={"done":true}&order=-createdAt en el backend: sin query builder, sin ORM, y la misma URL funciona en un navegador con los dos 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 qué la regla de validación vive en el backend y no en Pydantic?

Porque son las reglas sobre tus datos. Pydantic comprueba la forma de una solicitud; no sabe qué es una tarea válida, y solo se ejecuta en este container. Una regla en el backend se aplica a la app en Python, a un comando curl y al próximo container que escribas en otro lenguaje.

cloud/main.js se ejecuta dentro del backend. Recorta el título, rechaza uno vacío y pone done por defecto:

// 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);
});

Pégalo en Cloud Code → cloud/main.js en la dashboard del backend, Deploy, y demuéstralo con una solicitud. El nuestro respondió 400 {"code":142,"error":"title is required."} a un título de tres espacios, después del segundo deploy.

¿Cómo se despliega un Dockerfile de FastAPI desde GitHub?

Sube el repositorio a GitHub y después New App → Deploy from GitHub en el lado de Containers. La primera vez, GitHub te pide instalar la app de Back4app Containers en los repositorios que elijas; la lista de la dashboard se actualiza en cuanto terminas.

Configura. Selecciona el repositorio. El formulario detectó el Dockerfile y ofreció un buildpack de Python como alternativa: leyó el lenguaje del repositorio. Nos quedamos con el Dockerfile, dejamos el puerto en automático y añadimos PARSE_APP_ID y PARSE_REST_KEY.

También cambiamos Health check de / a /healthz. Resultó no importar para el deploy (mira la sección de errores), pero es la ruta que tu propia monitorización debería usar.

El formulario de deploy de fastapi-tasks: Dockerfile seleccionado con un Python Buildpack como alternativa, dos variables de entorno con los valores ocultos, health check /healthz

Despliega. Hicimos clic a las 15:00:16. El botón decía “Deploying…” durante 30 segundos antes de que apareciera la página de la app; después PREPARING a las 15:00:46, BUILDING IMAGE durante 46 segundos (pip install sobre python:3.12-slim), LAUNCHING CONTAINER a las 15:01:33 y DEPLOYMENT READY a las 15:01:42.

Medido · primer deploy · 15 de septiembre de 2026
86 sclic en Deploy → DEPLOYMENT READY
→
46 sde ellos, pip install + build de la imagen
17 sel primer 200 llegó antes de la línea Ready

Cómo lo medimos: un cronómetro desde el clic en Deploy hasta la línea DEPLOYMENT READY de la dashboard, plan Free, un deploy el 15 de septiembre de 2026, con un bucle consultando la URL pública cada segundo en paralelo para capturar el primer 200. pip install domina, así que tu tiempo se mueve con requirements.txt.

deploy-check.sh hizo después el ciclo completo contra la URL en producción: health, create (201, con el objectId de vuelta), read, update (PATCH {"done": true}), validate (un título en blanco → 422), delete (204). Doce segundos después de Ready, las seis pasaron.

La página /docs de FastAPI funciona sin cambios en la URL, que es la forma más rápida de enseñarle la API a alguien sin mandarle un curl:

La Swagger UI de FastAPI en /docs sobre la URL b4a.run, con las seis rutas y los schemas TaskIn, TaskPatch y de validación

¿Dónde acaban las tareas? El backend detrás de la API

Tres pantallas de la dashboard del backend, y cada fila que aparece en ellas la escribió el container de arriba: sin datos de semilla, sin schema definido a mano.

Overview: de dónde salen los dos headers. Dashboard → New App → Build your Backend → Create. La página Overview muestra el App ID y, en el desplegable Keys, la REST API key. Esos dos valores se convierten en PARSE_APP_ID y PARSE_REST_KEY en el formulario del container; no hay cadena de conexión en ningún sitio.

La Overview del backend de fastapi-tasks: App ID, el desplegable Keys en Rest Key (valor difuminado), Parse Server 7.5.2 y MongoDB 3.6 a la derecha

Cloud Code: donde vive la regla. cloud/main.js abierto en el editor con el hook beforeSave("Task"), y la insignia Files pending deploy (1) junto al botón Deploy. En un backend recién creado esa insignia es la pista: sigue ahí después del primer Deploy, y así sabes que no se envió nada.

El editor de Cloud Code con cloud/main.js abierto mostrando el hook beforeSave de Task, y la insignia Files pending deploy (1) junto al botón Deploy

Database Browser: la clase Task. Seis filas. Las tres más recientes llegaron por el POST /tasks del container mientras escribíamos esta sección; la titulada Deploy the container se envió con espacios alrededor y se guardó recortada.

Las dos filas sin título visible son tres espacios cada una: se escribieron el 15 de septiembre, antes de desplegar el hook, que es exactamente el agujero que el hook cierra.

El Database Browser mostrando la clase Task con seis objetos: las columnas title y done inferidas por el primer POST, tres tareas nuevas del 17 de septiembre y dos filas más antiguas con el título en blanco

¿Cuánto dura la URL y cuánto cuesta que sea permanente?

En el plan Free, 60 minutos desde el momento en que empieza el deploy; después el container se destruye y la dashboard muestra URL Expired. En el plan Shared, US$ 5 al mes por 0,5 vCPU y 512 MB, la URL es permanente, y el cambio de plan por sí solo volvió a desplegar la app en la misma dirección (todo a 16 de septiembre de 2026).

La hora gratuita es real y es suficiente: cada paso de este post, errores incluidos, cabe en una ventana. No es suficiente para dejar la API corriendo para un cliente, y la plataforma lo dice tal cual: la tarjeta del plan Free dice “Ideal for developing, learning, and prototyping”.

Medimos la caducidad al minuto en el worker de nuestro post sobre procesos siempre activos: deploy iniciado a las 14:55:28, URL desaparecida a las 15:55:27. Esta API mostró el mismo estado URL Expired a la mañana siguiente.

A la mañana siguiente cambiamos el plan. Sin ningún clic en Deploy, el log mostró PREPARING DEPLOYMENT a las 12:35:32 y DEPLOYMENT READY a las 12:36:24 (52 segundos) en la misma URL b4a.run, y deploy-check.sh volvió a pasar.

La Overview del container en el plan Shared: US$ 5,00 al mes, 0,5 vCPU y 512 MB, estado Available, el redeploy automático Ready en 52 s, el log terminando en el CMD de uvicorn y Pushed image

Medido · plan Shared · 16 de septiembre de 2026
52 scambio de plan → redesplegado, misma URL
→
US$ 5al mes, 0,5 vCPU · 512 MB
60 minvida de cada deploy en el plan Free

¿Qué significan 422, 404 y 502 en esta API y cómo se corrigen?

Rompimos el deploy a propósito, y la plataforma lo rompió una vez por nosotros. Cada entrada empieza por lo que verás de verdad.

KeyError: 'PARSE_APP_ID' en cuanto uvicorn importa la app. os.environ["PARSE_APP_ID"] lanza la excepción en el momento del import cuando falta la variable, así que no hay proceso al que llegue el health check. Añade las dos variables en Settings → Environment y despliega otra vez; una variable guardada tiene efecto en el siguiente deploy, no en el que está corriendo.

422 {"detail":{"code":142,"error":"title is required."}} en un título que Pydantic aceptó. Funciona como debe: el hook del backend lo rechazó. La forma del 422 es distinta de la lista de errores de Pydantic; si tu cliente parsea detail, contempla tanto una lista como un objeto.

404 {"detail":{"code":101,"error":"Object not found."}} en GET /tasks/{id}. El id no es el objectId de una Task; Parse responde 101 y el container lo mapea a 404. Los ids son cadenas de diez caracteres que distinguen mayúsculas de minúsculas, como OFTBmFAMs3.

502 {"detail":{"error":"{\"error\":\"unauthorized\"}"}} en todas las rutas salvo /healthz. Una PARSE_REST_KEY incorrecta. El backend responde unauthorized sin código de Parse, así que parse_error cae en el 502 con el cuerpo crudo como cadena. El health check pasó porque /healthz no toca el backend, y la comprobación de la plataforma es una comprobación de puerto de todos modos. Haz que /healthz ejecute una query barata para que deploy-check.sh y tu propio monitor detecten una clave incorrecta.

DEPLOYMENT READY en una API cuyas rutas fallan todas. El paso de health de la plataforma comprueba que el puerto responde, no el estado: en nuestro worker, una ruta que devolvía 404 lo pasó. Una clave incorrecta o un handler caído no se detectan, por tanto, en el deploy: ejecuta deploy-check.sh contra la URL después de cada deploy.

404 not found en la URL, y la dashboard dice URL Expired. La hora gratuita terminó y el container ya no existe. Vuelve a desplegar para otra hora, o cambia el plan: el cambio de plan redespliega por sí solo.

¿Cuándo es esta la respuesta equivocada?

Por nuestro propio manifiesto, un post en el que solo gana la plataforma no se publica, así que aquí está la otra columna.

  • Tu API es CRUD y nada más. Entonces no necesitas el container: la capa REST del backend es la API, y un cliente en Python puede llamarla directamente con los mismos dos headers.
  • Necesitas funciones de Postgres. Joins entre muchas tablas, funciones de ventana, PostGIS, un schema que migras con Alembic: el backend es Parse Server sobre MongoDB (Postgres en los planes de pago), y su lenguaje de consulta es JSON en where, no SQL.
  • Necesitas un worker en segundo plano junto a la API. Un container es un proceso; un consumidor de cola va en su propio container, como en nuestro post sobre procesos siempre activos.
  • Quieres construir las imágenes en local y subirlas. La vía de imagen solo acepta Docker Hub.

Si ninguno de esos casos te describe, un container de FastAPI sin estado delante de un backend gestionado es la forma que volveríamos a elegir, y el par Pydantic más hook es como lo validaríamos.

¿Qué añadirías a la API de tareas a continuación?

Tienes una API que se redespliega en menos de un minuto, una base de datos que nunca aprovisionaste y reglas que sobreviven a que el container sea reemplazado. A partir de aquí:

  • Dale propietarios a las tareas. La clase User de Parse añade endpoints de creación de cuenta e inicio de sesión sin código en el container (medido, con restablecimiento de contraseña); un session token en el header de la solicitud acota cada query una vez cerrada la clase (la sonda de doce solicitudes).
  • Activa el autodeploy. En un plan de pago, Settings → Build & deploy → Autodeploy; medimos un push hasta producción en 44 segundos en el worker.
  • Haz honesto el health check. Haz que /healthz ejecute GET /classes/Task?limit=0 contra el backend para que una clave incorrecta aparezca en tu monitorización, no en el primer usuario.

Todo el código de arriba está completo y probado: el repositorio companion incluye deploy-check.sh, que ejecuta health, create, read, update, validate y delete contra cualquier URL de deploy que le des.

Etiquetaspythonfastapidockercontainersrestparse-server

Preguntas frecuentes

¿Puedo desplegar una app FastAPI con Docker sin administrar un servidor?

Sí. En Back4app Containers conectas un repositorio de GitHub que tenga un Dockerfile; la plataforma construye la imagen con kaniko, ejecuta uvicorn, termina el HTTPS y te da una URL. Nuestro primer deploy fue del clic en Deploy a DEPLOYMENT READY en 86 segundos (15 de septiembre de 2026), 46 de ellos el pip install.

¿De dónde sale la base de datos si el container no tiene estado?

De una app de backend de Back4app: un Parse Server gestionado con una REST API. El container de FastAPI lo llama con httpx usando dos headers, el App ID y la REST key, y nunca abre una conexión a la base de datos. La clase Task la creó el primer POST; no se definió ningún schema de antemano.

¿FastAPI necesita un Dockerfile en esta plataforma?

No. El formulario de deploy detectó el repositorio como Python y ofreció un buildpack de Python como alternativa al Dockerfile. Usamos el Dockerfile porque fija el intérprete (python:3.12-slim) y el comando exacto de uvicorn; el buildpack es la vía cuando no quieres encargarte de eso.

¿Por qué un título en blanco pasó Pydantic pero no el backend?

Field(min_length=1) cuenta caracteres, y tres espacios son tres caracteres, así que Pydantic aceptó ' '. El hook beforeSave del backend recorta el título y rechaza uno vacío con el código de error 142, que el container convierte en HTTP 422. Deja la regla en el backend para que un comando curl y la app en Python reciban la misma respuesta.

Compara con

Cómo desplegar una app Node.js con Docker y una base de datos, sin administrar un servidor →

Lecturas complementarias

Revisado y verificado por Back4app Engineering, Editorial review en Back4app

Probado el 16 de septiembre de 2026 — Containers: Back4app, plan Shared, 0,5 vCPU, 512 MB (plan Free durante la primera hora) · Backend: Back4app, USA East · Local: Python 3.12, FastAPI 0.115, httpx.

Más de Back4app Engineering

Containers · tutorial

Cómo desplegar una app Node.js con Docker y una base de datos, sin administrar un servidor

Containers · tutorial

Cómo ejecutar un bot, un webhook o un cron job de Node.js 24/7 sin VPS

Backend · tutorial

Cómo añadir autenticación a una app web o Flutter sin escribir un servidor de auth