
Ejecutar una API con fastapi dev, publicar Uvicorn directamente en Internet o depender de un único proceso puede provocar problemas de capacidad, TLS y recuperación ante fallos.
Para producción, FastAPI recomienda construir una imagen propia y ejecutar fastapi run, que utiliza Uvicorn. La terminación TLS puede delegarse a Nginx, un balanceador o el proveedor cloud. La estrategia de workers depende del entorno: varios procesos pueden resultar útiles en un servidor único, mientras que en Kubernetes suele convenir un proceso por contenedor y varias réplicas. Documentación de FastAPI sobre contenedores.
Cuándo conviene aplicarlo
Este esquema resulta útil para:
- APIs y microservicios Python.
- Backends para aplicaciones web o móviles.
- Servicios internos con requisitos de disponibilidad.
- Despliegues en Docker Compose, Kubernetes o servicios gestionados.
Requisitos previos
Necesitarás:
- Un proyecto FastAPI funcional.
- Dependencias bloqueadas en
requirements.txt,uv.locko un formato equivalente. - Docker y acceso al registry.
- Variables de entorno definidas fuera de la imagen.
- Nginx o un balanceador si necesitas TLS y proxy inverso.
- Endpoints de salud y una estrategia de logs.
- Límites de CPU y memoria medidos para la aplicación.
Arquitectura recomendada
El flujo habitual conecta:
- Cliente.
- Nginx o balanceador con terminación TLS.
- Red privada de contenedores.
- Contenedor FastAPI.
- Uno o varios workers.
- Logs, métricas y comprobaciones de salud.
Solo el proxy debería publicar un puerto hacia Internet. FastAPI puede escuchar en 0.0.0.0:8000 dentro de la red privada sin exponer ese puerto directamente en el host.
Paso 1: preparar el Dockerfile
El siguiente ejemplo utiliza Python 3.12, instala primero las dependencias para aprovechar la caché y ejecuta la aplicación con un usuario sin privilegios:
# syntax=docker/dockerfile:1
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
RUN addgroup --system --gid 10001 app \
&& adduser --system \
--uid 10001 \
--ingroup app \
--home /nonexistent \
--no-create-home \
app
COPY requirements.txt /app/requirements.txt
RUN pip install \
--no-cache-dir \
--upgrade \
-r /app/requirements.txt
COPY --chown=10001:10001 ./app /app/app
USER 10001:10001
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/live', timeout=2)"]
CMD ["fastapi", "run", "app/main.py", "--host", "0.0.0.0", "--port", "8000"]La forma JSON de CMD permite que el proceso reciba correctamente las señales de terminación y ejecute los eventos de cierre de FastAPI. COPY --chown evita dejar el código bajo propiedad de root. Referencia de Dockerfile.
Agrega también un .dockerignore:
.git
.github
.env
.env.*
__pycache__/
*.py[cod]
.venv/
tests/
dist/No excluyas migraciones, certificados públicos u otros archivos que la aplicación necesite en tiempo de ejecución.
Paso 2: implementar liveness y readiness
Liveness responde si el proceso ASGI está funcionando. No debe consultar servicios externos:
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/health/live", include_in_schema=False)
async def liveness() -> dict[str, str]:
return {"status": "ok"}Readiness indica si la instancia puede recibir tráfico. Aquí sí puede realizar una comprobación ligera de dependencias:
@app.get("/health/ready", include_in_schema=False)
async def readiness() -> dict[str, str]:
try:
await check_database()
except Exception as exc:
raise HTTPException(
status_code=503,
detail="service not ready",
) from exc
return {"status": "ready"}check_database() representa una función específica de la aplicación. Debe utilizar un timeout corto y no ejecutar consultas costosas.
Evita devolver versiones, cadenas de conexión o detalles internos del error. Si estos endpoints no necesitan ser públicos, restríngelos a la red interna o al sistema de monitoreo.
Paso 3: construir y probar la imagen
Construye una imagen local:
docker build \
--pull \
--tag "fastapi-app:${GIT_SHA:-local}" \
.No es necesario escapar los dos puntos de la etiqueta.
Ejecuta el contenedor limitando recursos y publicando el puerto únicamente en la interfaz local:
docker run -d \
--name fastapi-app \
--restart unless-stopped \
--stop-timeout 30 \
--memory 1g \
--cpus 2 \
--env-file /ruta/segura/fastapi.env \
--publish 127.0.0.1:8000:8000 \
"fastapi-app:${GIT_SHA:-local}"Los contenedores no tienen límites de CPU o memoria de forma predeterminada, por lo que deben definirse después de medir el consumo real. Límites de recursos en Docker.
Comprueba el estado:
curl --fail --silent --show-error \
http://127.0.0.1:8000/health/live
docker inspect \
--format '{{json .State.Health}}' \
fastapi-app |
jq .
docker logs --tail 100 fastapi-appPaso 4: elegir la estrategia de workers
En un servidor único o un despliegue sencillo con Docker Compose, puedes ejecutar varios workers:
fastapi run app/main.py \
--host 0.0.0.0 \
--port 8000 \
--workers 4Cada worker es un proceso separado y carga su propia copia de la aplicación. Cuatro workers no implican automáticamente cuatro veces más rendimiento, pero sí pueden acercarse a cuatro veces el consumo base de memoria.
Empieza con uno o dos workers y mide:
- Latencia p50, p95 y p99.
- Uso de CPU.
- Memoria por proceso.
- Errores y timeouts.
- Tiempo de arranque.
- Capacidad de la base de datos y del pool de conexiones.
En Kubernetes, Nomad o un sistema que ya replica contenedores, ejecuta normalmente un worker por contenedor y escala réplicas. FastAPI documenta explícitamente ambas estrategias. Workers y replicación.
Las migraciones de base de datos no deben ejecutarse una vez por worker. Ejecútalas como una tarea previa y controlada antes de habilitar las nuevas instancias.
Paso 5: configurar Nginx como proxy
Una configuración base puede utilizar un nombre DNS interno de Docker:
upstream fastapi_backend {
server fastapi:8000;
keepalive 32;
}
server {
listen 443 ssl;
server_name api.ejemplo.com;
ssl_certificate /etc/nginx/tls/fullchain.pem;
ssl_certificate_key /etc/nginx/tls/private-key.pem;
location / {
proxy_pass http://fastapi_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
}
}Nginx permite modificar explícitamente Host y las cabeceras enviadas al servidor upstream mediante proxy_set_header. Documentación oficial de Nginx.
Valida antes de recargar:
nginx -t
nginx -s reloadSi utilizas WebSockets, agrega la configuración de Upgrade y Connection correspondiente. Si publicas la aplicación bajo un prefijo como /api, configura también root_path.
Paso 6: confiar en el proxy correcto
Para que FastAPI interprete HTTPS, host e IP original, habilita las cabeceras proxy y especifica las direcciones confiables:
fastapi run app/main.py \
--host 0.0.0.0 \
--port 8000 \
--proxy-headers \
--forwarded-allow-ips="172.20.0.0/24"Sustituye el rango por la red real donde se encuentra Nginx.
También puede utilizarse --forwarded-allow-ips="*", pero únicamente cuando el puerto de FastAPI no admite conexiones directas y la red está suficientemente aislada. De otro modo, un cliente podría enviar cabeceras X-Forwarded-* falsas. FastAPI detrás de un proxy.
Paso 7: validar el despliegue completo
Realiza las comprobaciones en este orden:
curl --fail http://127.0.0.1:8000/health/live
curl --fail http://127.0.0.1:8000/health/ready
curl --fail https://api.ejemplo.com/health/readyDespués verifica:
- Que una redirección genere una URL
https://. - Que FastAPI no sea accesible desde Internet por el puerto 8000.
- Que Nginx rechace certificados o configuraciones inválidas.
- Que el contenedor pase a
unhealthycuando falle liveness. - Que readiness responda
503cuando una dependencia no esté disponible. - Que un
docker stopproduzca un cierre ordenado. - Que los logs no contengan tokens, contraseñas ni cuerpos sensibles.
- Que el servicio se recupere después de reiniciar un worker o contenedor.
Mantén la versión anterior de la imagen disponible hasta completar las pruebas de humo y confirmar las métricas.

