Saltar al contenido

FastAPI en Contenedores - Docker

Al desplegar aplicaciones FastAPI un enfoque común es construir una imagen de contenedor Linux. Normalmente se hace usando Docker. Luego puedes desplegar esa imagen de contenedor de varias maneras posibles.

Usar contenedores Linux tiene varias ventajas incluyendo seguridad, replicabilidad, simplicidad, entre otras.

Consejo

¿Tienes prisa y ya conoces todo esto? Salta al Dockerfile de abajo 👇.

Vista previa del Dockerfile 👀
FROM python:3.14

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

COPY ./app /code/app

CMD ["fastapi", "run", "app/main.py", "--port", "80"]

# If running behind a proxy like Nginx or Traefik add --proxy-headers
# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"]

Qué es un Contenedor

Los contenedores (principalmente contenedores Linux) son una forma muy ligera de empaquetar aplicaciones incluyendo todas sus dependencias y archivos necesarios manteniéndolos aislados de otros contenedores (otras aplicaciones o componentes) en el mismo sistema.

Los contenedores Linux se ejecutan usando el mismo kernel Linux del host (máquina, máquina virtual, servidor en la nube, etc). Esto simplemente significa que son muy ligeros (comparados con máquinas virtuales completas que emulan un sistema operativo entero).

De esta manera, los contenedores consumen pocos recursos, una cantidad comparable a ejecutar los procesos directamente (una máquina virtual consumiría mucho más).

Los contenedores también tienen sus propios procesos aislados en ejecución (comúnmente un solo proceso), sistema de archivos y red, simplificando el despliegue, la seguridad, el desarrollo, etc.

Qué es una Imagen de Contenedor

Un contenedor se ejecuta a partir de una imagen de contenedor.

Una imagen de contenedor es una versión estática de todos los archivos, variables de entorno y el comando/programa por defecto que deberían estar presentes en un contenedor. Estática aquí significa que la imagen de contenedor no está en ejecución, no se está ejecutando, son solo los archivos empaquetados y los metadatos.

En contraste con una "imagen de contenedor" que es el contenido estático almacenado, un "contenedor" normalmente se refiere a la instancia en ejecución, lo que está siendo ejecutado.

Cuando el contenedor se inicia y está en ejecución (iniciado desde una imagen de contenedor) podría crear o cambiar archivos, variables de entorno, etc. Esos cambios existirán solo en ese contenedor, pero no persistirán en la imagen de contenedor subyacente (no se guardarán en disco).

Una imagen de contenedor es comparable al archivo y contenidos del programa, por ejemplo python y algún archivo main.py.

Y el contenedor mismo (en contraste con la imagen de contenedor) es la instancia en ejecución de la imagen, comparable a un proceso. De hecho, un contenedor está en ejecución solo cuando tiene un proceso en ejecución (y normalmente es un solo proceso). El contenedor se detiene cuando no hay ningún proceso ejecutándose en él.

Imágenes de Contenedor

Docker ha sido una de las principales herramientas para crear y gestionar imágenes de contenedor y contenedores.

Y hay un Docker Hub público con imágenes de contenedor oficiales prehechas para muchas herramientas, entornos, bases de datos y aplicaciones.

Por ejemplo, hay una Imagen Python oficial.

Y hay muchas otras imágenes para diferentes cosas como bases de datos, por ejemplo para:

Al usar una imagen de contenedor prehecha es muy fácil combinar y usar diferentes herramientas. Por ejemplo, para probar una nueva base de datos. En la mayoría de los casos, puedes usar las imágenes oficiales y simplemente configurarlas con variables de entorno.

De esa manera, en muchos casos puedes aprender sobre contenedores y Docker y reutilizar ese conocimiento con muchas herramientas y componentes diferentes.

Así, podrías ejecutar múltiples contenedores con diferentes cosas, como una base de datos, una aplicación Python, un servidor web con una aplicación frontend en React, y conectarlos entre sí a través de su red interna.

Todos los sistemas de gestión de contenedores (como Docker o Kubernetes) tienen estas características de red integradas en ellos.

