Cómo desplegué mi web en una Raspberry Pi con Cloudflare Tunnel y CI/CD

Un sitio estático servido desde casa, sin abrir puertos, con despliegue automático desde GitHub. Y los siete fallos encadenados de Tailscale y rsync que me costó.

Quería publicar un sitio estático desde una Raspberry Pi que tengo en casa, con tres condiciones: no abrir ningún puerto en el router, que el despliegue fuera automático al hacer push a GitHub, y no pagar por nada que pudiera hacer yo. Lo conseguí, pero el camino tuvo más baches de los que esperaba. Este es el mapa, para que el próximo (o mi yo futuro) no tropiece con las mismas piedras.

La arquitectura, en una frase

GitHub construye el sitio, entra en mi red privada por Tailscale, copia los ficheros a la Pi con rsync, y Cloudflare Tunnel publica el nginx de la Pi en un dominio real. Nada de esto necesita un puerto abierto de cara a internet.

push a main


GitHub Actions ── build ── entra al tailnet ── rsync ──▶ Raspberry Pi
                                                            │ nginx (solo localhost)

                                                   Cloudflare Tunnel ──▶ midominio

Por qué Cloudflare Tunnel y no abrir un puerto

Abrir el 80/443 del router significa exponer mi red doméstica y pelearme con una IP que cambia. Un túnel de Cloudflare le da la vuelta al problema: es la Pi la que sale hacia Cloudflare y mantiene la conexión abierta; el tráfico entra por ahí. El router queda cerrado a cal y canto.

El túnel lo gestiono desde un fichero de configuración versionado, no desde el panel de Cloudflare. Así el “qué se publica y hacia dónde” vive en el repo, y no en un dashboard que dentro de un año no recordaré haber tocado.

Los siete baches (la parte útil)

El diseño es limpio. Ponerlo en marcha fue una cadena de fallos donde cada uno tapaba al siguiente. Por orden de aparición:

  1. El túnel conectaba a ratos y se caía. Cloudflared usa QUIC (UDP) por defecto, y muchas redes domésticas lo bloquean. Forzar protocol: http2 lo arregló.
  2. Reintentos infinitos contra el borde. Cloudflared abre 4 conexiones para tener redundancia; si la red solo deja establecer una, las otras tres mueren con i/o timeout y llenan el log sin dar redundancia real. ha-connections: 1.
  3. El contenedor del túnel no leía sus credenciales. Corre como usuario sin privilegios y las credenciales están en modo 0400 de su dueño. Se resuelve arrancándolo con el uid:gid de ese usuario —sin necesidad de root.
  4. rsync -e "tailscale ssh" no vale. rsync invoca el shell remoto como ssh -l usuario host, y ese envoltorio no acepta la opción -l.
  5. ssh a secas tampoco. La acción de Tailscale levanta tailscaled en modo userspace, así que el kernel del runner no enruta las 100.x y la conexión muere en connection timed out. La solución: un ProxyCommand tailscale nc %h %p en la configuración de ssh —no en rsync -e, porque rsync parte esa cadena por espacios sin respetar las comillas.
  6. La acción de Tailscale reporta success mintiendo. Sus intentos de tailscale up pueden fallar y el paso sale verde igual. El fallo reaparece mucho más abajo como un error de red en rsync, y persigues el problema equivocado durante media hora. Añadí un paso que comprueba tailscale status y aborta si el runner no ha entrado en el tailnet.
  7. Cloudflare cacheaba mis 404 cuatro horas. Pedí un fichero justo antes de desplegarlo, el borde se quedó con el 404, y el archivo siguió “sin existir” para todo el mundo aunque ya estuviera servido. Marcar la página de error como Cache-Control: no-store lo cura.

La lección que me llevo

De todos, el que más tiempo me robó fue el número 6: una acción que reporta éxito habiendo fallado. No hay peor bug que el que oculta su propia causa. La moraleja quedó grabada: si un paso puede fallar en silencio, añade una comprobación explícita que lo haga gritar. Un success en el que no puedes confiar es peor que un error honesto.

El resultado: hago push, y un par de minutos después el cambio está en línea, servido desde una placa que cabe en la palma de la mano. Cada vez que lo veo funcionar me sigue pareciendo un poco magia.