Problemas frecuentes
El contenedor reinicia continuamente
Comprueba los imports, variables obligatorias, permisos de archivos y el comando de inicio:
docker inspect fastapi-app
docker logs --tail 200 fastapi-appDistingue entre un fallo de la aplicación, un healthcheck fallido y una terminación por falta de memoria.
Nginx devuelve 502
Verifica que FastAPI escuche en 0.0.0.0:8000, que ambos contenedores compartan red y que fastapi:8000 resuelva desde Nginx.
Las redirecciones utilizan HTTP
Confirma X-Forwarded-Proto, --proxy-headers y --forwarded-allow-ips. El problema suele aparecer cuando Uvicorn no confía en la dirección del proxy.
El contenedor queda sin memoria
Cada worker replica el estado de la aplicación. Reduce workers, revisa modelos o cachés cargados al inicio y establece un límite compatible con el consumo máximo observado.
El healthcheck pasa, pero la API no funciona
Probablemente solo se está verificando liveness. Agrega readiness con las dependencias imprescindibles y comprueba que el balanceador utilice el endpoint correcto.

Buenas prácticas para producción
- Construye la imagen desde una versión fijada de Python.
- Bloquea versiones y verifica dependencias en CI.
- Ejecuta como usuario no root.
- Mantén secretos fuera de la imagen.
- Publica imágenes con tag de commit y conserva su digest.
- Expón únicamente Nginx o el balanceador.
- Separa liveness de readiness.
- Envía logs estructurados a
stdoutystderr. - Define timeouts y límites de recursos.
- Mide latencia, errores, saturación y reinicios.
- Prueba el cierre ordenado durante cada release.
- Usa un worker por contenedor cuando el orquestador gestione réplicas.