Contenedores y Procesos

Una imagen de contenedor normalmente incluye en sus metadatos el programa o comando por defecto que debería ejecutarse cuando se inicia el contenedor y los parámetros que se pasarán a ese programa. Muy similar a lo que sería si estuviera en la línea de comandos.

Cuando se inicia un contenedor, ejecutará ese comando/programa (aunque puedes sobrescribirlo y hacer que ejecute un comando/programa diferente).

Un contenedor está en ejecución mientras el proceso principal (comando o programa) esté en ejecución.

Un contenedor normalmente tiene un solo proceso, pero también es posible iniciar subprocesos desde el proceso principal, y de esa manera tendrás múltiples procesos en el mismo contenedor.

Pero no es posible tener un contenedor en ejecución sin al menos un proceso en ejecución. Si el proceso principal se detiene, el contenedor se detiene.

Construir una Imagen Docker para FastAPI

Bien, ¡construyamos algo ahora! 🚀

Te mostraré cómo construir una imagen Docker para FastAPI desde cero, basada en la imagen oficial de Python.

Esto es lo que querrías hacer en la mayoría de los casos, por ejemplo:

  • Usando Kubernetes o herramientas similares
  • Cuando se ejecuta en una Raspberry Pi
  • Usando un servicio en la nube que ejecutaría una imagen de contenedor por ti, etc.

Requisitos de Paquetes

Normalmente tendrías los requisitos de paquetes para tu aplicación en algún archivo.

Dependería principalmente de la herramienta que uses para instalar esos requisitos.

La forma más común de hacerlo es tener un archivo requirements.txt con los nombres de los paquetes y sus versiones, uno por línea.

Por supuesto, usarías las mismas ideas que leíste en Acerca de las versiones de FastAPI para establecer los rangos de versiones.

Por ejemplo, tu requirements.txt podría verse así:

fastapi[standard]>=0.113.0,<0.114.0
pydantic>=2.7.0,<3.0.0

Y normalmente instalarías esas dependencias de paquetes con pip, por ejemplo:

fast →pip install -r requirements.txtSuccessfully installed fastapi pydantic

restart ↻

Nota

Hay otros formatos y herramientas para definir e instalar dependencias de paquetes.

Crear el Código de FastAPI

  • Crea un directorio app y entra en él.
  • Crea un archivo vacío __init__.py.
  • Crea un archivo main.py con:
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
    return {"item_id": item_id, "q": q}

Dockerfile

Ahora en el mismo directorio del proyecto crea un archivo Dockerfile con:


FROM python:3.14


WORKDIR /code


COPY ./requirements.txt /code/requirements.txt


RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt


COPY ./app /code/app


CMD ["fastapi", "run", "app/main.py", "--port", "80"]

Consejo

Revisa lo que hace cada línea haciendo clic en cada burbuja de número en el código. 👆

Aviso

Asegúrate de siempre usar la forma exec de la instrucción CMD, como se explica a continuación.

Usar CMD - Forma Exec

La instrucción CMD de Docker puede escribirse usando dos formas:

✅ Forma Exec:

# ✅ Do this
CMD ["fastapi", "run", "app/main.py", "--port", "80"]

⛔️ Forma Shell:

# ⛔️ Don't do this
CMD fastapi run app/main.py --port 80

Asegúrate de usar siempre la forma exec para garantizar que FastAPI pueda apagarse correctamente y que los eventos del ciclo de vida se activen.

Puedes leer más sobre esto en la documentación de Docker sobre la forma shell y exec.

Esto puede ser bastante notable cuando se usa docker compose. Consulta esta sección de las preguntas frecuentes de Docker Compose para más detalles técnicos: ¿Por qué mis servicios tardan 10 segundos en recrearse o detenerse?.

Estructura de Directorios

Ahora deberías tener una estructura de directorios como:

.
├── app
│   ├── __init__.py
│   └── main.py
├── Dockerfile
└── requirements.txt

Detrás de un Proxy de Terminación TLS

