El frente arranca con el inventario MEDIDO de gioser, no con un plan: PID 1 es `arje-zero` (nuestro init ya gobierna un servidor real, aunque sobre userland Artix), Hetzner Cloud arranca por **BIOS** —que es justo el layout que `takana-install.sh` ya escribe— y el kernel propio trae VIRTIO/EXT4 `=y` con `netup` haciendo DHCPv4, que es exactamente el caso de una caja hcloud. Dos hallazgos que cambian el trabajo: - **La secuencia de arranque de gioser no está escrita en ningún lado.** `rc-status` dice `stopped` para caddy, gitea, sshd, cronie y act-runner y los cinco están VIVOS colgando de PID 1. Un `arje-absorb --from openrc` produciría una Semilla perfecta que no levanta el servidor. - **La mitad del Caddyfile describe un servidor que ya no existe**: 4 de 19 dominios sin DNS y sin ficheros en `/var/www`, uno en 502. Una mudanza fiel copia también la basura ⇒ la regla queda escrita: no se muda lo que no responde. `[perfil.servidor]` hereda `cli` (SDD 27 §7.1) y declara 5 raíces que existen (openssh, caddy, curl, wget, tmux) y **5 que NO** (takana, chrony, nftables, cronie, logrotate). Quedan `wanted`, y `wanted` no es `debt` ⇒ `drenaje.json` seguirá diciendo `deuda=0`. Se declaran igual: sin ellas el perfil saldría N/N —completo y verde— describiendo un servidor sin hora, sin cortafuegos y sin latido. Es la lección de `foot` en escritorio-sway, pagada por adelantado. Cuenta esperada: 5, verificada resolviendo cada nombre contra el campo `name` de las 869 recetas. La peor de esas cinco: **takana no tiene receta**. El corpus construye 869 y no la suya, así que el host no puede instalar takana desde el repo de takana — el bloqueante de que el servidor se sirva a sí mismo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
20 KiB
SDD 28 — El servidor de producción: instalar takana puro en una caja remota y mudarse a ella
Escrito 2026-09-09 a pedido del usuario («qué tan lejos estamos de montar un server takana en
Hetzner y que sea producción, y mover allá todo lo que tengo acá, y de una vez mantenerlo por repo
takana»). Todo número marcado (medido) salió de un comando corrido ese día sobre gioser.
Continúa SDD 19 (lanzamiento público) y SDD 27 (metapaquetes e instalador). No repite el instalador: SDD 27 decide QUÉ se instala y con qué verbo; esto decide DÓNDE, y cómo se llega.
0. La corrección de encuadre: ya estamos en Hetzner
gioser es un servidor Hetzner (hcloud id 126324694, hel1, 4 cores / 7,6 GiB) (medido). La
pregunta no era «cómo llegar a Hetzner» sino cómo tener una caja de takana en vez de ser un
inquilino más de una máquina compartida que:
- sirve 19 dominios por Caddy,
- tiene
/mnt/vvval 89 % (27 G libres) (medido), - es fija y sin backup por pedido explícito del usuario (blindaje anti-borrado en
deadman.shyfarm-down.sh, ver ADR 0014 y la memoria del proyecto), - y no publica nada de takana: cero entradas de takana en el Caddyfile (medido).
0.1 El hallazgo que cambia el tamaño del problema
$ ps -p 1 -o comm=,args=
arje-zero /usr/local/sbin/arje-zero
$ cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-linux root=UUID=54de62df-… rw net.ifnames=0 splash \
init=/usr/local/sbin/arje-zero loglevel=4
El init de takana ya gobierna un servidor de producción real. Caddy, gitea, sshd, qdrant y
act-runner cuelgan directo de PID 1 (medido: ps -eo pid,ppid,comm | awk '$2==1').
Pero eso no es takana puro: es Artix con nuestro init inyectado por init= en el cmdline. El
kernel, la libc y los 19 servicios son ajenos. Se probó la mitad difícil sobre userland prestado.
0.2 ⚠ La declaración del arranque no existe en ningún sitio legible
rc-status de OpenRC dice stopped para caddy, gitea, sshd, cronie y act-runner, y los cinco
están vivos (medido, los dos comandos). /etc/arje/cards.d tiene 4 tarjetas (acpid,
openrc-openclaw, squid, zram-swap) y ninguna de ellas los levanta.
⇒ La secuencia de arranque de esta caja no está escrita. Es lo menos reproducible de toda la mudanza y lo primero que hay que capturar — ver §4.
1. Inventario de lo que hay que mudar (medido 2026-09-09)
Datos en /mnt/vvv (217 G de 255)
tawasuyu |
124 G |
takana |
91 G — incluye el store de 55 G bind-mounteado desde el otro volumen ⇒ ~36 G propios |
rustup |
29 G |
act-runner · gioserv · _respaldo |
6,5 · 5,4 · 4,1 G |
~20 dirs *-standalone |
agora, card, chasqui, cosmos, dominium, khipu, minga, mirada, nakui, nahual, pata, pineal, pluma, shuma, takiy, llimphi… |
El volumen harkaq-cosecha (250 G, 148 G usados)
store |
55 G, 1327 artefactos — lo irreemplazable |
work-sources + cargo-home + gopath |
~85 G — caché regenerable, no se muda |
work-tarballs · devfs-cache · work-out |
3,2 G · 4,5 G · 397 M |
El store no se copia por red. Los dos volúmenes están en hel1; un volumen hcloud se desengancha
de una caja y se engancha a otra en la misma ubicación. La mudanza del store son dos comandos.
Servicios vivos, todos hijos de arje-zero
caddy :80/:443 · gitea :3002 · sshd :2345 · act_runner (el CI de tawasuyu) · qdrant
:6333/:6334 · openclaw · zeroclaw · shuma-gateway :7378 · tejido :4102 y :33097 ·
puerta-f6e393ff · uvicorn :8771 · python3 :8770 · squid :1137 · fail2ban · glances ·
php-fpm · metalog · adb :5037.
Lo que no está en ningún repo
/var/lib/gitea (2,2 G: SQLite + los repos de git de todo, incluido el origin de takana) ·
/var/www (287 M) · /home/sergio/gioser-web/ · /etc/caddy/Caddyfile + 20 ficheros .bak ·
los certificados de letsencrypt · los crontabs · /etc/arje/cards.d · las 3 credenciales de
~/.config/hammer/*.env · las llaves ~/.ssh/{github5,sergio} · el token hcloud · y la memoria
del agente en ~/.claude/.
1.1 La basura, contada — porque una mudanza fiel copia también la basura
De los 19 dominios del Caddyfile (probados uno por uno desde la propia caja):
| responden 200 | rotos |
|---|---|
| gioser.net · tawasuyu.net · git.gioser.net · summa.gioser.net · sergio.gioser.net | api.gioser.net → 502 (backend caído) |
| aura · sigma · kosmofono · terapeuta.ec → sin DNS |
Y sus raíces no existen en disco: /var/www sólo tiene terapeuta, goaccess, ogisoer,
git-tawasuyu, letsencrypt y un .bak — no hay aura_frontend, ni sigma, ni summa/frontend,
ni kosmofono. La mitad del Caddyfile describe un servidor que ya no existe.
⇒ Regla de la mudanza: no se muda lo que no responde. Lo que no tiene DNS ni ficheros se anota y se deja morir con la caja vieja. La utilidad del §4 tiene que saber decir declarado / vivo / fósil.
2. La caja: qué, cuánto y por qué
Precios consultados a la API de hcloud el 2026-09-09 (hel1, x86, tipos no deprecados) (medido):
| tipo | recursos | €/mes |
|---|---|---|
cx53 |
16 c / 32 G / 320 G | 29,49 |
cpx41 |
8 c / 16 G / 240 G | 32,49 |
ccx23 (dedicado) |
4 c / 16 G / 160 G | 85,99 |
Elegido cx53: es el mejor punto de la tabla y entra holgado el conjunto tawasuyu + takana +
rustup (~244 G) si algún día se muda todo.
Decisiones del usuario (2026-09-09):
- La caja PUBLICA; el LXC prestado sigue moliendo.
dev.gioser.netcuesta €0 y es la máquina con más RAM (6 c / 16 G + 8 G swap). Separar servir de compilar mantiene la caja chica y barata. La caja nueva puede además compilar, con el LXC enchufado como segundo worker. - «Producción» = operativo pero privado. Sin descarga pública todavía ⇒ no se activan los bloqueantes legales del SDD 19 §2 (espejo de fuentes público por HTTP, marca de terceros). Lo que sí hace falta igual es la clave de release estable (§5).
- gioser se BORRA cuando la mudanza esté comprobada. Eso convierte esto en un experimento con fecha de caducidad, y cambia el diseño entero: ver §6.
⚠ El blindaje anti-borrado de gioser es deliberado y hay que levantarlo a mano y a propósito.
deadman.sh y farm-down.sh lo protegen por lista negra de nombre y por ausencia del label
role=hammer-worker. Ninguna automatización debe poder borrarlo: cuando llegue el día, se borra a
mano, con la comprobación del §6 en verde y no antes.
3. Instalar takana puro en una caja remota
3.1 Lo que ya está, y por qué está más cerca de lo que parece
El dato que lo destraba: Hetzner Cloud arranca por BIOS, no por EFI. (medido en gioser: no
existe /sys/firmware/efi; GPT + GRUB; /boot es vfat de 1 G.) Y scripts/takana-install.sh
escribe exactamente ese layout: GPT + GRUB BIOS, SeaBIOS → GRUB → kernel → arje-zero PID1.
El camino EFI —que fue el que más trabajo costó (ADR 0010)— es
el que Hetzner no necesita.
| pieza | estado (medido) |
|---|---|
| kernel con virtio | recipes/linux.toml compila VIRTIO, VIRTIO_PCI, VIRTIO_BLK, VIRTIO_NET, EXT4_FS =y ⇒ sin initramfs |
| red | crates/netup = cliente DHCPv4 propio en Rust sobre netlink, autodetecta interfaz y espera carrier. gioser toma su IP por DHCP en eth0 (/32, net.ifnames=0, DNS 185.12.64.2) ⇒ es justo su caso |
| acceso | recipes/openssh.toml sellado; el product-rootfs vigente ya trae sshd; takana-install.sh acepta AUTHKEYS |
| servir | recipes/caddy.toml existe |
| escritura a disco | scripts/takana-install.sh (no interactivo) y scripts/takana-live-install.sh (TUI), probado por scripts/install-tui-test.sh |
3.2 El camino remoto: rescue → dd → reboot
Hetzner Cloud tiene un sistema de rescate (Debian por SSH) que se activa desde la API sin tocar la caja. Desde ahí se escribe la imagen al disco y se reinicia. No hace falta ISO, ni consola, ni medio físico — que es justo lo que hace innecesario el 90 % del trabajo de ISO/USB para este caso.
3.3 Los cuatro huecos, nombrados
perfil.servidor— HECHO hoy endocs/state/targets.toml. Ver §3.4.console=ttyS0en el cmdline — el único salvavidas si el arranque falla y no hay teclado. Hetzner da consola web; sin serial en el kernel, esa consola no muestra nada útil.- IPv6 —
netuphace DHCPv4 y Hetzner entrega un/64estático. La caja arrancaría con IPv4 sola. Aceptable para empezar, pero hay que saberlo antes, no descubrirlo. - Un
.imgdel perfil servidor que el rescue pueda escribir. Eshydrate-profile.py+install-image.sh, los dos ya existentes — ver §7.
3.4 El perfil, y la deuda que declara (hecho 2026-09-09)
[perfil.servidor] hereda cli (que hereda base, SDD 27 §7.1)
y añade 5 raíces que existen (openssh, caddy, curl, wget, tmux) y 5 que NO:
| raíz que no resuelve | por qué está declarada igual |
|---|---|
takana |
el propio takana no tiene receta. El corpus construye 869 recetas y no la suya ⇒ el binario sólo existe como cargo build sobre un clon. Es EL bloqueante del §5 |
chrony |
sin NTP, un reloj que deriva rompe TLS y las firmas — y falla diciendo «firma inválida», no «la hora» |
nftables |
dev.gioser.net está hoy expuesto con fuerza bruta a root en el journal; una caja nueva no debería nacer así |
cronie |
el latido son tres líneas de crontab, y sin systemd no hay timers: o hay cron, o el latido es una tarjeta de arje-zero. Decisión pendiente |
logrotate |
en una caja que sirve, el log de acceso crece hasta llenar el disco |
Quedan wanted en el grafo, y wanted no es debt ⇒ drenaje.json seguirá diciendo
deuda=0. Se declaran igual porque una deuda escrita se ve y una omitida no: sin ellas el perfil
saldría N/N —completo y verde— describiendo un servidor sin hora, sin cortafuegos y sin latido. Es
la lección de foot en escritorio-sway (121/121 sellado y sin emulador de terminal), pagada por
adelantado. Cuenta esperada: 5. Un sexto wanted es un error de tipeo, no una receta nueva.
4. La utilidad de migración: absorber lo VIVO, no lo declarado
arje-absorb ya existe, está sellado (recipes/arje-absorb.toml) y su --help dice: lee
sysvinit | runit | dinit | openrc | auto y emite una Tarjeta Semilla con cada servicio del init
ajeno como hija de arje-zero. Tiene parsers para los cinco (systemd.rs, openrc.rs, runit.rs,
dinit.rs, sysvinit.rs).
Pero lee la declaración, y en gioser la declaración miente (§0.2). Un arje-absorb --from openrc
sobre esta caja produciría una Semilla que no levanta el servidor, y lo haría en silencio: la
Semilla sería sintácticamente perfecta.
⇒ Lo que falta es un segundo lector: absorber del proceso vivo (/proc: ppid==1, cmdline,
cwd, entorno, fds en LISTEN) y reconciliar ambas lecturas marcando dónde discrepan. Eso es lo
que el usuario nombró como «lo aprendido de matilda» — absorber lo que hay, no lo que está escrito.
La utilidad debe clasificar en tres, no en dos:
| clase | criterio | qué se hace |
|---|---|---|
| vivo | proceso corriendo y declarado | se muda |
| declarado, muerto | en la config, sin proceso ni ficheros (los 4 dominios sin DNS) | fósil: se anota y se deja morir |
| vivo, no declarado | proceso con ppid==1 que ningún init declara (caddy, gitea, sshd, act-runner) |
se escribe la tarjeta que faltaba — es el trabajo real |
Se construye al final, estrenándose con la migración gioser → caja nueva, con el caso del
rc-status que miente como test de aceptación. Su especificación es el §1 de este documento.
5. Que el servidor sirva sus propios paquetes a su propio host
Existe el camino entero: scripts/build-repo.sh (corpus → .swm firmados + índice),
takana install --repo que ya acepta lista HTTP multi-origen y verifica cada paquete contra el
digest del índice firmado (ADR 0014), takana upgrade con
rollback, y caddy para servir dist/repo con un file_server.
Lo que falta no es servir, son dos cosas:
- La clave.
build-repo.shfirma hoy con una clave efímera por defecto (keygen releasesi no se le pasaKEY=) (medido, línea 33). Sin clave estable, «firmado» no significa nada. Es el SDD 19 §3.2 y es requisito aunque el repo sea privado. - La receta de
takana(§3.4). El host necesitatakanainstalado para consumir los paquetes, y hoy no puede instalarlo desde el repo porque takana no está en el repo.
⚠ Y una caja que se sirve a sí misma es un solo origen — justo lo que el ADR 0014 existe para no tener. El camino normal puede ser el local, pero la lista de orígenes del host debe incluir también el Storage Box o gioser. Si no, el primer origen roto es el último.
Lo que esto vale: cerrar el bucle es el ensayo de «actualización en sitio probada de verdad» (SDD 19 §5.2), hecho sobre una máquina que importa pero que no es el laptop de nadie.
6. La mudanza como experimento: el criterio de borrado
El usuario decidió que gioser se borra al final. Eso no es un detalle de calendario: es lo que convierte la mudanza en un experimento con criterio de aceptación, y hay que escribirlo antes.
Regla base: un ausente falla ruidosamente; un vacío llega hasta el final diciendo que todo fue bien. Aplicado acá: «lo copié» no es prueba de nada. La prueba es que el servicio responde en la caja nueva y que el dato se verifica por contenido.
6.1 Las puertas, en orden. Ninguna se salta.
| # | puerta | cómo se comprueba |
|---|---|---|
| 1 | la caja nueva arranca takana puro y se entra por SSH | ssh a la IP nueva tras el reboot del rescue |
| 2 | el store está entero | 1327 artefactos y ninguno vacío — respaldo-storagebox.sh --listar separa los vacíos; un directorio vacío no es un artefacto |
| 3 | los grafos salen idénticos | build-state.py en las dos máquinas ⇒ los cuatro grafos byte a byte. Es la prueba de que un hub está bien montado, no que «funcione» |
| 4 | la granja late en la caja nueva | una cosecha completa con el LXC enchufado por .fleet y artefactos que vuelven |
| 5 | el repo se sirve y el host se actualiza de sí mismo | §5 en verde, con la clave estable |
| 6 | cada dominio vivo responde 200 desde fuera | la tabla del §1.1, repetida contra la IP nueva |
| 7 | el respaldo al Storage Box corre desde la caja nueva | una corrida completa, no un --seco |
| 8 | lo fósil está anotado y decidido | lista del §1.1 con «muere» o «revive» al lado de cada uno |
⚠ Puerta 3 tiene una trampa conocida: sealed_remoto dependía de la máquina (laptop 700, gioser
751 sobre el MISMO corpus) y por eso se sacó del JSON. Si los grafos difieren, mirar primero si la
diferencia es «cuánto de esto tengo en disco» y no «qué existe».
6.2 Lo que la mudanza tiene que producir, además de la mudanza
El usuario lo pidió explícito: que este experimento saque recetas y las pruebe. La caja vieja es un catálogo de software real en producción, y cada servicio que hoy corre sobre binarios de Artix es una receta candidata con un caso de uso comprobado detrás:
| lo que corre hoy en Artix | receta |
|---|---|
caddy |
✅ existe (recipes/caddy.toml) |
openssh |
✅ existe |
gitea |
✗ — y es la que más peso tiene: aloja el origin de takana |
qdrant, squid, fail2ban, php-fpm, metalog, glances |
✗ |
chrony, nftables, cronie, logrotate |
✗ — ya declaradas wanted en perfil.servidor |
Cada una que se cierre se prueba sola: el servicio o levanta en la caja nueva o no, y eso se ve el mismo día. Es la diferencia entre una receta «sellada» y una receta usada.
7. Reusar los scripts que ya existen, y no escribir de nuevo
Pedido explícito del usuario. El inventario de lo que ya hace el trabajo:
| para | script existente | qué le falta |
|---|---|---|
| armar el rootfs de un perfil | scripts/hydrate-profile.py |
nada — es el camino bueno (los hydrate-*.sh por escritorio divergen de targets.toml) |
producir el .img |
scripts/install-image.sh / scripts/product-image.sh |
apuntar al perfil.servidor |
| escribir a disco | scripts/takana-install.sh |
el envoltorio de rescue del §3.2 |
| instalador con TUI | scripts/takana-live-install.sh |
ser el mismo motor que el remoto, con las respuestas por fichero |
| provisionar un worker | scripts/farm/vps-setup.sh |
ya soporta apt y dnf; le falta la rama «takana puro» (sin gestor de paquetes ajeno) |
| enchufar a la granja | scripts/farm/.fleet |
está en .gitignore ⇒ un hub recién clonado nace con la flota vacía en silencio |
| latido | scripts/farm/cosecha-cron.sh |
decidir cron vs tarjeta de arje (§3.4) |
| respaldo | scripts/respaldo-storagebox.sh |
nada — es interrumpible y continuable |
| absorber el init ajeno | arje-absorb |
el lector de procesos vivos (§4) |
⚠ Un motor, tres frentes. La tentación es escribir el instalador remoto aparte del TUI. Ahí es
donde divergen: ya pasó con los hydrate-*.sh contra targets.toml, y con la unidad systemd del
worker que quedó vieja mientras el repo estaba al día. La instalación remota tiene que ser la misma
que hace la persona, grabada en un fichero de respuestas.
8. churay como instalador centralizado — la dirección, no la decisión
El usuario lo planteó el 2026-09-09: un instalador único, con CLI y TUI, que sirva para
- las apps de tawasuyu,
- paquetes sueltos en cualquier distro, al estilo nix (instalar sin ser root, sin tocar el sistema anfitrión),
- takana mismo,
- y
llimphi.
Encaja con lo que ya hay y por eso vale escribirlo: takana install ya resuelve por nombre contra un
índice firmado y ya acepta múltiples orígenes; qorpa (ADR 0015)
ya corre imágenes ajenas enjauladas sobre el mismo kernel; y el vigía de espejos de churay ya corre
en cron desde gioser.
No se decide acá. Es un ADR propio, y tiene al menos tres preguntas abiertas antes de escribir
una línea: (1) si el instalador «en cualquier distro» hidrata binarios o reproduce desde fuente —la
misma pregunta del SDD 27 §4, y la respuesta puede no
ser la misma fuera de takana; (2) quién es el dueño del verbo, si takana install crece o si
churay lo envuelve; (3) qué pasa con un host glibc, que es exactamente lo que el ADR 0015 aparta
del store a propósito. Queda anotado para que se decida, no para que se descubra a mitad.
9. Estado y orden
| paso | estado |
|---|---|
0. perfil.servidor + este documento + el renombre hammer-* de los instaladores |
✅ 2026-09-09 |
1. caja cx53 de sacrificio: rescue → dd → arranca takana puro (§3) |
pendiente |
| 2. el servidor se sirve sus paquetes y se actualiza a sí mismo (§5) | pendiente — requiere clave estable + receta de takana |
| 3. mudanza con las 8 puertas del §6, y recetas nuevas del §6.2 | pendiente |
| 4. la utilidad de absorción (§4), estrenándose con esta migración | pendiente |
5. churay como instalador centralizado (§8) |
ADR propio, sin decidir |
El volumen del store NO se engancha a la caja nueva hasta que pase la puerta 1. Una caja de sacrificio con el store dentro deja de ser de sacrificio.