TPTito Pinardo Gutiérrez
← Volver al rack
English
1U · LANDINGRust · Axum · PostgreSQL · 12 MB de RAM En producción

Posición 5 de 25 en el rack · En producción

Unidad 1U · Proyecto personal en producción

Esta web

Código público · CI en verde

Un único binario de Rust que lleva dentro las plantillas, los estilos, los textos y hasta el CV. Renderiza todas las páginas una vez al arrancar y las sirve desde memoria. Lo único que escribe cualquiera, el formulario, se guarda en PostgreSQL antes de avisarme.

Por qué Rust
Un binario pequeño, sin entorno que instalar, que usa unos 12 MB de RAM y se comprueba en compilación: las plantillas, las consultas SQL y los textos de los dos idiomas.
Dónde vive
En su propio contenedor sin Docker ni privilegios, el único del homelab que rechaza por defecto todo lo que entra. Solo Traefik puede hablar con él.
Qué hay detrás
Terraform crea el contenedor, Ansible lo configura y despliega, y el binario se niega a arrancar si el contenido tiene una errata.
  • ~12 MBde RAM en uso
  • 0dependencias en el navegador
  • 1regla de entrada: solo desde Traefik
  • 0 Bde IP guardada: solo un hash con sal

Esta web por dentro

Lo que estás leyendo ahora mismo sale de aquí. Elige un recorrido para ver qué pasa con tu visita o con un mensaje.

Hermesde visita en la unidad