Si estás ejecutando tu contenedor detrás de un Proxy de Terminación TLS (balanceador de carga) como Nginx o Traefik, añade la opción --proxy-headers, esto le indicará a Uvicorn (a través del CLI de FastAPI) que confíe en los headers enviados por ese proxy indicándole que la aplicación se está ejecutando detrás de HTTPS, etc.

CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"]

Caché de Docker

Hay un truco importante en este Dockerfile: primero copiamos el archivo con las dependencias únicamente, no el resto del código. Déjame explicarte por qué.

COPY ./requirements.txt /code/requirements.txt

Docker y otras herramientas construyen estas imágenes de contenedor incrementalmente, añadiendo una capa sobre otra, comenzando desde el inicio del Dockerfile y añadiendo cualquier archivo creado por cada una de las instrucciones del Dockerfile.

Docker y herramientas similares también usan una caché interna al construir la imagen, si un archivo no ha cambiado desde la última vez que se construyó la imagen de contenedor, entonces reutilizará la misma capa creada la última vez, en lugar de copiar el archivo nuevamente y crear una nueva capa desde cero.

Simplemente evitar la copia de archivos no necesariamente mejora mucho las cosas, pero como se usó la caché para ese paso, puede usar la caché para el siguiente paso. Por ejemplo, podría usar la caché para la instrucción que instala las dependencias con:

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

El archivo con los requisitos de paquetes no cambiará frecuentemente. Así que, al copiar solo ese archivo, Docker podrá usar la caché para ese paso.

Y luego, Docker podrá usar la caché para el siguiente paso que descarga e instala esas dependencias. Y aquí es donde ahorramos mucho tiempo. ✨ ...y evitamos el aburrimiento de esperar. 😪😆

Descargar e instalar las dependencias de paquetes podría tardar minutos, pero usando la caché tardaría segundos como máximo.

Y como estarías construyendo la imagen de contenedor una y otra vez durante el desarrollo para comprobar que tus cambios en el código funcionan, hay mucho tiempo acumulado que esto ahorraría.

Luego, cerca del final del Dockerfile, copiamos todo el código. Como esto es lo que cambia con más frecuencia, lo ponemos cerca del final, porque casi siempre, cualquier cosa después de este paso no podrá usar la caché.

COPY ./app /code/app

Construir la Imagen Docker

Ahora que todos los archivos están en su lugar, vamos a construir la imagen de contenedor.

  • Ve al directorio del proyecto (donde está tu Dockerfile, que contiene tu directorio app).
  • Construye tu imagen FastAPI:
fast →docker build -t myimage .

restart ↻

Consejo

Nota el . al final, es equivalente a ./, le indica a Docker el directorio a usar para construir la imagen de contenedor.

En este caso, es el mismo directorio actual (.).

Iniciar el Contenedor Docker

  • Ejecuta un contenedor basado en tu imagen:
fast →docker run -d --name mycontainer -p 80:80 myimage
restart ↻

Revísalo

Deberías poder comprobarlo en la URL de tu contenedor Docker, por ejemplo: http://192.168.99.100/items/5?q=somequery o http://127.0.0.1/items/5?q=somequery (o equivalente, usando tu host Docker).

Verás algo como:

{"item_id": 5, "q": "somequery"}

Documentación interactiva de la API

Ahora puedes ir a http://192.168.99.100/docs o http://127.0.0.1/docs (o equivalente, usando tu host Docker).

Verás la documentación interactiva automática de la API (proporcionada por Swagger UI):

Swagger UI

Documentación alternativa de la API

Y también puedes ir a http://192.168.99.100/redoc o http://127.0.0.1/redoc (o equivalente, usando tu host Docker).

Verás la documentación alternativa automática (proporcionada por ReDoc):

ReDoc

Construir una Imagen Docker con un FastAPI de Archivo Único

Si tu FastAPI es un solo archivo, por ejemplo, main.py sin un directorio ./app, tu estructura de archivos podría verse así:

.
├── Dockerfile
├── main.py
└── requirements.txt

