← Blog

Desplegar FastAPI en Hetzner

Railyard

6 de julio de 2026 · 7 min de lectura

Sigue esto en tu propio servidor. Empieza gratis, sin tarjeta

uvicorn main:app --reload es cómo corre una app FastAPI en una laptop; lo que necesita para producción es un gestor de procesos real, una base de datos que sobreviva a un reinicio, migraciones que corran antes de que el código nuevo entre en vivo, y HTTPS al frente. Nada de eso es específico de FastAPI exactamente, pero los detalles difieren lo suficiente de un despliegue de Flask o Django, un servidor ASGI en vez de WSGI, Alembic en vez de las migraciones integradas de Django, como para que copiar un Dockerfile de Python genérico suele dejar algo a medio configurar. Esta guía construye lo real en Docker en un servidor de Hetzner Cloud, con Caddy manejando los certificados, y termina con una app FastAPI corriendo en tu propio dominio, que sabrás cómo actualizar y avanzar.

Lo que necesitas

  • Un servidor de Hetzner Cloud. Un CX23 (2 vCPU, 4 GB de memoria) a €5.99/mes sin IVA corre cómodamente una sola app FastAPI con su base de datos Postgres al lado; sube a un CX33 (4 vCPU, 8 GB, €8.99/mes) si la app también hace trabajo de fondo real.
  • Un dominio, con el DNS ya apuntando al servidor antes de pedir HTTPS.
  • Una cuenta de Hetzner Cloud y un token de API (Security → API Tokens, Read & Write) si quieres aprovisionar el servidor vía la API en vez de la consola.
  • Una app FastAPI en un repositorio Git. Esta guía usa FastAPI 0.161.1, que requiere Python 3.10 o más reciente; el Dockerfile de abajo fija Python 3.13.
  • Docker y el plugin de Compose, ya sea localmente para probar el build primero o directamente en el servidor.

Paso 1: crea el servidor

En Hetzner Cloud Console, crea un proyecto si hace falta, luego Add Server: elige Ubuntu 24.04, el tipo de vCPU compartida CX23, una ubicación (Núremberg, Falkenstein y Helsinki quedan todas en la UE), agrega tu clave SSH, y créalo.

ssh root@your-server-ip

Deberías llegar a un shell root sin pedir contraseña.

Paso 2: instala Docker

curl -fsSL https://get.docker.com | sh
docker --version
docker compose version

Paso 3: escribe el Dockerfile

FROM python:3.13-slim

WORKDIR /app

RUN apt-get update \
    && apt-get install -y --no-install-recommends build-essential libpq-dev \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

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

fastapi run es el comando de producción que los documentos oficiales de FastAPI recomiendan; corre Uvicorn por debajo sin un comando separado que recordar. libpq-dev y build-essential están ahí para psycopg, que necesita compilar contra la librería cliente de Postgres; quítalos si tu requirements.txt usa psycopg[binary] o asyncpg en su lugar, ya que ambos vienen con wheels precompiladas.

Un requirements.txt mínimo para esta guía:

fastapi[standard]>=0.161
psycopg[binary]>=3.2
sqlalchemy>=2.0
alembic>=1.13

Constrúyelo una vez para confirmar que compila limpio:

docker build -t my-fastapi-app .

Paso 4: escribe el archivo Compose

services:
  app:
    build: .
    restart: unless-stopped
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    expose:
      - "8000"

  db:
    image: postgres:18-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 5s
      timeout: 5s
      retries: 5

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      - app

volumes:
  db-data:
  caddy-data:
  caddy-config:

Tu archivo .env en el servidor:

DB_NAME=app
DB_USER=app
DB_PASSWORD=change-this
DATABASE_URL=postgresql+psycopg://app:change-this@db:5432/app

Valida antes de arrancar:

docker compose config

Paso 5: escribe el Caddyfile

api.example.com {
    reverse_proxy app:8000
}

Paso 6: primer arranque y migraciones

docker compose up -d
docker compose exec app alembic upgrade head
docker compose ps

Deberías ver app, db y caddy todos como running, y https://api.example.com debería responder con un certificado válido. Los docs interactivos de FastAPI viven en /docs por defecto; visitarlo debería mostrar los endpoints reales de tu API.

Paso 7: despliega nuevas versiones

git pull
docker compose build app
docker compose up -d --no-deps app
docker compose exec app alembic upgrade head

Reconstruir solo app deja db y caddy intactos, así que el certificado y la base de datos siguen corriendo durante el despliegue. Correr la migración después de que el contenedor nuevo esté arriba, en vez de durante el build, hace que un fallo de migración aparezca como un error de comando claro en vez de una imagen a medio desplegar.

Respaldos

Postgres en el contenedor db es lo único con estado en este stack que no es reproducible desde Git. Programa un dump diario fuera del servidor:

docker compose exec -T db pg_dump -U app app | gzip > backup-$(date +%F).sql.gz

Copia ese archivo a almacenamiento de objetos u otra máquina con una tarea cron, y restáuralo periódicamente en una base de datos de prueba para confirmar que de verdad sirve antes de necesitarlo en serio.

El atajo: hazlo con Railyard

