Volver a publicaciones

Homelab

Uptime Kuma: vigila tus servicios y enseña su estado en vivo en tu CV

8 min de lectura

#uptime-kuma#monitorizacion#docker#caddy#portfolio

Portada: línea de latido y barras de disponibilidad sobre fondo oscuro con el título Uptime Kuma

Puntos clave

  • Uptime Kuma en Docker, publicado solo en 127.0.0.1 y con límite de memoria de 512 MB.
  • Monitores por URL pública para lo público, por IP y puerto para lo interno, y TCP para bases de datos.
  • Notificaciones desde el primer día: un monitor sin avisos no sirve de nada.
  • Caddy publica solo la página de estado y su API; el panel queda en la LAN/VPN.
  • Un widget en JS vanilla muestra el estado en tu web y se oculta si la API falla.

En mi portfolio hay un widget llamado “Estado en vivo”. Muestra si mis servicios están funcionando ahora mismo y su disponibilidad real. No es una captura ni un texto que yo escribo: lo alimenta la página de estado pública de Uptime Kuma, que vigila mis servicios cada minuto.

Para alguien que revisa mi CV es una prueba que puede comprobar: no digo que mantengo servicios en producción, lo enseño. En este capítulo cuento cómo lo monté y los fallos que me encontré por el camino.

Qué vas a montar

  • Uptime Kuma en Docker, accesible solo a través del proxy.
  • Monitores bien elegidos y notificaciones desde el primer día.
  • Una página de estado pública con solo lo que debe verse.
  • Caddy publicando esa página y su API, y el panel cerrado a la LAN/VPN.
  • Un widget en JavaScript para mostrar el estado en tu web.

Requisitos

  • Docker y Docker Compose en el NAS.
  • Caddy delante (ver el capítulo 2 de la serie). En los ejemplos, uptime.example.com.
  • En los ejemplos, 192.168.1.10 es la IP de tu NAS. Es un valor de ejemplo: usa la tuya.

Paso 1: el docker-compose.yml

services:
  uptime-kuma:
    image: louislam/uptime-kuma:1        # rama 1: la 2 cambia la base de datos
    container_name: uptime-kuma
    ports:
      - "127.0.0.1:3004:3001"            # solo el proxy llega a él
    volumes:
      - ./data:/app/data                 # base de datos y configuración
    mem_limit: 512m                      # con 256m se quedaba sin memoria
    healthcheck:
      test: ["CMD", "extra/healthcheck"] # la imagen 1 no trae wget
      interval: 60s
      timeout: 10s
      retries: 3
    restart: unless-stopped

Cada línea tiene su historia:

  • La etiqueta de la imagen. Me quedo en :1 a propósito. La versión 2 cambia el esquema de la base de datos, y esa migración quiero hacerla con calma. Además, aprendí que fijar una etiqueta muy concreta también tiene riesgo: tenía :1.23, esa etiqueta desapareció del registro y el contenedor no se pudo recrear. Fija la versión, sí, pero revísala de vez en cuando.
  • 127.0.0.1:3004. El puerto solo escucha en el propio NAS. Desde la red no se llega; solo Caddy.
  • mem_limit: 512m. Con 256 MB y unos 14 monitores, el contenedor moría por falta de memoria (OOM). Con 512 MB va holgado.
  • El healthcheck. Mi primer healthcheck usaba wget, pero la imagen 1 no lo trae. Resultado: el contenedor aparecía como unhealthy aunque funcionaba perfectamente. La imagen incluye su propio comprobador, extra/healthcheck: usa ese.
docker compose up -d
docker ps --filter name=uptime-kuma    # espera a ver "healthy"

La primera vez que entres, Uptime Kuma te pide crear el usuario administrador.

Paso 2: monitores que de verdad detectan fallos

Mi primer intento fue apuntar los monitores a nombres de contenedor, como http://grafana:3000. Fallaban todos con getaddrinfo ENOTFOUND. Los nombres de contenedor solo se resuelven dentro de la misma red Docker, y cada servicio estaba en la suya.

Ahora sigo esta regla:

Tipo de servicio Monitor Por qué
Público HTTP(s) a la URL pública (https://app.example.com) Prueba toda la cadena: DNS, proxy, certificado y servicio
Interno HTTP(s) a http://192.168.1.10:PUERTO No depende de redes Docker
Base de datos TCP Port a 192.168.1.10 y su puerto Comprueba que acepta conexiones

Dos ajustes en los monitores HTTP(s) públicos:

  • Códigos aceptados: 200-399. Muchos servicios responden con una redirección al login, y eso no es un fallo.
  • Aviso de caducidad del certificado activado. Si la renovación falla, te enteras antes de que caduque.

Si un servicio interno solo escucha en 127.0.0.1 (como Uptime Kuma o Jellyfin en esta serie), no se llega por la IP del NAS: vigílalo por su URL del proxy.

Paso 3: notificaciones desde el primer día

Este fallo me dolió. Un contenedor llevaba semanas parado y nadie se enteró. Tenía el monitor, pero no los avisos. Un monitor que nadie mira es exactamente lo mismo que no tener monitor.

Uptime Kuma admite muchos canales: Telegram, ntfy, correo electrónico y decenas más. Configura al menos uno en los ajustes de notificaciones, márcalo para que se aplique por defecto a los monitores nuevos y pruébalo con el botón de test antes de fiarte.

Paso 4: la página de estado pública

En Uptime Kuma, crea una página de estado con un slug corto, por ejemplo portfolio. Quedará en https://uptime.example.com/status/portfolio.

Añade solo los servicios públicos. Nada de paneles de administración, bases de datos ni servicios internos: una página de estado también le dice a un atacante qué tienes y dónde.

Paso 5: Caddy publica la página, no el panel

Quiero que cualquiera vea la página de estado, pero que el panel solo se abra desde casa o la VPN. El truco es un matcher con dos condiciones: la ruta no es de la página de estado y la IP no es de casa. Si se cumplen las dos, 403.

uptime.example.com {
    import security_headers

    # Todo lo que no sea la página de estado, solo desde LAN/VPN
    @privado {
        not path /status /status/* /api/status-page/* /api/entry-page /assets/* /icon.svg /favicon.ico /manifest.json /upload/*
        not remote_ip 192.168.1.0/24 10.8.0.0/24 127.0.0.1/8
    }
    respond @privado 403

    # CORS para que tu web pueda leer la API de la página de estado
    @statusapi path /api/status-page/*
    header @statusapi Access-Control-Allow-Origin "*"
    header @statusapi Access-Control-Allow-Methods "GET, OPTIONS"
    header @statusapi Vary "Origin"

    reverse_proxy localhost:3004
}

Las rutas permitidas son las que necesita la página de estado para cargar: la propia página, su API, los estilos y scripts (/assets/*), los iconos y las imágenes subidas (/upload/*). Las redes son las de ejemplo del capítulo 2 (192.168.1.0/24 para casa, 10.8.0.0/24 para la VPN).

Abrir CORS con * aquí no es un riesgo: son datos públicos y de solo lectura, lo mismo que ve cualquiera en la página.

Paso 6: el estado en tu web o CV

La página de estado usa dos endpoints públicos que tú también puedes leer:

  • GET https://uptime.example.com/api/status-page/portfolio: los grupos y monitores de la página (nombre e id de cada uno).
  • GET https://uptime.example.com/api/status-page/heartbeat/portfolio: los últimos latidos de cada monitor y su porcentaje de disponibilidad.

Con eso monto el widget. Primero, el contenedor en tu HTML, oculto por defecto:

<section id="estado-en-vivo" hidden>
  <h2>Estado en vivo</h2>
  <ul></ul>
  <a href="https://uptime.example.com/status/portfolio">Ver la página de estado</a>
</section>

Y el script, en JavaScript sin librerías:

const BASE = "https://uptime.example.com";
const SLUG = "portfolio";
const widget = document.getElementById("estado-en-vivo");
const list = widget.querySelector("ul");

// Códigos de estado de Uptime Kuma
const STATUS = { 0: "Caído", 1: "Operativo", 2: "Pendiente", 3: "Mantenimiento" };

async function getJson(url) {
  const res = await fetch(url, { cache: "no-store" });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

async function refresh() {
  try {
    const [page, beats] = await Promise.all([
      getJson(`${BASE}/api/status-page/${SLUG}`),
      getJson(`${BASE}/api/status-page/heartbeat/${SLUG}`),
    ]);

    const monitors = page.publicGroupList.flatMap((group) => group.monitorList);
    if (monitors.length === 0) throw new Error("Sin monitores");

    const items = monitors.map((monitor) => {
      const history = beats.heartbeatList[monitor.id] ?? [];
      const last = history[history.length - 1];            // el latido más reciente
      const uptime = beats.uptimeList[`${monitor.id}_24`]; // de 0 a 1, últimas 24 h

      const li = document.createElement("li");
      li.dataset.status = last ? String(last.status) : "none"; // para darle color con CSS
      const state = last ? STATUS[last.status] ?? "Desconocido" : "Sin datos";
      const percent = typeof uptime === "number" ? ` · ${(uptime * 100).toFixed(2)} % (24 h)` : "";
      li.textContent = `${monitor.name}: ${state}${percent}`;
      return li;
    });

    list.replaceChildren(...items);
    widget.hidden = false;
  } catch {
    // Si la API falla, mejor no enseñar nada que enseñar un estado roto
    widget.hidden = true;
  }
}

refresh();
setInterval(refresh, 60_000); // refresca cada 60 segundos

Tres detalles importantes:

  • Uso textContent, no innerHTML. Los nombres vienen de otro servidor: así no se puede inyectar HTML.
  • Si la API falla, el widget se oculta. Un bloque en rojo que dice “error” en tu portfolio da peor imagen que no enseñar nada.
  • Refresco cada 60 segundos, el mismo ritmo al que Uptime Kuma comprueba los servicios. Más a menudo no aporta nada.

Enlázalo desde tu CV y LinkedIn

Además del widget, pon el enlace directo a la página de estado en tu CV y en LinkedIn, con un texto claro como “disponibilidad de mis servicios en tiempo real”. Quien lo abra ve datos vivos, no una promesa.

Problemas frecuentes

Síntoma Causa probable Solución
Monitores con getaddrinfo ENOTFOUND Nombre de contenedor de otra red Docker URL pública o http://IP-NAS:PUERTO
El contenedor aparece unhealthy pero funciona Healthcheck con wget, que la imagen no trae ["CMD", "extra/healthcheck"]
El contenedor se reinicia solo Falta de memoria (OOM) Sube mem_limit a 512m
No se puede recrear el contenedor La etiqueta fijada ya no existe Usa una etiqueta vigente de la rama 1
La página de estado carga sin estilos Falta alguna ruta pública en @privado Revisa /assets/* y /upload/*
El widget no aparece CORS o slug incorrecto Mira la consola del navegador y prueba la URL de la API

Conclusión

Uptime Kuma me avisa cuando algo se cae y, además, me da algo que enseñar. La lección más cara fue la del contenedor parado durante semanas: un monitor sin notificaciones no vigila nada. La segunda, que la página pública debe contener solo lo que ya es público.

En el próximo capítulo daré un paso más: automatizar tareas del homelab con n8n, para que el NAS no solo avise, sino que también actúe.

My portfolio has a widget called “Live status”. It shows whether my services are running right now and their real availability. It is not a screenshot or a text I write: it is fed by the public status page of Uptime Kuma, which checks my services every minute.

For someone reviewing my CV it is proof they can verify: I do not say I maintain services in production, I show it. In this chapter I explain how I built it and the failures I ran into along the way.

What you will build

  • Uptime Kuma in Docker, reachable only through the proxy.
  • Well-chosen monitors and notifications from day one.
  • A public status page with only what should be visible.
  • Caddy publishing that page and its API, with the panel closed to the LAN/VPN.
  • A JavaScript widget to show the status on your site.

Requirements

  • Docker and Docker Compose on the NAS.
  • Caddy in front (see chapter 2 of the series). In the examples, uptime.example.com.
  • In the examples, 192.168.1.10 is your NAS IP. It is an example value: use yours.

Step 1: the docker-compose.yml

services:
  uptime-kuma:
    image: louislam/uptime-kuma:1        # 1.x branch: 2 changes the database
    container_name: uptime-kuma
    ports:
      - "127.0.0.1:3004:3001"            # only the proxy can reach it
    volumes:
      - ./data:/app/data                 # database and configuration
    mem_limit: 512m                      # with 256m it ran out of memory
    healthcheck:
      test: ["CMD", "extra/healthcheck"] # image 1 does not ship wget
      interval: 60s
      timeout: 10s
      retries: 3
    restart: unless-stopped

Every line has a story:

  • The image tag. I stay on :1 on purpose. Version 2 changes the database schema, and I want to do that migration calmly. I also learned that pinning a very specific tag is risky too: I had :1.23, that tag disappeared from the registry and the container could not be recreated. Pin the version, yes, but review it from time to time.
  • 127.0.0.1:3004. The port only listens on the NAS itself. It cannot be reached from the network; only Caddy gets to it.
  • mem_limit: 512m. With 256 MB and about 14 monitors, the container died from lack of memory (OOM). With 512 MB it has room to spare.
  • The healthcheck. My first healthcheck used wget, but image 1 does not ship it. Result: the container showed up as unhealthy even though it worked perfectly. The image includes its own checker, extra/healthcheck: use that.
docker compose up -d
docker ps --filter name=uptime-kuma    # wait until you see "healthy"

The first time you log in, Uptime Kuma asks you to create the admin user.

Step 2: monitors that actually detect failures

My first attempt was to point monitors at container names, like http://grafana:3000. They all failed with getaddrinfo ENOTFOUND. Container names only resolve inside the same Docker network, and each service was on its own.

Now I follow this rule:

Service type Monitor Why
Public HTTP(s) to the public URL (https://app.example.com) Tests the whole chain: DNS, proxy, certificate and service
Internal HTTP(s) to http://192.168.1.10:PORT Does not depend on Docker networks
Database TCP Port to 192.168.1.10 and its port Checks it accepts connections

Two settings on public HTTP(s) monitors:

  • Accepted status codes: 200-399. Many services answer with a redirect to the login page, and that is not a failure.
  • Certificate expiry notification enabled. If renewal fails, you find out before it expires.

If an internal service only listens on 127.0.0.1 (like Uptime Kuma or Jellyfin in this series), it cannot be reached through the NAS IP: monitor it through its proxy URL.

Step 3: notifications from day one

This failure hurt. A container had been stopped for weeks and nobody noticed. I had the monitor, but not the alerts. A monitor nobody watches is exactly the same as having no monitor.

Uptime Kuma supports many channels: Telegram, ntfy, email and dozens more. Set up at least one in the notification settings, mark it to apply by default to new monitors and test it with the test button before trusting it.

Step 4: the public status page

In Uptime Kuma, create a status page with a short slug, for example portfolio. It will live at https://uptime.example.com/status/portfolio.

Add only public services. No admin panels, databases or internal services: a status page also tells an attacker what you have and where.

Step 5: Caddy publishes the page, not the panel

I want anyone to see the status page, but the panel should only open from home or the VPN. The trick is a matcher with two conditions: the path is not part of the status page and the IP is not from home. If both are true, 403.

uptime.example.com {
    import security_headers

    # Everything except the status page, LAN/VPN only
    @privado {
        not path /status /status/* /api/status-page/* /api/entry-page /assets/* /icon.svg /favicon.ico /manifest.json /upload/*
        not remote_ip 192.168.1.0/24 10.8.0.0/24 127.0.0.1/8
    }
    respond @privado 403

    # CORS so your site can read the status page API
    @statusapi path /api/status-page/*
    header @statusapi Access-Control-Allow-Origin "*"
    header @statusapi Access-Control-Allow-Methods "GET, OPTIONS"
    header @statusapi Vary "Origin"

    reverse_proxy localhost:3004
}

The allowed paths are the ones the status page needs to load: the page itself, its API, styles and scripts (/assets/*), icons and uploaded images (/upload/*). The networks are the example ones from chapter 2 (192.168.1.0/24 for home, 10.8.0.0/24 for the VPN).

Opening CORS with * here is not a risk: it is public, read-only data, the same anyone sees on the page.

Step 6: the status on your site or CV

The status page uses two public endpoints that you can read too:

  • GET https://uptime.example.com/api/status-page/portfolio: the page’s groups and monitors (name and id of each).
  • GET https://uptime.example.com/api/status-page/heartbeat/portfolio: the latest heartbeats of each monitor and its availability percentage.

With that I build the widget. First, the container in your HTML, hidden by default:

<section id="live-status" hidden>
  <h2>Live status</h2>
  <ul></ul>
  <a href="https://uptime.example.com/status/portfolio">See the status page</a>
</section>

And the script, in plain JavaScript without libraries:

const BASE = "https://uptime.example.com";
const SLUG = "portfolio";
const widget = document.getElementById("live-status");
const list = widget.querySelector("ul");

// Uptime Kuma status codes
const STATUS = { 0: "Down", 1: "Up", 2: "Pending", 3: "Maintenance" };

async function getJson(url) {
  const res = await fetch(url, { cache: "no-store" });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

async function refresh() {
  try {
    const [page, beats] = await Promise.all([
      getJson(`${BASE}/api/status-page/${SLUG}`),
      getJson(`${BASE}/api/status-page/heartbeat/${SLUG}`),
    ]);

    const monitors = page.publicGroupList.flatMap((group) => group.monitorList);
    if (monitors.length === 0) throw new Error("No monitors");

    const items = monitors.map((monitor) => {
      const history = beats.heartbeatList[monitor.id] ?? [];
      const last = history[history.length - 1];            // most recent heartbeat
      const uptime = beats.uptimeList[`${monitor.id}_24`]; // 0 to 1, last 24 h

      const li = document.createElement("li");
      li.dataset.status = last ? String(last.status) : "none"; // to colour it with CSS
      const state = last ? STATUS[last.status] ?? "Unknown" : "No data";
      const percent = typeof uptime === "number" ? ` · ${(uptime * 100).toFixed(2)} % (24 h)` : "";
      li.textContent = `${monitor.name}: ${state}${percent}`;
      return li;
    });

    list.replaceChildren(...items);
    widget.hidden = false;
  } catch {
    // If the API fails, showing nothing is better than showing a broken status
    widget.hidden = true;
  }
}

refresh();
setInterval(refresh, 60_000); // refresh every 60 seconds

Three important details:

  • I use textContent, not innerHTML. The names come from another server: this way no HTML can be injected.
  • If the API fails, the widget hides itself. A red block saying “error” on your portfolio looks worse than showing nothing.
  • Refresh every 60 seconds, the same pace at which Uptime Kuma checks the services. More often adds nothing.

Besides the widget, put the direct link to the status page on your CV and LinkedIn, with a clear label like “real-time availability of my services”. Whoever opens it sees live data, not a promise.

Common problems

Symptom Likely cause Fix
Monitors with getaddrinfo ENOTFOUND Container name from another Docker network Public URL or http://NAS-IP:PORT
The container shows unhealthy but works Healthcheck with wget, which the image lacks ["CMD", "extra/healthcheck"]
The container keeps restarting Out of memory (OOM) Raise mem_limit to 512m
The container cannot be recreated The pinned tag no longer exists Use a current tag from the 1.x branch
The status page loads without styles A public path is missing from @privado Check /assets/* and /upload/*
The widget does not show up CORS or wrong slug Check the browser console and try the API URL

Conclusion

Uptime Kuma warns me when something goes down and, on top of that, gives me something to show. The most expensive lesson was the container stopped for weeks: a monitor without notifications watches nothing. The second one: the public page must contain only what is already public.

In the next chapter I will go one step further: automating homelab tasks with n8n, so the NAS does not just warn, it also acts.

Preguntas frecuentes

¿Por qué no uso la versión 2 de Uptime Kuma?

Porque cambia el esquema de la base de datos. Con los datos de meses en la versión 1, prefiero quedarme en la rama 1 y planear la migración con calma y una copia de seguridad.

¿Es seguro abrir la API de la página de estado a cualquier origen?

Sí, siempre que solo publiques esa API. Son datos públicos y de solo lectura: lo mismo que ve cualquiera en la página de estado. El panel y el resto de la API siguen cerrados.

¿Qué servicios pongo en la página pública?

Solo los que ya son públicos. Nada de paneles de administración, bases de datos ni servicios internos: una página de estado también es un mapa para un atacante.

Fuentes

Proyecto relacionado

Infraestructura Pesoz

Homelab self-hosted en producción 24/7: un NAS Synology con ~30 servicios en Docker tras Caddy + CrowdSec y un portátil con GPU como servidor de IA y observabilidad en k3s. VPN WireGuard, GitOps con Forgejo y estado en vivo con Uptime Kuma.

Compartir:LinkedInX
ivanjonasfc@dev: ~ — zshsesión en vivo

❯ neofetch

Perfilivanjonasfc@dev

Iván Jonás Fernández Correa
user
ivanjonasfc@dev
role
Full-Stack & DevOps
loc
Gijón, Asturias
títulos
DAM + ASIR
stack
Java · Python · TS · Docker · Linux · IA
busco
Full-Stack · DevOps · Sistemas
status
Disponible para trabajar

❯ uptime --human

Años en tecnología
6+
Proyectos en producción
6
Proyectos entregados
10+
Servicios self-hosted
~30

❯ figlet "Iván Jonás"

# Hola, soyIván Jonás

Desarrollador Full-Stack & DevOpsFull-Stack Developer & DevOps

Desarrollador Full-Stack & DevOps · Gijón, Asturias · ● Disponible para trabajar

↓ desliza, toca o escribe para abrir la sesión

❯ cat about.md

Desarrollo aplicaciones web y móviles de principio a fin (Java/Spring Boot, Python/FastAPI, TypeScript/React y Flutter) y me ocupo de su puesta en producción y operación con Docker, Linux, CI/CD y monitorización. Actualmente administro la infraestructura IT de una pyme y mantengo un homelab de unos 30 servicios en el que evalúo nuevas tecnologías, incluida IA local y privada (Ollama, RAG, MCP). Técnico Superior en DAM y ASIR.

❯ ls ~

Iván en números

Mi actividad en GitHub, mi trayectoria y mi infraestructura · datos en vivo.

Community

0

followers

followerfollowerfollowerfollowerfollower+8

Lenguajes

TypeScript32%
Dart14%
Python12%
HTML7%
SCSS5%

Tech Stack

Frontend

React
Next.js
Angular
Astro
Ionic
Flutter
TypeScript
Tailwind
shadcn/ui
React
Next.js
Angular
Astro
Ionic
Flutter
TypeScript
Tailwind
shadcn/ui

Backend

NestJS
FastAPI
Spring Boot
Python
Java
PostgreSQL
Redis
Qdrant
pgvector
MCP
RAG
LLMs
NestJS
FastAPI
Spring Boot
Python
Java
PostgreSQL
Redis
Qdrant
pgvector
MCP
RAG
LLMs

Tools

Docker
Linux
Nginx
Cloudflare
Forgejo
n8n
WireGuard
Prometheus
Grafana
Git
Windows Server
Active Directory
Loki
Tempo
Docker
Linux
Nginx
Cloudflare
Forgejo
n8n
WireGuard
Prometheus
Grafana
Git
Windows Server
Active Directory
Loki
Tempo

Proyectos Destacados

Soluciones reales a problemas reales. Una selección de mis trabajos más recientes.

Web Rutas Raíces
Cliente real

Web Rutas Raíces

Web de senderismo para cliente real en Asturias. Astro 7 + React 19 + Tailwind 4, mapas Leaflet con perfil de elevación a partir de GPX y versión bilingüe ES/EN. Alta de rutas automatizada con n8n + IA; WebP, CSP y cumplimiento RGPD.

AstroReactLeafletn8n
Infraestructura IT para una pyme
Cliente real

Infraestructura IT para una pyme

Diseño, implantación y soporte continuo de la infraestructura IT de una pequeña empresa que trabajaba desde un único portátil: NAS en RAID 1 con Btrfs y snapshots, permisos por grupos sobre SMB, puestos Windows 11 y una capa Docker para gestión documental, automatización y monitorización.

NASRAID 1BtrfsWindows 11DockerPaperless-ngx
OposApp
TFG

OposApp

Mi Trabajo de Fin de Grado (DAM): app móvil en Flutter que automatiza el seguimiento de convocatorias del BOPA y genera tests de práctica con IA local (Ollama), con privacidad total por diseño.

FlutterSpring BootIA Local (Ollama)PostgreSQLRGPD
English Brain — Tu coach de entrevistas en inglés con IA, autoalojado
Open Source · Activo

English Brain — Tu coach de entrevistas en inglés con IA, autoalojado

Una plataforma privada y autoalojada para preparar entrevistas técnicas en inglés hablando, no escribiendo. Tocas un orbe, hablas, y el coach te transcribe, te corrige la pronunciación fonema a fonema y te programa el repaso con repetición espaciada (py-fsrs). Toda la IA —el modelo de lenguaje, el reconocimiento de voz y la síntesis— corre en tu propio hardware: tu voz y tus respuestas nunca salen de tu red. Por dentro: cliente Flutter (Android, Web y Windows), orquestador FastAPI y una arquitectura GPU híbrida sobre Docker + Caddy.

IA localVozFlutterFastAPISelf-hostedPronunciación
QuantumSec — Kompliance
FCT 2025

QuantumSec — Kompliance

Prácticas FCT en remoto en una empresa de ciberseguridad (plataforma de cumplimiento Kompliance). Llevé su gestor de auditorías de Supabase a una API propia self-hosted con FastAPI: multitenancy validado por JWT, secretos cifrados, logs de auditoría inmutables y un gestor de contraseñas con claves de un solo uso cifradas.

FastAPIReactTypeScriptPostgreSQLSeguridad
DozeForge — Control total de tu Android por ADB (sin root)
Open Source · Beta 1

DozeForge — Control total de tu Android por ADB (sin root)

App de escritorio que te da el control real de tu Android desde el PC, sin rootear. La conectas por ADB y optimizas batería, memoria, red y decenas de ajustes del sistema desde un solo sitio: caza las apps que te drenan la batería (aunque se escondan tras Google Play Services), quita bloatware de fábrica y aplica perfiles de un clic — y todo es reversible con rollback atómico. Por dentro: Tauri 2 + Rust + SvelteKit 5, telemetría real (dumpsys/sysfs) y primitivas públicas de Android.

Android sin rootOptimizaciónTauri 2RustSvelteKit
SubsForge — Subtítulos con IA, 100% en local
Open Source

SubsForge — Subtítulos con IA, 100% en local

App de escritorio para traducir, generar, sincronizar, fusionar, limpiar, doblar, resumir e incrustar subtítulos de vídeo con IA — sin servicios de terceros y funcionando 100% en tu máquina. Nueve herramientas en una: traducción por lotes (nube u Ollama local), transcripción con Whisper, sincronización por detección de voz (VAD), doblaje con voces TTS, hardsub y limpieza de audio. Por dentro: Tauri 2 + Rust como puente y un sidecar de Python (faster-whisper, FFmpeg, ffsubsync, edge-tts) que hace el trabajo pesado sin bloquear nunca la interfaz.

Subtítulos IAWhisperTauri 2RustPython100% local
OrquestaGit — Tu DevOps senior local con IA
Open Source · En desarrollo

OrquestaGit — Tu DevOps senior local con IA

Una app de escritorio que automatiza el trabajo repetitivo de DevOps sobre tus repositorios: escanea su estado, bloquea secretos antes de cada commit, genera pipelines de CI/CD y audita dependencias, todo desde un único panel de control. La IA corre en local con Ollama, así que tu código nunca sale de tu equipo. Construida con Tauri 2 y un motor de orquestación en Python.

DevOpsIA localSeguridadTauri 2PythonCI/CD
Job-radar · Empleo CRM
Open Source

Job-radar · Empleo CRM

CRM web self-hosted que automatiza y centraliza mi búsqueda de empleo: reúne las ofertas, las puntúa con IA (Groq/Gemini/Ollama), genera carta y ajustes de CV, y las sigue en un kanban. Astro SSR + React + PostgreSQL, todo configurable desde la web y replicable con un comando.

Astro SSRReactPostgreSQLIA · LLMsn8nDocker
LifeOS — Suite familiar con IA
En desarrollo

LifeOS — Suite familiar con IA

Suite SaaS familiar autohospedada con IA propia ("Lito"). Backend NestJS + PostgreSQL + BullMQ y app Angular 18 + Ionic. RAG sobre pgvector/Qdrant y LLMs vía Model Context Protocol (MCP). Módulos de chat, gastos con splits en tiempo real, calendario, nube y notas.

NestJSAngularIA / RAGMCPDocker
Infraestructura Pesoz
En Producción

Infraestructura Pesoz

Homelab self-hosted en producción 24/7: un NAS Synology con ~30 servicios en Docker tras Caddy + CrowdSec y un portátil con GPU como servidor de IA y observabilidad en k3s. VPN WireGuard, GitOps con Forgejo y estado en vivo con Uptime Kuma.

Dockerk3sCaddyWireGuardCI/CDObservabilidad
Hooklab
Nuevo

Hooklab

Constructor, firmador y verificador de webhooks 100% en el navegador: crea payloads con variables simuladas, firma y verifica HMAC (GitHub, Stripe, Shopify, Slack), genera con IA local y dispara un POST. Zero-backend, con el estado compartible en la URL.

AstroZero-backendWeb CryptoIA local
Acopio de Código
Nuevo

Acopio de Código

Página web interactiva con snippets de código de varios lenguajes: busca, copia y ten todo a mano. Autohospedada en mi propia infraestructura.

ReactSnippetsSelf-hosted
Vagabond Battery Saver
Open Source

Vagabond Battery Saver

Módulo Magisk para Android root. Optimización agresiva de batería mediante gestión de governors de CPU y perfiles de energía al bloquear la pantalla.

AndroidShell ScriptMagisk
Happines&CO
Educativo

Happines&CO

Sitio web corporativo para empresa de eventos, desarrollado durante el ciclo de DAM. Diseño responsive y estructura semántica limpia.

HTML5CSS3JavaScript
Portfolio Web
Meta

Portfolio Web

Este mismo sitio. Astro 7 + React (islas) + Tailwind, self-hosted con Docker y Caddy. Cada proyecto es una carpeta con su contenido y se da de alta con un script (npm run proyecto:add).

AstroReactTailwindSelf-hosted

Últimas publicaciones

Guías prácticas probadas en mi propia infraestructura.

Portada: escudo con símbolo de prohibido sobre fondo oscuro con el título AdGuard Home
Redes y seguridad

AdGuard Home en Docker: sin publicidad en toda la casa y tus subdominios en local

Monto AdGuard Home en Docker para bloquear publicidad en toda la red, usar DNS cifrado y resolver mis subdominios en casa sin depender del router.

8 min de lectura

#adguard-home#dns#docker#privacidad#homelab
Leer
Portada: candado con flechas de entrada sobre fondo oscuro con el título Caddy como puerta de entrada
Redes y seguridad

Caddy como puerta de entrada del homelab: HTTPS, cabeceras seguras y CrowdSec

Cómo uso Caddy en Docker como única entrada a mi NAS: HTTPS por DNS de Cloudflare, snippets de seguridad, paneles solo LAN y CrowdSec contra IPs abusivas.

11 min de lectura

#caddy#https#docker#crowdsec#seguridad#cloudflare
Leer
Portada: una caja de contenedor con una campana de aviso y un avión de papel sobre fondo oscuro con el título Diun y Telegram
Automatización

Diun y alertas en Telegram: entérate de las imágenes nuevas sin actualizar a ciegas

Uso Diun para saber cuándo hay imágenes Docker nuevas y un único bot de Telegram para recibir los avisos de Diun, CrowdSec y Uptime Kuma.

7 min de lectura

#diun#docker#telegram#alertas#observabilidad
Leer
Portada: ramas de Git que se unen en una marca de verificación sobre fondo oscuro con el título Forgejo y GitHub Actions
Automatización

Forgejo y GitHub Actions: tu infraestructura en Git y un repo público profesional

Monto Forgejo en casa como fuente de verdad de mi infraestructura, protejo los secretos y preparo un repo público con CI, Dependabot y plantillas.

13 min de lectura

#git#forgejo#github-actions#ci#gitops#buenas-practicas
Leer
Portada: logotipo de reproducción sobre fondo oscuro con el título Jellyfin en mi NAS
Homelab

Jellyfin en un NAS: Docker, transcoding por hardware y la TV que no quería conectar

Cómo monto Jellyfin en un NAS con Intel, activo el transcoding por GPU, lo publico con Caddy y arreglo el error -27 de las TV LG antiguas.

7 min de lectura

#jellyfin#docker#nas#caddy#transcoding#synology
Leer
Portada: gráfica de líneas y barras de un panel de métricas sobre fondo oscuro con el título Observabilidad en casa
Homelab

Observabilidad en casa: Grafana, Prometheus y Loki sin ahogar el NAS

Monto métricas, logs y alertas con Prometheus, Grafana y Loki, y cuento por qué acabé moviendo el stack del NAS a un portátil con k3s.

11 min de lectura

#grafana#prometheus#loki#observabilidad#docker#kubernetes
Leer
Portada: línea de latido y barras de disponibilidad sobre fondo oscuro con el título Uptime Kuma
Homelab

Uptime Kuma: vigila tus servicios y enseña su estado en vivo en tu CV

Monto Uptime Kuma en Docker, configuro monitores y avisos útiles, publico solo la página de estado y la muestro en mi portfolio con un widget en JS.

8 min de lectura

#uptime-kuma#monitorizacion#docker#caddy#portfolio
Leer

Trabajemos juntos

¿Buscas un perfil full-stack / DevOps, un colaborador o un freelance? Cuéntame qué necesitas y te respondo en menos de 24 h.

ivanjonasfc@dev:~/contacto

# ¿Qué te trae por aquí?

↳
↳
↳

Sin spam. Tus datos solo se usan para responderte.

Terminal interactiva