Entonces solo tendrías que cambiar las rutas correspondientes para copiar el archivo dentro del Dockerfile:

FROM python:3.14

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt


COPY ./main.py /code/


CMD ["fastapi", "run", "main.py", "--port", "80"]

Cuando pasas el archivo a fastapi run detectará automáticamente que es un solo archivo y no parte de un paquete, y sabrá cómo importarlo y servir tu aplicación FastAPI. 😎

Conceptos de Despliegue

Hablemos nuevamente sobre algunos de los mismos Conceptos de Despliegue en términos de contenedores.

Los contenedores son principalmente una herramienta para simplificar el proceso de construcción y despliegue de una aplicación, pero no imponen un enfoque particular para manejar estos conceptos de despliegue, y hay varias estrategias posibles.

Las buenas noticias son que con cada estrategia diferente hay una forma de cubrir todos los conceptos de despliegue. 🎉

Revisemos estos conceptos de despliegue en términos de contenedores:

  • HTTPS
  • Ejecución al inicio
  • Reinicios
  • Replicación (el número de procesos en ejecución)
  • Memoria
  • Pasos previos antes de iniciar

HTTPS

Si nos enfocamos solo en la imagen de contenedor para una aplicación FastAPI (y luego en el contenedor en ejecución), HTTPS normalmente sería manejado externamente por otra herramienta.

Podría ser otro contenedor, por ejemplo con Traefik, manejando HTTPS y la adquisición automática de certificados.

Consejo

Traefik tiene integraciones con Docker, Kubernetes y otros, por lo que es muy fácil configurar y configurar HTTPS para tus contenedores con él.

Alternativamente, HTTPS podría ser manejado por un proveedor en la nube como uno de sus servicios (mientras aún se ejecuta la aplicación en un contenedor).

Ejecución al Inicio y Reinicios

Normalmente hay otra herramienta encargada de iniciar y ejecutar tu contenedor.

Podría ser Docker directamente, Docker Compose, Kubernetes, un servicio en la nube, etc.

En la mayoría (o todos) de los casos, hay una opción simple para habilitar la ejecución del contenedor al inicio y habilitar reinicios ante fallos. Por ejemplo, en Docker, es la opción de línea de comandos --restart.

Sin usar contenedores, hacer que las aplicaciones se ejecuten al inicio y con reinicios puede ser engorroso y difícil. Pero cuando se trabaja con contenedores en la mayoría de los casos esa funcionalidad está incluida por defecto. ✨

Replicación - Número de Procesos

Si tienes un clúster de máquinas con Kubernetes, Docker Swarm Mode, Nomad u otro sistema complejo similar para gestionar contenedores distribuidos en múltiples máquinas, entonces probablemente querrás manejar la replicación a nivel de clúster en lugar de usar un gestor de procesos (como Uvicorn con workers) en cada contenedor.

Uno de esos sistemas de gestión de contenedores distribuidos como Kubernetes normalmente tiene alguna forma integrada de manejar la replicación de contenedores mientras sigue soportando balanceo de carga para las peticiones entrantes. Todo a nivel de clúster.

En esos casos, probablemente querrás construir una imagen Docker desde cero como se explicó anteriormente, instalar tus dependencias y ejecutar un solo proceso Uvicorn en lugar de usar múltiples workers de Uvicorn.

Balanceador de Carga

Cuando se usan contenedores, normalmente tendrías algún componente escuchando en el puerto principal. Posiblemente podría ser otro contenedor que también sea un Proxy de Terminación TLS para manejar HTTPS o alguna herramienta similar.

Como este componente tomaría la carga de peticiones y la distribuiría entre los workers de manera (con suerte) balanceada, también se le conoce comúnmente como Balanceador de Carga.

Consejo

El mismo componente Proxy de Terminación TLS usado para HTTPS probablemente también sería un Balanceador de Carga.

Y cuando se trabaja con contenedores, el mismo sistema que usas para iniciarlos y gestionarlos ya tendría herramientas internas para transmitir la comunicación de red (por ejemplo, peticiones HTTP) desde ese balanceador de carga (que también podría ser un Proxy de Terminación TLS) al contenedor o contenedores con tu aplicación.

