A Dev.to post describes Fitz’s approach to reducing the “last 20%” of service setup—health checks, secret handling, observability, and container/deployment wiring—by embedding common production concerns into the language and its tooling. The article claims Fitz uses decorators for health checks, where functions annotated as readiness/liveness auto-mount to HTTP endpoints and return values determine HTTP status. It also describes “secrets as types,” arguing that representing sensitive values as an opaque Secret<T> type prevents accidental logging or serialization; redaction happens by default, and exposing the raw value requires an explicit method call. For observability, Fitz allegedly integrates with OpenTelemetry via a single OTEL_EXPORTER_OTLP_ENDPOINT environment variable, with trace context propagated into logs, and includes optional per-route opt-out. The post further describes structured logging with automatic trace correlation, feature flags implemented as configuration/compile-time defaults plus environment overrides, and a “fitz docker init” command that generates a multi-stage Dockerfile, docker-compose.yml (including Postgres and health checks), and .dockerignore by analyzing the program’s AST. Finally, it outlines “fitz deploy” as a wrapper for docker build/push and docker compose up, while stating that deploy targets like Fly.io, Railway, and Kubernetes are not included in the MVP.
Fitz proposes making deployment features part of the language workflow
A Dev.to post describes Fitz’s approach to reducing the “last 20%” of service setup—health checks, secret handling, observability, and container/deployment wiring—by embedding common production concer...
- Fitz uses decorators to define liveness/readiness health checks that map to HTTP endpoints, with status derived from returned boolean values.
- Sensitive configuration is modeled as a Secret<T> opaque type that redacts values in logs and JSON by default and requires explicit exposure to reveal it.
- Observability is connected to OpenTelemetry using OTEL_EXPORTER_OTLP_ENDPOINT, with HTTP requests exported as spans and trace IDs propagated into logs.
- fitz docker init generates a multi-stage Dockerfile, docker-compose.yml (including Postgres and /healthz checks), and a default .dockerignore by AST-based detection of program decorators.
- fitz deploy is described as a thin wrapper around docker build/push for images and docker compose up for local stacks; the post says Fly.io/Railway/Kubernetes targets are not part of the MVP.
Un recorrido por la historia de deployment de Fitz — healthchecks, secrets como tipos opacos, observability con OpenTelemetry, Dockerfiles autogenerados y fitz deploy. Production-ready no es una checklist, es sintaxis. La historia de deployment que la mayoría de los lenguajes no cuenta El primer 80% de un servicio es divertido: rutas, types, lógica de negocio, tests. El último 20% es la parte que efectivamente entrega la cosa, y ahí es donde todo el mundo pega con cinta cinco herramientas distintas: Una librería de healthcheck estilo psutil porque Kubernetes quiere /healthz. python-decouple o pydantic-settings para env vars, más tu propia clase Secret que con suerte no termina en logs. opentelemetry-instrumentation-fastapi más opentelemetry-exporter-otlp-proto-http más opentelemetry-instrumentation-sqlalchemy más cualquiera sea la combinación correcta de versiones este mes. Un Dockerfile copiado de un post de blog, con tres cosas mal para tu setup. Un docker-compose.yml que está bien excepto por el env var que tiene que venir de .env.production. Un Makefile o justfile con los comandos reales, o un YAML de CI que hace el equivalente. Llevo diez años haciendo esto en cada proyecto. Es la parte donde el lenguaje deja de ayudarte. Fitz se niega a hacer eso. Deployment está en el lenguaje. Acá está el stack completo de producción en Fitz, y qué reemplaza cada pieza. Health checks como decoradores @server(43928) fn main() => 0 @healthz fn liveness() -> Bool => true @readyz async fn readiness(db: DbConn) -> Bool { return match db.exec("SELECT 1").await { Ok(_) => true, Err(_) => false, } } /healthz y /readyz se auto-montan en el router HTTP. Nada para importar, ninguna librería para configurar. El return value de la función determina el HTTP status (200 si true, 503 si false). Kubernetes contento. El compilador también hace cumplir el shape: la función retorna Bool o Result<Bool>, toma un DbConn si lo necesita (el runtime lo inyecta). Si te olvidás del @readyz entero, el endpoint de readiness simplemente no existe — no hay librería que "casi configures" mal. En Python: # requirements.txt: starlette-healthcheck, pyhealthcheck, ... from healthcheck import HealthCheck health = HealthCheck() def db_check(): try: db.execute("SELECT 1") return True, "ok" except Exception as e: return False, str(e) health.add_check(db_check) app.add_route("/healthz", health.run) # acordate de montarlo En Fitz: @readyz async fn readiness(db: DbConn) -> Bool { return match db.exec("SELECT 1").await { Ok(_) => true, Err(_) => false } } Eso es todo. Misma semántica, sin librería, sin paso de mounting, sin docs que tenés que volver a leer. Secrets como tipos opacos El bug que más vi en código de producción: alguien loguea la password, la API key, el JWT secret. El logger entrega a Loki / Splunk / Sentry / lo que sea. El secret está ahora en logs de producción. Todo el mundo está de acuerdo en que esto es malo. Nadie tiene una buena respuesta en las librerías estándar — os.environ["FOO"] es solo un string. pydantic.SecretStr existe pero tenés que opt-in en todos lados y la gente se olvida. Fitz hace de los secrets un tipo de primera clase: let JWT_SECRET: Secret<Str> = secret("JWT_SECRET") let DB_URL: Secret<Str> = secret("DATABASE_URL") let LOG_LEVEL: Str = config("LOG_LEVEL", "info") Tres cosas siguen del hecho de que Secret<T> sea un tipo: print(JWT_SECRET) imprime `""`*. El trait Display redacta el valor. La serialización JSON redacta. log.info("config", { jwt: JWT_SECRET }) envía "jwt": "***" a tu sistema de observability. Hay exactamente una forma de exponer el valor: JWT_SECRET.expose(). Explícita, grepeable. El code review puede auditar cada call site en segundos. config("LOG_LEVEL", "info") es el hermano no-secret. Mismo lookup de env var, mismo default, tipo Str plano — sin redacción porque no es sensible. El sistema de tipos hace la distinción obvia en lugar de depender de convenciones de nombres. Combinado con el migrator (Parte 2), los secrets de Postgres viven en Secret<Str> desde el momento en que entran al binario hasta que se entregan al driver. No hay variable string con la password dando vueltas. Observability con un solo env var Tracing y métricas son las partes de observability de producción que todo el mundo quiere y nadie quiere configurar. El SDK Python de OpenTelemetry actualmente te pide que: Instales opentelemetry-api, opentelemetry-sdk, el exporter para tu protocolo, y un paquete de instrumentation por cada librería que uses. Configures el tracer provider, el meter provider, los resource attributes, el sampler. Lo enganches en un hook de startup. Esperes que ninguna de las versiones de esos paquetes se haya roto entre releases de Python. En Fitz: OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318 ./mybin Esa es la integración. Cada request HTTP abre un span que exporta a OTLP. El span lleva http.method, http.target (template de la ruta, no path con params), http.status_code, duration_ms. El trace_id y span_id se propagan a cada log.info(...) adentro del handler — cuando grepeás Jaeger por trace_id, encontrás cada log line relacionado en todos los servicios al instante. Cuando el env var no está seteado: cero overhead, cero llamadas de red, ninguna tarea de exporter corriendo. Podés opt-out por ruta: @server(observability=false) fn main() => 0 Podés agregar spans explícitos adentro de la lógica de negocio: @trace(name="process_order") @metric(name="orders") async fn process(order: Order) -> Result<Receipt> { let validated = validate(order)? let charged = charge(validated).await? return Ok(receipt_for(charged)) } @trace abre un tracing::info_span!. @metric registra un histograma <name>_duration_seconds y un counter <name>_calls_total, populados al hacer drop del scope de la función — funciona con paths return explícitos, sin código muerto. Las métricas también se exponen en /metrics (formato Prometheus) si lo activás: @server(prometheus=true) fn main() => 0 Para tiendas OpenTelemetry-native, el exporter OTLP envía también las métricas — ambos backends funcionan. Logs estructurados con auto-correlación log.info("user.signup", { user_id: user.id, email: user.email, plan: "free", }) Eso emite una línea JSON a stderr con timestamp, level, msg, los fields explícitos, y automáticamente el trace_id/span_id del request HTTP actual. Sin llamada a tracer.get_current_span(). El wrapper HTTP setea el contexto; el logger lo lee. Correlacionás logs y traces por trace_id sin pensar en eso. Si valores Secret<T> aparecen en los kwargs, se redactan antes de serializar. No hay forma de loguear accidentalmente un secret salvo escribir .expose() explícito. Pretty-print en dev, JSON en producción: $ ./mybin 2026-06-05 14:23:11 INFO http.access method=GET target=/users/{id} status=200 duration_ms=12 trace_id=ab12cd34... 2026-06-05 14:23:11 INFO user.lookup user_id=42 found=true $ FITZ_LOG_FORMAT=json ./mybin {"timestamp":"2026-06-05T14:23:11Z","level":"INFO","msg":"http.access","method":"GET","target":"/users/{id}","status":200,"duration_ms":12,"trace_id":"ab12cd34..."} El happy path de Loki/Datadog/Splunk es el mismo JSON estés usando OpenTelemetry o no. Ningún agente re-parsea prefijos. Feature flags como decorador Los feature flags estilo unleash/launchdarkly son normalmente una llamada de servicio por evaluación. Para la mayoría de los proyectos, la respuesta correcta es mucho más simple: una flag en tu config, un override por env var, un 404 si la flag está off. @flag("new-checkout") @post("/v2/checkout") fn v2_checkout(body: Cart) -> Receipt { ... } Dos fuentes: Sección [flags] en fitz.toml — defaults compile-time horneados al binario. Env vars FITZ_FLAG_<NAME> — override runtime sin recompilar. Default es false (fail-safe). Cuando la flag está off, el handler HTTP/WS retorna 404 — y la ruta está gateada antes de que corran los middlewares y la auth, así que no gastás ciclos en algo que el user no puede alcanzar de todas formas. Adentro de la lógica de negocio: if flag("show-experimental-banner") { show_banner() } flags.list() enumera flags conocidas (config + env). flags.is_enabled("name") es el alias. El modelo no está tratando de reemplazar a LaunchDarkly. Está tratando de eliminar la excusa para commitear if user.id == 42 y gatear features para testing. Servicio de feature flags real cuando necesitás targeting, porcentajes, audit log. Flags built-in cuando solo querés un kill switch. Dockerfile generado de tu AST fitz docker init Lee tu main.fitz y escribe tres archivos: Dockerfile — multi-stage. El builder usa la imagen toolchain de Fitz. La stage de runtime es gcr.io/distroless/cc-debian12 (o python:3.12-slim-bookworm si hay un from python import ... en tu código — distroless no puede hostear CPython). .dockerignore — defaults que matchean los smell tests (target/, .git/, .env*, __pycache__/). docker-compose.yml — tu app más la infraestructura que necesita. db.connect(...) en tu código agrega postgres:16-alpine con healthcheck y volumen pgdata. @server(8080) setea EXPOSE 8080. @cron agrega restart: unless-stopped. Healthcheck contra /healthz porque hay un decorador @healthz. La detección es AST-only (~50ms). No ejecuta tu programa, no toca el disco, no probe ports. Lee el árbol sintáctico, busca decoradores y calls landmarks, llena el template correcto. Esta es la parte de deployment que tuve mal en cada proyecto: el Dockerfile que está casi bien pero no tiene libpq para Postgres, el compose que está casi bien pero montea el volumen equivocado. fitz docker init lo agarra bien la primera vez porque el AST le dice qué necesitás. Los archivos se commitean. Editalos cuando los necesites: $ fitz docker init # generar $ # editás Dockerfile, editás docker-compose.yml — son archivos normales $ git add Dockerfile docker-compose.yml .dockerignore $ git commit fitz docker build es el wrapper fino que corre docker build -t <pkg-name>:latest . con el working directory correcto. fitz deploy # Build de imagen y push al registry (saltá el push con --no-push). fitz deploy docker --tag mycorp/api:v1 # Levantá el stack compose local (con -d por default; --no-detach para foreground). fitz deploy compose Estos no son herramientas nuevas. Son wrappers finos sobre docker build/docker push y docker compose up. El punto no es inventar un sistema de deploy; el punto es que el comando de deploy existe en la misma toolchain que fitz build. No tenés que mantener un deploy.sh al lado de tu código. Targets en el MVP: docker y compose. Targets explícitamente fuera del MVP: fly, railway, k8s. Para esos, corré flyctl deploy / railway up / kubectl apply directo — ya son buenas herramientas, Fitz no necesita re-envolverlas. Si aparece demanda por un target específico más adelante, se puede agregar — el helper crate son ~430 líneas. Cómo se ve end-to-end Un servicio real en Fitz hoy: @server(8080, prometheus=true) fn main() => 0 let DB_URL: Secret<Str> = secret("DATABASE_URL") let JWT_SECRET: Secret<Str> = secret("JWT_SECRET") let db = db.connect(DB_URL.expose()) @healthz fn liveness() -> Bool => true @readyz async fn readiness() -> Bool { return match db.exec("SELECT 1").await { Ok(_) => true, Err(_) => false } } @table("users") type User { @primary id: Int, email: Str, name: Str, role: Str } @auth_provider fn check_token(headers: Map<Str, Str>) -> Result<User> { /* verify JWT */ } @authenticated @get("/me") fn me(user: User) -> User => user @trace(name="charge") @metric(name="charges") @requires("billing") @post("/charge") async fn charge(body: ChargeRequest, user: User) -> Result<Receipt> { log.info("charge.attempt", { user_id: user.id, amount: body.amount }) let receipt = stripe_charge(body).await? return Ok(receipt) } @cron("0 0 3 * * *", retry={max: 5, backoff: "exponential"}, store=db) async fn cleanup_expired_tokens() { auth.cleanup_expired(db).await? } Lo deployás: fitz docker init # genera Dockerfile + compose con postgres + healthcheck git add . && git commit -m "deploy setup" fitz deploy docker --tag mycorp/api:v1 En producción: docker run --rm \ -e DATABASE_URL=postgres://... \ -e JWT_SECRET=... \ -e OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 \ -p 8080:8080 \ mycorp/api:v1 Ese binario: Auto-montea /healthz, /readyz, /openapi.json, /docs, /metrics, /asyncapi.json. Exporta cada request HTTP como span OpenTelemetry con trace_id propagado a logs. Valida JWTs con passwords hasheadas Argon2id. Corre cleanup scheduled con retry persistente sobre las tablas fitz_cron_jobs/fitz_cron_runs. Redacta cada Secret<T> de logs, JSON responses, y fields estructurados. Maneja SIGTERM gracefully (drainando requests, después exit). Compila a ~18 MB de binario nativo. Escribiste ~50 líneas de código de negocio. El resto es el lenguaje haciendo lo que el lenguaje debería hacer. Lo que todavía no está acá (honesto) Te dije la verdad en la Parte 1: un solo dev. La historia de deployment tiene gaps conocidos: fitz deploy fly / fitz deploy railway / fitz deploy k8s no están construidos. Usá flyctl deploy o railway up o kubectl apply directo. Los CLIs nativos son excelentes — Fitz no necesita re-envolverlos hoy. Config de sidecar log shipping en el compose generado. Si querés Fluent Bit / Vector / Loki Promtail cableado, editá el compose a mano. El autogen cubre el shape del programa, no el backend de observability. Limits de CPU/memoria en compose. El MVP no los setea — deploys de producción (compose stack a un server) deberían agregar deploy.resources.limits. SBOM / firma de imagen. No se autogenera. La imagen es output de docker build. Firmá con cosign si lo necesitás. Todo lo demás de arriba está en v0.15.0 hoy, con tests, con paridad bit-a-bit fitz run ↔ fitz build, con ejemplos en los docs. Por qué importa El split 80/20 que describí al principio — 80% código divertido, 20% pegoteo de producción — es un impuesto sobre cada lenguaje diseñado antes de 2015. Los lenguajes que diseñaron para producción desde el día uno (Go es el ejemplo obvio) cambiaron otras cosas para llegar ahí. Fitz está tratando de mantener el feel gradual-typed, expression-rich, async-first de Python — y aún así taxar producción con cero peso extra. Si pasaste por una semana de deploy y sentiste "esto no debería requerir cinco pestañas de Stack Overflow", ese es el sentimiento al que estoy construyendo para eliminar. Probalo: # Linux / macOS / WSL curl -sSf https://thegreekman76.github.io/fitz/install.sh | sh # Windows (PowerShell) irm https://thegreekman76.github.io/fitz/install.ps1 | iex # Reabrí la terminal, después: fitz new mi-api-prod --http cd mi-api-prod # Editá main.fitz para sumar @healthz, @table, una ruta HTTP fitz docker init fitz deploy compose Para VSCode (recomendado — hover con tipos, autocomplete, signature help): bajá el fitz-lang-<plataforma>.vsix desde la página de releases y code --install-extension fitz-lang-<plataforma>.vsix --force. El Language Server viene incluido. Vas a tener un servicio con healthchecks, observability, y Docker compose en tu proyecto en menos de cinco minutos. Avisame qué se rompió. Repo: github.com/Thegreekman76/fitz Docs y curso: thegreekman76.github.io/fitz Capítulo de la guía sobre deployment: thegreekman76.github.io/fitz/guide/#35-deployment Roadmap: docs/roadmap.md CHANGELOG: CHANGELOG.md Issues: github.com/Thegreekman76/fitz/issues Hasta la próxima.
2 hours agoA walk through the deployment story of Fitz — healthchecks, secrets as opaque types, OpenTelemetry observability, autogenerated Dockerfiles, and fitz deploy. Production-ready isn't a checklist, it's syntax. The deployment story most languages don't tell The first 80% of a service is fun: routes, types, business logic, tests. The last 20% is the part that ships the thing, and that's where everyone tapes together five different tools: Some psutil-style healthcheck library because Kubernetes wants /healthz. python-decouple or pydantic-settings for env vars, plus your own Secret class that hopefully doesn't end up in logs. opentelemetry-instrumentation-fastapi plus opentelemetry-exporter-otlp-proto-http plus opentelemetry-instrumentation-sqlalchemy plus whatever the right combo of versions is this month. A Dockerfile copy-pasted from a blog post, with three things wrong for your setup. A docker-compose.yml that's right except for the env var that has to come from .env.production. A Makefile or justfile with the actual commands, or a CI YAML that does the equivalent. I've done this every project for ten years. It's the part where the language stops helping you. Fitz refuses to do that. Deployment is in the language. Here's the full production stack in Fitz, and what each piece replaces. Health checks as decorators @server(43928) fn main() => 0 @healthz fn liveness() -> Bool => true @readyz async fn readiness(db: DbConn) -> Bool { return match db.exec("SELECT 1").await { Ok(_) => true, Err(_) => false, } } /healthz and /readyz auto-mount on the HTTP router. There's nothing to import, no library to configure. The return value of the function determines the HTTP status (200 if true, 503 if false). Kubernetes is happy. The compiler also enforces shape: the function returns Bool or Result<Bool>, takes a DbConn if it needs one (the runtime injects it). If you forget @readyz entirely, the readiness endpoint just doesn't exist — there's no library to "almost configure" wrong. In Python: # requirements.txt: starlette-healthcheck, pyhealthcheck, ... from healthcheck import HealthCheck health = HealthCheck() def db_check(): try: db.execute("SELECT 1") return True, "ok" except Exception as e: return False, str(e) health.add_check(db_check) app.add_route("/healthz", health.run) # remember to mount it In Fitz: @readyz async fn readiness(db: DbConn) -> Bool { return match db.exec("SELECT 1").await { Ok(_) => true, Err(_) => false } } That's it. Same semantics, no library, no mount step, no docs you have to re-read. Secrets as opaque types The bug I've seen the most in production code: somebody logs the password, the API key, the JWT secret. The logger ships to Loki / Splunk / Sentry / whatever. The secret is now in production logs. Everyone agrees this is bad. Nobody has a good answer in stock libraries — os.environ["FOO"] is just a string. pydantic.SecretStr exists but you have to opt in everywhere and people forget. Fitz makes secrets a first-class type: let JWT_SECRET: Secret<Str> = secret("JWT_SECRET") let DB_URL: Secret<Str> = secret("DATABASE_URL") let LOG_LEVEL: Str = config("LOG_LEVEL", "info") Three things follow from Secret<T> being a type: print(JWT_SECRET) prints `""`*. The Display trait redacts the value. JSON serialization redacts. log.info("config", { jwt: JWT_SECRET }) ships "jwt": "***" to your observability system. There's exactly one way to expose the value: JWT_SECRET.expose(). Explicit, grep-able. Code review can audit every call site in seconds. config("LOG_LEVEL", "info") is the non-secret sibling. Same env var lookup, same default, plain Str type — no redaction because it's not sensitive. The type system makes the distinction obvious instead of relying on naming conventions. Combined with the migrator (Part 2), Postgres secrets live in Secret<Str> from the moment they enter the binary until they're handed to the driver. There's no string variable holding the password sitting around. Observability with one env var Tracing and metrics are the parts of production observability that everyone wants and nobody wants to set up. The Python OpenTelemetry SDK currently requires you to: Install opentelemetry-api, opentelemetry-sdk, the exporter for your protocol, and one instrumentation package per library you use. Configure the tracer provider, the meter provider, the resource attributes, the sampler. Wire it up in a startup hook. Hope none of the versions of those packages broke between Python releases. In Fitz: OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318 ./mybin That's the integration. Every HTTP request opens a span that exports to OTLP. The span carries http.method, http.target (route template, not path with params), http.status_code, duration_ms. The trace_id and span_id are propagated to every log.info(...) inside the handler — when you grep Jaeger by trace_id, you instantly find every related log line across services. When the env var is not set: zero overhead, zero network calls, no exporter task running. You can opt out per route: @server(observability=false) fn main() => 0 You can add explicit spans inside business logic: @trace(name="process_order") @metric(name="orders") async fn process(order: Order) -> Result<Receipt> { let validated = validate(order)? let charged = charge(validated).await? return Ok(receipt_for(charged)) } @trace opens a tracing::info_span!. @metric registers a <name>_duration_seconds histogram and a <name>_calls_total counter, populated on drop of the function scope — works with explicit return paths, no dead code. Metrics also expose at /metrics (Prometheus format) if you opt in: @server(prometheus=true) fn main() => 0 For OpenTelemetry-native shops, the OTLP exporter sends metrics too — both backends work. Structured logs with auto-correlation log.info("user.signup", { user_id: user.id, email: user.email, plan: "free", }) That emits a JSON line to stderr with timestamp, level, msg, the explicit fields, and automatically the trace_id/span_id of the current HTTP request. No tracer.get_current_span() call. The HTTP wrapper sets the context; the logger reads it. You correlate logs and traces by trace_id without thinking about it. If Secret<T> values appear in the kwargs, they're redacted before serialization. There is no way to accidentally log a secret short of writing .expose() explicitly. Pretty-printed in dev, JSON in production: $ ./mybin 2026-06-05 14:23:11 INFO http.access method=GET target=/users/{id} status=200 duration_ms=12 trace_id=ab12cd34... 2026-06-05 14:23:11 INFO user.lookup user_id=42 found=true $ FITZ_LOG_FORMAT=json ./mybin {"timestamp":"2026-06-05T14:23:11Z","level":"INFO","msg":"http.access","method":"GET","target":"/users/{id}","status":200,"duration_ms":12,"trace_id":"ab12cd34..."} The Loki/Datadog/Splunk happy path is the same JSON whether you're using OpenTelemetry or not. No agent re-parses prefixes. Feature flags as a decorator Feature flags from unleash/launchdarkly are usually a service call per evaluation. For most projects, the right answer is much simpler: a flag in your config, an env var override, a 404 if the flag is off. @flag("new-checkout") @post("/v2/checkout") fn v2_checkout(body: Cart) -> Receipt { ... } Two sources: [flags] section in fitz.toml — compile-time defaults baked into the binary. FITZ_FLAG_<NAME> env vars — runtime override without recompiling. Default is false (fail-safe). When the flag is off, the HTTP/WS handler returns 404 — and the route is gated before middleware and auth run, so you don't waste cycles on something the user can't reach anyway. Inside business logic: if flag("show-experimental-banner") { show_banner() } flags.list() enumerates known flags (config + env). flags.is_enabled("name") is the alias. The model isn't trying to replace LaunchDarkly. It's trying to remove the excuse for committing if user.id == 42 to gate features for testing. Real feature flag service when you need targeting, percentages, audit log. Built-in flags when you just want a kill switch. Dockerfile generated from your AST fitz docker init Reads your main.fitz and writes three files: Dockerfile — multi-stage. The builder uses the Fitz toolchain image. The runtime stage is gcr.io/distroless/cc-debian12 (or python:3.12-slim-bookworm if there's a from python import ... in your code — distroless can't host CPython). .dockerignore — defaults that match the smell tests (target/, .git/, .env*, __pycache__/). docker-compose.yml — your app plus the infrastructure it needs. db.connect(...) in your code adds postgres:16-alpine with a healthcheck and a pgdata volume. @server(8080) sets EXPOSE 8080. @cron adds restart: unless-stopped. Healthcheck against /healthz because there's an @healthz decorator. The detection is AST-only (~50ms). It doesn't run your program, doesn't poke at the disk, doesn't probe ports. It reads the syntax tree, looks for landmark decorators and calls, fills in the right template. This is the part of deployment that I've gotten wrong every project: the Dockerfile that's almost right but doesn't have the libpq for Postgres, the compose that's almost right but mounts the wrong volume. fitz docker init gets it right the first time because the AST tells it what you need. The files are committed. Edit them when you need to: $ fitz docker init # generate $ # edit Dockerfile, edit docker-compose.yml — these are normal files $ git add Dockerfile docker-compose.yml .dockerignore $ git commit fitz docker build is the thin wrapper that runs docker build -t <pkg-name>:latest . with the right working directory. fitz deploy # Build the image and push to a registry (skip the push with --no-push). fitz deploy docker --tag mycorp/api:v1 # Bring up the compose stack locally (with -d by default; --no-detach for foreground). fitz deploy compose These are not new tools. They're thin wrappers over docker build/docker push and docker compose up. The point isn't to invent a deploy system; the point is that the deploy command exists in the same toolchain as fitz build. You don't have to maintain a deploy.sh next to your code. Targets in the MVP: docker and compose. Targets explicitly not in the MVP: fly, railway, k8s. For those, run flyctl deploy / railway up / kubectl apply directly — they're already good tools, Fitz doesn't need to re-wrap them. If demand appears for a specific target later, it can be added — the helper crate is ~430 lines. What it looks like end-to-end A real service in Fitz today: @server(8080, prometheus=true) fn main() => 0 let DB_URL: Secret<Str> = secret("DATABASE_URL") let JWT_SECRET: Secret<Str> = secret("JWT_SECRET") let db = db.connect(DB_URL.expose()) @healthz fn liveness() -> Bool => true @readyz async fn readiness() -> Bool { return match db.exec("SELECT 1").await { Ok(_) => true, Err(_) => false } } @table("users") type User { @primary id: Int, email: Str, name: Str, role: Str } @auth_provider fn check_token(headers: Map<Str, Str>) -> Result<User> { /* JWT verify */ } @authenticated @get("/me") fn me(user: User) -> User => user @trace(name="charge") @metric(name="charges") @requires("billing") @post("/charge") async fn charge(body: ChargeRequest, user: User) -> Result<Receipt> { log.info("charge.attempt", { user_id: user.id, amount: body.amount }) let receipt = stripe_charge(body).await? return Ok(receipt) } @cron("0 0 3 * * *", retry={max: 5, backoff: "exponential"}, store=db) async fn cleanup_expired_tokens() { auth.cleanup_expired(db).await? } Deploy it: fitz docker init # generates Dockerfile + compose with postgres + healthcheck git add . && git commit -m "deploy setup" fitz deploy docker --tag mycorp/api:v1 In production: docker run --rm \ -e DATABASE_URL=postgres://... \ -e JWT_SECRET=... \ -e OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 \ -p 8080:8080 \ mycorp/api:v1 That binary: Auto-mounts /healthz, /readyz, /openapi.json, /docs, /metrics, /asyncapi.json. Exports every HTTP request as an OpenTelemetry span with trace_id propagated to logs. Validates JWTs with Argon2id-hashed passwords. Runs scheduled cleanup with persistent retry over the fitz_cron_jobs/fitz_cron_runs tables. Redacts every Secret<T> from logs, JSON responses, and structured fields. Handles SIGTERM gracefully (draining requests, then exit). Compiles to ~18 MB of native binary. You wrote ~50 lines of business code. The rest is the language doing what the language should do. What's not in here yet (honest) I told you the truth in Part 1: one developer. The deployment story has known gaps: fitz deploy fly / fitz deploy railway / fitz deploy k8s are not built. Use flyctl deploy or railway up or kubectl apply directly. The native CLIs are excellent — Fitz doesn't need to re-wrap them today. Sidecar log shipping config in the generated compose. If you want Fluent Bit / Vector / Loki Promtail wired in, edit the compose by hand. The autogen covers the program shape, not the observability backend. CPU/memory limits in compose. The MVP doesn't set them — production deploys (compose stack to a server) should add deploy.resources.limits. SBOM / image signing. Not auto-generated. The image is just docker build output. Sign with cosign if you need it. Everything else above is in v0.15.0 today, with tests, with bit-for-bit fitz run ↔ fitz build parity, with examples in the docs. Why it matters The 80/20 split I described at the top — 80% fun code, 20% production stapling — is a tax on every language designed before 2015. The languages that designed for production from day one (Go is the obvious example) traded other things to get there. Fitz is trying to keep the gradual-typed, expression-rich, async-first feel of Python — and still tax production with zero extra weight. If you've gone through a deploy week and felt "this should not require five Stack Overflow tabs", that's the feeling I'm building toward removing. Try it: # Linux / macOS / WSL curl -sSf https://thegreekman76.github.io/fitz/install.sh | sh # Windows (PowerShell) irm https://thegreekman76.github.io/fitz/install.ps1 | iex # Reopen the terminal, then: fitz new my-prod-api --http cd my-prod-api # Edit main.fitz to add @healthz, @table, an HTTP route fitz docker init fitz deploy compose For VSCode (recommended — hover with types, autocomplete, signature help): grab the fitz-lang-<platform>.vsix from the releases page and code --install-extension fitz-lang-<platform>.vsix --force. The Language Server is bundled. You'll have a service with healthchecks, observability, and Docker compose in your project in under five minutes. Let me know what broke. Repo: github.com/Thegreekman76/fitz Docs and course: thegreekman76.github.io/fitz Guide chapter on deployment: thegreekman76.github.io/fitz/guide/#35-deployment Roadmap: docs/roadmap.md CHANGELOG: CHANGELOG.md Issues: github.com/Thegreekman76/fitz/issues Until the next one.
2 hours ago
Amazon discount wave cuts prices on M5 MacBook Air, M5 Pro MacBook Pro, Apple Watch Series 11, AirPods
Multiple outlets report new Amazon markdowns across Apple’s lineup, led by MacBook deals on M5 models. MacRumors highlig...
Infinix launches HOT 70 series with 12-colorway Dynamic Shine design
Infinix introduces its HOT 70 series, presenting what the company describes as an industry-first Dynamic Shine Design. T...
Canadian privacy watchdog says Grok AI image tool violated privacy law
Canada’s privacy commissioner says the Grok AI chatbot’s image generation feature violated the country’s privacy law. Th...