Desplegar FastAPI en Hetzner corre la misma receta desde una URL de repositorio, sin que escribas el Dockerfile, el archivo Compose o el Caddyfile tú mismo. Conecta un servidor con un token de Hetzner Cloud Console, o trae uno que ya hayas aprovisionado. Railyard detecta FastAPI desde requirements.txt o pyproject.toml, sirve main:app en $PORT con uvicorn (apuntado automáticamente a tu propio módulo si la app vive en otro lugar), y reconoce psycopg, asyncpg o SQLAlchemy como dependencia para aprovisionar Postgres con DATABASE_URL inyectada. Define alembic upgrade head como comando de release bajo Settings → Deploy para migrar en cada release, igual que el paso 6 de arriba pero corrido automáticamente antes de que cada versión nueva reciba tráfico. Cualquier proceso de un Procfile se vuelve un worker que puedes escalar por separado del proceso web. Mira los docs de desplegar desde un repositorio para las reglas exactas de detección.

Solución de problemas

  • connection refused en el primer arranque: app arrancó antes de que Postgres terminara de inicializarse. depends_on con una condición de healthcheck (como en el paso 4) debería prevenir esto, pero si quitas el healthcheck, agrega en su lugar un bucle corto de reintento alrededor del primer comando de migración en vez de asumir que la base está lista apenas arranca su contenedor.
  • Errores 500 sin detalle en la respuesta: FastAPI no expone los tracebacks en las respuestas por defecto fuera de herramientas de depuración; revisa docker compose logs app para la excepción real.
  • ModuleNotFoundError para psycopg en runtime pese a un build limpio: usualmente un desajuste entre psycopg (la variante pura en Python) y psycopg[binary] o psycopg2-binary a través de ediciones de requirements.txt; fija uno explícitamente en vez de dejar que un lockfile desactualizado resuelva uno distinto al previsto.
  • El HTTPS nunca se emite, Caddy sigue reintentando: el DNS todavía no apunta al servidor. dig +short api.example.com debería coincidir con la IP del servidor.
  • Alembic dice “Target database is not up to date” al desplegar: la migración de un despliegue anterior no corrió, a menudo porque el comando de migración del paso 7 se saltó manualmente una vez. Corre docker compose exec app alembic current para ver dónde está la base de datos realmente contra alembic heads para dónde el código espera que esté.

Preguntas frecuentes

¿Por qué fastapi run en vez de llamar a uvicorn directamente? Hacen lo mismo por debajo; fastapi run es el comando que los documentos de FastAPI recomiendan para producción en las versiones que cubre esta guía, ya que elige valores por defecto sensatos sin banderas adicionales.

¿Debería usar varios workers de Uvicorn en un solo contenedor? Los documentos de despliegue de FastAPI lo desaconsejan para cualquier cosa corriendo bajo Docker Compose, Kubernetes, o similar: deja que el orquestador corra varias réplicas de contenedor en vez de un solo contenedor manejando varios procesos worker, ya que las réplicas se pueden reiniciar, escalar y verificar de forma independiente.

¿Postgres o algo más? SQLAlchemy y la mayoría del ecosistema de FastAPI son agnósticos de base de datos; esta guía usa Postgres por ser el emparejamiento más común, pero cambiar la imagen db y el esquema de DATABASE_URL funciona igual para MySQL.

¿De verdad alcanza un CX23? Para una sola API sirviendo volúmenes de tráfico típicos, sí; 4 GB se reparten cómodamente entre el contenedor de la app y Postgres. Trabajo de fondo pesado en CPU (procesamiento de datos grandes, inferencia de ML) es una razón para subir de tamaño.

¿Y si mi app usa una app factory en vez de una variable app a nivel de módulo? Apunta el CMD del Dockerfile o el comando detectado por Railyard explícitamente a ella, por ejemplo fastapi run app/main.py --port 8000 se vuelve la llamada equivalente contra un nombre de función factory si así está estructurada tu app; el ejemplo de esta guía asume una instancia app común.

¿Funciona con endpoints async y sync mezclados en una misma app? Sí; FastAPI corre las operaciones de ruta síncronas en un pool de hilos automáticamente para que no bloqueen el loop de eventos, lo cual es parte de por qué un solo proceso de Uvicorn maneja una mezcla de ambos sin configuración extra de tu parte.

Una app FastAPI en un servidor de Hetzner que aprovisionaste tú mismo cuesta €5.99 al mes, con cada dependencia, versión de Python y paso de migración explícito en tres archivos que controlas. Desplegar una versión nueva una vez que existen es reconstruir un contenedor y correr un comando de migración, nada oculto en el medio.

Por qué eligen Railyard para esto

  • Tu propia cuenta en la nube. Despliega en DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, o cualquier servidor Linux por SSH, facturado directamente por tu proveedor, sin márgenes adicionales.
  • Un precio fijo por equipo. El plan Free es gratis para siempre: un servidor más un servidor de base de datos, tres apps, sin tarjeta. Pro cuesta $19 al mes por hasta cinco servidores y apps ilimitadas. Team cuesta $49 al mes con servidores ilimitados. Sin cargos por app, por despliegue, por asiento ni por gigabyte.
  • Plantillas de un clic. Despliega apps de código abierto, como bases de datos, analítica y herramientas de desarrollo, en tu propio servidor con un clic.
  • Bases de datos administradas. Postgres, MySQL y Redis, con copias de seguridad programadas y verificadas por restauración, y recuperación a un punto en el tiempo.
  • Despliegue con git push. Despliega desde GitHub, GitLab o la CLI, con HTTPS automático y lanzamientos sin tiempo de inactividad.

Sáltate la configuración del servidor.

Sube un repositorio FastAPI a Railyard y aprovisiona Postgres, corre las migraciones, y despliega en cada push.

Plan gratis, sin tarjeta, y 14 días de Pro para cada equipo nuevo. Un precio fijo por equipo.