Un Balanceador de Carga - Múltiples Contenedores Worker

Cuando se trabaja con Kubernetes o sistemas similares de gestión de contenedores distribuidos, usar sus mecanismos de red internos permitiría que el único balanceador de carga que está escuchando en el puerto principal transmita la comunicación (peticiones) posiblemente a múltiples contenedores ejecutando tu aplicación.

Cada uno de estos contenedores ejecutando tu aplicación normalmente tendría un solo proceso (por ejemplo, un proceso Uvicorn ejecutando tu aplicación FastAPI). Todos serían contenedores idénticos, ejecutando lo mismo, pero cada uno con su propio proceso, memoria, etc. De esa manera aprovecharías la paralelización en diferentes núcleos de la CPU, o incluso en diferentes máquinas.

Y el sistema de contenedores distribuidos con el balanceador de carga distribuiría las peticiones a cada uno de los contenedores con tu aplicación por turnos. Así, cada petición podría ser manejada por uno de los múltiples contenedores replicados ejecutando tu aplicación.

Y normalmente este balanceador de carga podría manejar peticiones que van a otras aplicaciones en tu clúster (por ejemplo, a un dominio diferente, o bajo un prefijo de ruta URL diferente), y transmitiría esa comunicación a los contenedores correctos para esa otra aplicación ejecutándose en tu clúster.

Un Proceso por Contenedor

En este tipo de escenario, probablemente querrías tener un solo proceso (Uvicorn) por contenedor, ya que estarías manejando la replicación a nivel de clúster.

Así que, en este caso, no querrías tener múltiples workers en el contenedor, por ejemplo con la opción de línea de comandos --workers. Querrías tener un solo proceso Uvicorn por contenedor (pero probablemente múltiples contenedores).

Tener otro gestor de procesos dentro del contenedor (como sería con múltiples workers) solo añadiría complejidad innecesaria que probablemente ya estás manejando con tu sistema de clúster.

Contenedores con Múltiples Procesos y Casos Especiales

Por supuesto, hay casos especiales donde podrías querer tener un contenedor con varios procesos worker de Uvicorn dentro.

En esos casos, puedes usar la opción de línea de comandos --workers para establecer el número de workers que quieres ejecutar:

FROM python:3.14

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

COPY ./app /code/app


CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"]

Aquí hay algunos ejemplos de cuándo podría tener sentido:

Una Aplicación Simple

Podrías querer un gestor de procesos en el contenedor si tu aplicación es lo suficientemente simple como para ejecutarla en un solo servidor, no un clúster.

Docker Compose

Podrías estar desplegando en un solo servidor (no un clúster) con Docker Compose, así que no tendrías una forma fácil de gestionar la replicación de contenedores (con Docker Compose) mientras se preserva la red compartida y el balanceo de carga.

Entonces podrías querer tener un solo contenedor con un gestor de procesos que inicie varios procesos worker dentro.


El punto principal es que ninguna de estas son reglas escritas en piedra que debas seguir a ciegas. Puedes usar estas ideas para evaluar tu propio caso de uso y decidir cuál es el mejor enfoque para tu sistema, revisando cómo gestionar los conceptos de:

  • Seguridad - HTTPS
  • Ejecución al inicio
  • Reinicios
  • Replicación (el número de procesos en ejecución)
  • Memoria
  • Pasos previos antes de iniciar

Memoria

Si ejecutas un solo proceso por contenedor tendrás una cantidad de memoria consumida por cada uno de esos contenedores más o menos bien definida, estable y limitada (más de uno si están replicados).

Y luego puedes establecer esos mismos límites y requisitos de memoria en tus configuraciones para tu sistema de gestión de contenedores (por ejemplo en Kubernetes). De esa manera podrá replicar los contenedores en las máquinas disponibles teniendo en cuenta la cantidad de memoria que necesitan y la cantidad disponible en las máquinas del clúster.