Pulsa una pieza o elige un recorrido. Si vienes de una habilidad, ya están encendidas las piezas donde la uso.

      Tu visita
      1. Tú — Abres la web.
      2. Cloudflare — Cloudflare la recibe primero.
      3. Traefik — Traefik pone el certificado y la pasa al contenedor de la web.
      4. Servidor Axum — Axum recibe la petición. No hay que consultar nada: la página ya está hecha.
      5. Cabeceras de seguridad — Se añaden las cabeceras de seguridad.
      6. Páginas pre-renderizadas — Sale la página en tu idioma, renderizada al arrancar. Si ya la tenías, un 304 y listo.
      7. Tu navegador — Tu navegador la pinta y el JavaScript le añade el mapa, la terminal, los logros y el sonido.
      8. Tú — Y la estás leyendo.
      Me escribes por el formulario
      1. Tú — Rellenas el formulario y lo envías.
      2. Traefik — Entra por Traefik, como todo.
      3. Formulario de contacto — Se validan los campos. Si el campo trampa viene relleno, era un bot. Si cayó en las trampas para IA, llega igual, pero marcado.
      4. Límites de envío — Se comprueban los límites: por IP (guardada solo como hash) y globales.
      5. PostgreSQL — El mensaje se guarda en PostgreSQL. A partir de aquí no se pierde.
      6. Bandeja de salida — Queda un aviso pendiente en la bandeja de salida.
      7. Aviso a Matrix — Un trabajo coge el aviso y lo envía a Matrix.
      8. Mi servidor de Matrix — Matrix confirma con un identificador de evento y el aviso se marca como entregado.
      9. Mi móvil — Me llega al móvil.
      Matrix no responde
      1. Bandeja de salida — Hay un aviso pendiente.
      2. Aviso a Matrix — Se intenta enviar, pero Matrix no contesta.
      3. Bandeja de salida — El aviso sigue en la bandeja, con su intento contado. El mensaje ya está guardado.
      4. Aviso a Matrix — Al rato se reintenta. Si hay varios trabajadores, cada uno bloquea una fila distinta y nadie envía dos veces.
      5. Mi servidor de Matrix — Matrix responde y confirma: entregado.
      Publico una versión
      1. Repositorio en mi Forgejo — Hago push a mi Forgejo. Antes, un hook local busca secretos y nombres internos.
      2. Integración y despliegue continuos — El CI pasa formato, clippy, todas las pruebas y gitleaks.
      3. Repositorio en mi Forgejo — En verde, el runner de despliegue repite el control de fugas y compila la versión.
      4. Ansible — Ansible copia el binario nuevo al contenedor.
      5. Comprobación antes de reiniciar — Antes de reiniciar, ejecuta «landing check»: valida el contenido de los dos idiomas.
      6. Servicio endurecido — Reinicia el servicio y espera a que /healthz diga «ok», que comprueba también la base de datos.
      7. Servidor Axum — La versión nueva está sirviendo. Solo entonces el commit sale a GitHub.
      Monto el contenedor desde cero
      1. Terraform — Terraform crea el contenedor con su cortafuegos: entra SSH de administración y lo que venga de Traefik. Nada más.
      2. Ansible — Ansible instala y configura PostgreSQL.
      3. PostgreSQL — PostgreSQL sin escuchar en red.
      4. Sin red — La web se conecta por el socket local, con autenticación del sistema.
      5. Servicio endurecido — Ansible crea el servicio endurecido y lo arranca.
      Los LEDs del mapa
      1. Estado del homelab — Cada 5 minutos llega un resumen del estado del homelab. Si tiene más de 15 minutos, ni se publica.
      2. Servidor Axum — La web lo lee y lo sirve como un JSON pequeño.
      3. Páginas pre-renderizadas — La portada lo pide al cargar.
      4. Tu navegador — Y tu navegador enciende el LED de cada pieza. Si el dato tiene más de 2 horas, se oculta.
      1. Tú

        Lo que estás leyendo ahora.

      2. Cloudflare

        Delante del dominio: oculta la IP de casa y filtra.

      3. Traefik

        La puerta pública. Es lo único que puede hablar con el contenedor de la web.

        • Traefik
      4. Mi móvil

        Donde me llegan los mensajes del formulario.

      5. Repositorio en mi Forgejo

        El origen del código, en casa. El código es público en GitHub, pero esa copia solo recibe lo que ya está desplegado y ha pasado el control de fugas.

        • Git
        • Forgejo · Actions
      6. Integración y despliegue continuos

        En cada push, en un runner sin secretos: formato, clippy sin avisos, todas las pruebas contra un PostgreSQL de verdad y gitleaks. En verde, otro runner lo despliega.

        • Forgejo · Actions
        • GitLab CI · GitHub Actions
      7. Mi servidor de Matrix

        La sala donde llegan los avisos de mensajes nuevos, con un bot propio que solo puede escribir ahí.

      8. Servidor Axum

        Recibe la petición, busca la página ya renderizada y responde. Si el navegador ya la tiene, responde 304 sin cuerpo.

        • Rust
      9. Páginas pre-renderizadas

        Plantillas compiladas que se rellenan una vez al arrancar, en los dos idiomas, cada una con su ETag.

        • Rust
      10. Cabeceras de seguridad

        CSP estricta sin nada en línea, HSTS y el resto. Por eso el JavaScript solo mejora la página: todo funciona sin él.

        • Rust
      11. Contenido en TOML

        Textos, proyectos, piezas del laboratorio y estas unidades viven en ficheros TOML que se validan al arrancar. Una clave que falte en un idioma impide desplegar.

        • Rust
      12. Formulario de contacto

        Valida los campos, tiene un campo trampa para bots, trampas para reconocer a una IA y un captcha opcional.

        • Rust
      13. Límites de envío

        Por IP y globales, calculados con una sola consulta indexada. La IP nunca se guarda: solo un hash con una sal secreta.

        • Rust
        • PostgreSQL
      14. Bandeja de salida

        El mensaje se guarda primero y el aviso queda pendiente. Un trabajo lo reintenta hasta que Matrix confirma con un identificador de evento.

        • Rust
        • PostgreSQL
      15. Aviso a Matrix

        Envía el aviso a la sala con el bot propio de la web. Varios trabajadores no se pisan: cada uno coge una fila distinta.

        • Rust
      16. PostgreSQL

        Guarda los mensajes y los avisos pendientes. Las consultas se comprueban contra el esquema al compilar.

        • PostgreSQL
      17. Sin red

        PostgreSQL no escucha en ninguna interfaz: solo por el socket local y autenticación del sistema, sin contraseña que robar.

        • PostgreSQL
      18. Estado del homelab

        Cada 5 minutos el hipervisor deja un resumen: servicios en marcha, disponibilidad y el LED de cada pieza del mapa. Solo agregados.

      19. Tu navegador

        Guarda tus logros, la zona por la que ibas y si quieres sonido. Nada de eso sale de tu navegador.

      20. Terraform

        Crea el contenedor con sus recursos, su red y su cortafuegos: el único con política de entrada «rechazar».

        • Terraform
      21. Ansible

        Instala PostgreSQL, el binario y su servicio, con los secretos cifrados en el vault.

        • Ansible
      22. Comprobación antes de reiniciar

        El rol ejecuta «landing check» con el binario nuevo: si el contenido no es válido, no se reinicia nada.

        • Rust
        • Ansible
      23. Servicio endurecido

        Sistema de ficheros de solo lectura, sin capacidades, filtro de llamadas al sistema, memoria limitada. Tras reiniciar, espera a que /healthz diga «ok».

        • Ansible

      Lo que uso de mi árbol

      Pulsa una habilidad: se encienden en el mapa las piezas donde la uso y te cuento para qué.

      Rust
      El servidor entero: Axum, plantillas askama compiladas, contenido validado al arrancar, cabeceras de seguridad y el trabajo que reintenta los avisos.
      PostgreSQL
      Mensajes y avisos pendientes, consultas comprobadas en compilación con sqlx, bloqueo de filas sin esperas para los reintentos y acceso solo por socket.
      Traefik
      La única entrada: certificado, cabeceras y una ruta generada desde la lista de servicios.
      Terraform
      Crea el contenedor de la web, sus recursos y su cortafuegos.
      Ansible
      El rol que instala PostgreSQL, despliega el binario, comprueba el contenido y espera al healthz.
      Git
      Repositorio público con historial saneado, un hook que bloquea secretos antes de cada push y el mismo control antes de cada publicación.
      Forgejo · Actions
      El origen del repositorio y su CI/CD: comprobar en un runner sin secretos, desplegar solo en verde y publicar la copia en GitHub después.
      GitLab CI · GitHub Actions
      GitHub Actions repite formato, clippy y todas las pruebas sobre la copia pública.

      Bitácora de incidencias

      Averías reales. Lee el síntoma, elige la causa que creas y mira cómo se arregló.

      1. #1

        «Peer authentication failed for user anonymous»

        04-10-2026 · primer despliegue

        El primer arranque en el contenedor falló: PostgreSQL rechazaba al usuario «anonymous». Al corregirlo, apareció «empty host».

        ¿Qué crees que pasaba?

        Ver la solución
        Causa
        Con autenticación del sistema por socket no hay contraseña, pero la librería de base de datos necesita el usuario escrito en la URL y un host nominal: no lo deduce.
        Solución
        La URL de conexión lleva el usuario explícito y «localhost» como host nominal, con la ruta del socket aparte.
        Lección
        Quitar la contraseña es más seguro, pero cambia lo que la librería necesita. Leer el error dos veces: el segundo decía exactamente qué faltaba.
      2. #2

        «En Firefox el laboratorio está roto»

        04-10-2026 · navegador

        En Firefox el laboratorio se veía como una lista de desplegables y tarjetas, sin mapa. En otros navegadores, bien.

        ¿Qué crees que pasaba?

        Ver la solución
        Causa
        En ese navegador se había activado alguna vez la vista rápida, que enseña lo esencial sin animaciones. Se guardaba sin caducidad y la web parecía rota.
        Solución
        La vista rápida caduca a los 30 minutos y, mientras está activa, el laboratorio dice que está en vista rápida y ofrece un botón para salir.
        Lección
        Lo que se recuerda por el visitante tiene que caducar o verse. Un estado invisible que dura para siempre parece un fallo.
      3. #3

        Despliegues que no llegaban

        05-10-2026 · operación

        Los despliegues daban «inalcanzable» a mitad de camino y las pruebas de base de datos fallaban por tiempo, sin haber cambiado nada.

        ¿Qué crees que pasaba?

        Ver la solución
        Causa
        El primer domingo de mes, el RAID del sistema se comprueba entero durante unas 5 horas. Con los discos al límite, un checkpoint de PostgreSQL llegó a tardar 1.190 segundos.
        Solución
        Desplegar con los mismos pasos del rol por separado (copiar, comprobar, reiniciar, healthz), validar la suite completa en la integración continua y relanzar el playbook entero al terminar: sin cambios pendientes.
        Lección
        Saber qué tareas programadas tiene el servidor antes de culpar al código. Y que la comprobación de calidad no dependa de una sola máquina.
      4. #4

        Un repositorio que había que publicar

        04-10-2026 · seguridad

        El código iba a ser público, pero su historial se había escrito pensando en un repositorio privado.

        ¿Qué crees que pasaba?

        Ver la solución
        Causa
        Un historial de Git guarda todo lo que alguna vez estuvo, aunque ya no esté: notas de planificación, nombres internos o configuraciones.
        Solución
        Historial reescrito y saneado antes del primer push, un hook local que bloquea secretos, direcciones internas y nombres concretos, y la auditoría de un clon limpio después de cada push.
        Lección
        Borrar algo en un commit nuevo no lo borra de la historia. Lo que se publica se revisa antes, y se vuelve a revisar desde fuera.

      Cómo se construyó

      1. 04-10-2026

        Plan y contenido

        • Decisiones: Rust y Axum, PostgreSQL bien hecho aunque el tráfico no lo pida, contacto guardado y avisado a Matrix.
        • El contenido sacado de mi documentación y saneado: nada de direcciones ni nombres internos.
      2. 04-10-2026

        El servidor y su contenedor

        • Binario con todo dentro y páginas renderizadas al arrancar.
        • Contenedor propio con el cortafuegos más estricto y PostgreSQL solo por socket.
        • Publicada otra vez, tras retirarla de internet el 26-09 por seguridad.
      3. Octubre de 2026

        El laboratorio y el juego

        • Hermes como guía guionizado, mapa del servidor con recorridos animados, modo detective y terminal.
        • Estado en vivo de cada pieza y aspecto de rack de servidores con enanitos.
        • Unidades del rack: una página por pieza para verla por dentro, con sonido de sala de servidores.

      Decisiones (y por qué)

      • Cómo se sirven las páginas

        Renderizadas una vez al arrancar

        El contenido no cambia mientras el binario corre: renderizarlo en cada visita sería trabajo repetido. Desde memoria y con ETag, casi ninguna visita llega a gastar nada.

      • Qué pasa si falla el aviso

        Guardar primero, avisar después

        El mensaje es lo importante, el aviso no. Con una bandeja de salida, que Matrix no responda nunca hace perder un mensaje.

      • Qué guarda de quien escribe

        Un hash de la IP, nunca la IP

        Para limitar envíos basta con saber si dos peticiones vienen del mismo sitio. Con un hash con sal secreta se puede comparar sin poder volver a la IP.

      • Mensajes escritos por una IA

        Reconocerlos, no bloquearlos

        Que una reclutadora le pida a su asistente que me escriba es legítimo, pero quiero saberlo. El formulario lleva una nota que solo leen las máquinas, un campo que solo ven ellas y unas medidas de cómo se rellenó (sin guardar nada de lo que se escribe). El mensaje llega siempre, con una línea que dice qué se vio. Probado con dos modelos locales: uno cayó en las dos trampas de texto y el otro no; para ese quedan las medidas del navegador.

      • JavaScript

        Mejora progresiva, sin librerías

        Todo se lee sin JavaScript y la CSP prohíbe cualquier script en línea. El mapa, la terminal, los logros y el sonido son una capa encima.

      Lo que aprendí

      • Un binario con todo dentro es fácil de desplegar, de revertir y de razonar.
      • Validar el contenido al arrancar convierte una errata en un despliegue que no ocurre, no en una página rota.
      • Lo que es público se audita desde fuera, después de publicarlo.

      Lo siguiente

      • Cachear el HTML en Cloudflare con una regla propia.
      • Activar el captcha del formulario si algún día aparecen bots.

      ¿Hablamos?

      Puedo contar cualquiera de estas decisiones en detalle en una entrevista.

      Descargar CV Contactar ← Volver al rack

      El código es público en GitHub. Aquí no aparecen direcciones, puertos internos ni secretos.