El ciclo local
jfast dev
Levanta los contenedores y espera a que estén sanos, aplica las migraciones, y recién entonces arranca la API, el worker de la cola y el frontend. Un comando para las cosas que uno hace cada mañana, en el orden que hace que las fallas aparezcan donde corresponde.
Qué hace, en orden
| Etapa | Qué corre | Si no puede |
|---|---|---|
| 1. Infraestructura | docker compose up -d <db> <cache>, y espera health |
dice por qué, y para |
| 2. Migraciones | alembic upgrade head |
para |
| 3. API | uvicorn main:app --reload --no-proxy-headers |
— |
| 4. Worker | jfast worker, cuando el plugin queue está activo |
dice por qué, y sigue |
| 5. Frontend | npm run dev en el proyecto del frontend |
dice por qué, y sigue |
El worker está porque la cola viene activada en todo servicio generado, y una
cola que nadie consume son jobs acumulándose en jfast_jobs -- y suscriptores
de eventos que nunca corren -- mientras la API responde 201. Levanta el mismo
main:app, así que corre los mismos @task y @subscribe de los módulos.
No recarga: reinicia jfast dev después de cambiar una task o un
suscriptor.
--no-proxy-headersno es adorno. uvicorn trae su propio resolutor de cabeceras forwarded encendido, confiando en127.0.0.1-- que es justo lo que bindeaserve-- y reescribe la dirección del cliente antes de que corra cualquier middleware, así quetrusted_proxiesnunca llega a decidir.jfast devyjfast servela pasan por ti.
Cada etapa se puede saltar, y cada salto se anuncia:
jfast dev --no-infra # the containers are already up
jfast dev --no-migrate # you are mid-migration and know it
jfast dev --no-web # backend only
jfast dev --no-worker # no queue worker (run `jfast worker` yourself)
jfast dev --port 9000 # override the port in jfast.toml
Sin Docker en la máquina, sin compose, sin alembic.ini, sin frontend: cada
caso degrada a una línea impresa y el resto igual corre. Un comando de
desarrollo que se niega a arrancar porque falta la mitad opcional es un comando
que la gente deja de usar.
Por qué una migración fallida detiene todo
Es el único hard stop, y a propósito.
Un servidor arrancado contra un esquema atrasado no falla al arrancar. Falla después, en una petición que no tiene nada que ver con la columna que falta, y con un error que nombra una tabla en vez de la migración que nadie corrió. La media hora que eso cuesta vale más que la comodidad de arrancar igual.
✗ alembic upgrade head failed:
(psycopg.errors.UndefinedColumn) column "tenant_id" does not exist
Arréglalo, o pasa --no-migrate y hazte cargo.
La traducción del .env
Esta es la parte que vale entender, porque es invisible cuando funciona.
El .env generado está escrito para compose, donde los servicios se
direccionan por nombre y compose interpola la contraseña:
JFAST_DB_DSN=postgresql+asyncpg://app:${SHOP_DATABASE_PASSWORD}@shop-database:5432/app
Las dos mitades son falsas para un proceso corriendo en tu máquina.
shop-database resuelve en la red de compose y en ningún otro lado, y nadie
interpola ${SHOP_DATABASE_PASSWORD} para un uvicorn pelado — el DSN llegaría
a asyncpg con las llaves puestas.
Así que jfast dev reescribe las dos antes de correr nada:
JFAST_DB_DSN=postgresql+asyncpg://app:s3cr3t@localhost:9431/app
El puerto del host sale de lo que compose realmente publica, leído del archivo de compose. El mismo entorno va a Alembic y al servidor, porque Alembic corre primero — poner la traducción solo en el servidor hace que la migración falle con un error de DNS nombrando un host que nunca iba a resolver ahí.
Lo que no puede resolver lo deja tal cual. Una suposición equivocada sería más
difícil de depurar que el valor original. Una variable que ya está puesta en tu
shell también se respeta, igual que pydantic-settings la prefiere sobre el
.env.
La traducción no es solo de jfast dev. jfast serve, jfast worker y
jfast migration check/plan la aplican cuando encuentran el archivo de compose de la misma forma (en el
directorio del servicio o un nivel arriba), y jfast exec -- <comando> corre
cualquier otra cosa con ella -- jfast exec -- alembic revision --autogenerate
-m "add invoices", jfast exec -- pytest. Dentro de un contenedor no se
traduce nada: ahí los nombres de compose resuelven y localhost es el propio
contenedor. La imagen de producción de todos modos no tiene un compose que
encontrar (.dockerignore lo excluye, y el del workspace queda fuera del
contexto de build).
Cómo se detiene
Ctrl-C detiene todo lo que arrancó. SIGTERM también — de timeout, de un
supervisor, de cerrar la terminal.
Lo segundo necesitó su propio handler, y vale saberlo si alguna vez tocas esto.
Los hijos se ponen a propósito en su propio grupo de procesos, para que un
Ctrl-C en la terminal no les llegue directo y el padre pueda bajarlos en orden.
Pero la disposición por defecto de SIGTERM mata al intérprete de una — sin
finally, sin limpieza — y los hijos, al estar en otro grupo, habrían
sobrevivido como huérfanos aguantando los puertos. El siguiente jfast dev
falla entonces con address already in use y una cacería confusa.
scripts/smoke_dev.sh lo verifica: arranca, manda SIGTERM, cuenta lo que
quedó.
dev contra serve
jfast serve |
jfast dev |
|
|---|---|---|
| Backend | sí | sí |
| Contenedores | no | los levanta |
| Migraciones | no | las aplica |
| Worker de la cola | no (jfast worker) |
lo arranca |
| Frontend | no | lo arranca |
.env traducido para el host |
sí | sí |
serve es la herramienta chica y se queda así: un proceso, sin efectos
secundarios, nada arrancado que después tengas que acordarte de bajar. Úsalo
cuando la base ya está corriendo y quieres un servidor y nada más.
Todo en contenedores
(cd shop-web && npm install && npm run build)
docker compose up --build
Que es lo que corre en producción. El frontend es lo único que se construye en
el host: Caddy sirve <frontend>/dist, donde escribe npm run build, y no
corre ningún contenedor de Node. El archivo de compose que escribe jfast start está completo — el .env del
workspace con las contraseñas generadas y el .env de cada servicio se
escriben junto a él, así que esto funciona en un clon limpio sin pasos extra.
Usa jfast dev cuando quieres reload y un debugger. Usa compose cuando
quieres saber que funciona como se va a desplegar.