Si tu aplicación es simple, esto probablemente no será un problema, y podría no ser necesario especificar límites estrictos de memoria. Pero si estás usando mucha memoria (por ejemplo con modelos de machine learning), deberías revisar cuánta memoria estás consumiendo y ajustar el número de contenedores que se ejecutan en cada máquina (y quizás añadir más máquinas a tu clúster).

Si ejecutas múltiples procesos por contenedor tendrás que asegurarte de que el número de procesos iniciados no consuma más memoria de la que está disponible.

Pasos Previos Antes de Iniciar y Contenedores

Si estás usando contenedores (por ejemplo Docker, Kubernetes), entonces hay dos enfoques principales que puedes usar.

Múltiples Contenedores

Si tienes múltiples contenedores, probablemente cada uno ejecutando un solo proceso (por ejemplo, en un clúster de Kubernetes), entonces probablemente querrás tener un contenedor separado que haga el trabajo de los pasos previos en un solo contenedor, ejecutando un solo proceso, antes de ejecutar los contenedores worker replicados.

Nota

Si estás usando Kubernetes, esto probablemente sería un Init Container.

Si en tu caso de uso no hay problema en ejecutar esos pasos previos múltiples veces en paralelo (por ejemplo si no estás ejecutando migraciones de base de datos, sino solo comprobando si la base de datos ya está lista), entonces también podrías simplemente ponerlos en cada contenedor justo antes de iniciar el proceso principal.

Contenedor Único

Si tienes una configuración simple, con un solo contenedor que luego inicia múltiples procesos worker (o también un solo proceso), entonces podrías ejecutar esos pasos previos en el mismo contenedor, justo antes de iniciar el proceso con la aplicación.

Imagen Base Docker

Solía haber una imagen Docker oficial de FastAPI: tiangolo/uvicorn-gunicorn-fastapi. Pero ahora está deprecada. ⛔️

Probablemente no deberías usar esta imagen Docker base (ni ninguna otra similar).

Si estás usando Kubernetes (u otros) y ya estás configurando la replicación a nivel de clúster, con múltiples contenedores. En esos casos, es mejor construir una imagen desde cero como se describió anteriormente: Construir una Imagen Docker para FastAPI.

Y si necesitas tener múltiples workers, puedes simplemente usar la opción de línea de comandos --workers.

Detalles Técnicos

La imagen Docker fue creada cuando Uvicorn no soportaba gestionar y reiniciar workers muertos, así que era necesario usar Gunicorn con Uvicorn, lo cual añadía bastante complejidad, solo para que Gunicorn gestionara y reiniciara los procesos worker de Uvicorn.

Pero ahora que Uvicorn (y el comando fastapi) soportan usar --workers, no hay razón para usar una imagen Docker base en lugar de construir la tuya propia (es prácticamente la misma cantidad de código 😅).

Desplegar la Imagen de Contenedor

Después de tener una Imagen de Contenedor (Docker) hay varias formas de desplegarla.

Por ejemplo:

  • Con Docker Compose en un solo servidor
  • Con un clúster de Kubernetes
  • Con un clúster en Docker Swarm Mode
  • Con otra herramienta como Nomad
  • Con un servicio en la nube que toma tu imagen de contenedor y la despliega

Imagen Docker con uv

Si estás usando uv para instalar y gestionar tu proyecto, puedes seguir su guía de Docker para uv.

Resumen

Usando sistemas de contenedores (por ejemplo con Docker y Kubernetes) se vuelve bastante sencillo manejar todos los conceptos de despliegue:

  • HTTPS
  • Ejecución al inicio
  • Reinicios
  • Replicación (el número de procesos en ejecución)
  • Memoria
  • Pasos previos antes de iniciar

En la mayoría de los casos, probablemente no querrás usar ninguna imagen base, y en su lugar construir una imagen de contenedor desde cero basada en la imagen Docker oficial de Python.

Cuidando el orden de las instrucciones en el Dockerfile y la caché de Docker puedes minimizar los tiempos de construcción, para maximizar tu productividad (y evitar el aburrimiento). 😎