Devbox remoto

Tutorial paso a paso

Servidor de desarrollo remoto con NetBird, Herdr y Claude Code

Lightsail São Paulo como entorno principal, NetBird como única puerta de entrada, sesiones de Claude Code que siguen vivas cuando cerrás la PC, y la PC de la oficina para lo que necesita Windows.

Qué vas a armar

Un servidor Ubuntu en Lightsail São Paulo concentra los repos, SQL Server y las sesiones de Claude Code. Las dos PC y el celular entran únicamente por NetBird, una red privada sobre WireGuard, y hacia Internet no queda abierto ningún puerto. La PC de la oficina además compila lo que necesita Windows.

Todo entra por la red NetBird; a Internet no se abre ningún puertoLightsail São Paulo · Ubuntu 24.04Red NetBirdtráfico cifradoNetBird wt0 · grupo dev-serversSin puertos de entrada: solo conexiones salientesSSH (puerto 22)Solo para el grupo dev-clientsHerdr + Claude Code CLISesiones que siguen vivas al desconectarte:workspace por proyecto, panel por tareaSQL Server (1433) y repositoriosDatos de desarrollo, sin puerto públicoPC oficinadev-clients y win-buildPC casadev-clients · cliente HerdrCelulardev-clients · Claude y MoshiInternet sin NetBirdSSH y SQL no respondenbloqueado

Los tres clientes entran por NetBird y recién ahí ven SSH, Herdr y SQL; desde Internet no responde ningún servicio. El alta de equipos y las reglas pasan por el servicio de coordinación de NetBird; el tráfico va cifrado directo entre tus equipos. El servidor sí sale a Internet: Claude Code necesita HTTPS hacia api.anthropic.com y claude.ai.

PuertosNetBird no pide abrir ningún puerto de entrada en ningún extremo: ni en Lightsail, ni en el router de tu casa, ni en el de la oficina. Cada equipo solo sale: TCP 443 hacia gestión y señalización, UDP 80, 443, 3478 y 5555 para armar la conexión directa, y UDP o TCP 443 hacia el relay. Si una red bloquea UDP, el túnel sigue por el relay en TCP 443, más lento. Un firewall que inspecciona TLS puede cortarlo o forzar el relay: excluí los dominios de NetBird de esa inspección.

Equipo Grupo en NetBird Qué corre
Servidor Lightsail (São Paulo) dev-servers NetBird, SSH, Herdr, Claude Code CLI, SQL Server 2025
PC oficina (Windows) dev-clients y win-build NetBird, cliente Herdr, nodo de build Windows
PC casa (Windows) dev-clients NetBird, cliente Herdr
Celular dev-clients NetBird, app de Claude (Remote Control), Moshi de respaldo

Supuestos

  • Lightsail en la región São Paulo, con Ubuntu 24.04 LTS.
  • NetBird en una cuenta dedicada a este entorno, separada de cualquier otra red que administres. El plan gratuito cubre hasta 5 usuarios y 100 máquinas.
  • Dos usuarios Linux: ubuntu administra y tiene sudo; dev trabaja sin sudo. Claude Code, Herdr y los repos corren como dev, así una sesión de Claude no puede leer las claves de AWS, de Cloudflare ni de SQL Server.
  • Por ahora sos el único usuario. Cuando se sumen los otros dos, cada uno necesita su usuario Linux y su propia cuenta de Claude: los términos de Anthropic no permiten compartir una cuenta.
  • SQL Server 2025 en Linux reemplaza a 2016.
  • Las apps que necesitan Windows se compilan en la PC de la oficina, que el servidor maneja por SSH (paso 7b).
  • En los comandos, IP_NETBIRD es la dirección NetBird del servidor e IP_PC_OFICINA la de la PC de la oficina. Las dos están en el rango 100.64.0.0/10.

Quién hace cada paso

Hasta el paso 5, Claude Code corre en tu PC y ejecuta los comandos en el servidor por SSH. Desde el paso 5 también puede correr en el servidor.

Paso Lo hacés vos Lo puede hacer Claude Code
1 Consola de Lightsail, clave SSH de tu PC Actualizar el sistema
2 Panel de NetBird: cuenta, grupos, políticas, setup key Subir y correr 01-netbird-server.sh
3 Instalar NetBird en las PC y el celular, iniciar sesión —
4 Borrar la regla SSH en Lightsail Subir y correr 02-endurecer.sh, comprobar
5 Iniciar sesión de Claude en el servidor Instalar Claude Code, clonar repos
6 — Instalar Herdr y su integración
7 Configuración SSH y agente en cada PC Instalar el cliente Herdr
7b Correr 06-nodo-windows.ps1 en la PC Clave y configuración SSH en el servidor, pruebas
8 App de Claude y Moshi en el celular, dispositivos de confianza Activar Remote Control y avisos en /config
9 Averiguar la collation de producción Correr 05-sqlserver.sh
10 Bucket S3, usuario IAM, alarmas Configurar y probar los backups, ensayar el reinicio

Los pasos 1 a 4 dejan el servidor accesible solo por NetBird. No instales nada más antes de terminar el paso 4.

Primero la red segura; después el entorno, y los datos al finalRed seguraPasos 1 a 4Instancia, NetBird,grupos y firewallEntorno de trabajoPasos 5 a 7bClaude Code, Herdr,PC y nodo WindowsCelularPaso 8Remote Control y MoshiDatos y respaldoPasos 9 y 10SQL Server, backupsy ensayo de reinicioPuerta: SSH público cerradoPuerta: reinicio y restauración probados
Las dos puertas son condiciones para seguir: no instales nada antes de cerrar el SSH público, y no dependas del entorno antes de ensayar un reinicio y una restauración.

Paso 1

Crear la instancia y dejar el firewall mínimo

Al terminar este paso tenés una instancia en São Paulo con IP fija, a la que solo tu IP actual puede entrar por SSH.

  1. En la PC de la oficina, generá tu clave SSH en PowerShell y ponele passphrase:

    powershell
    ssh-keygen -t ed25519 -C "pc-oficina"
    Get-Content $env:USERPROFILE\.ssh\id_ed25519.pub
  2. En la consola de Lightsail elegí la región São Paulo. Después: Create instance → Linux/Unix → OS Only → Ubuntu 24.04 LTS. En la sección de clave SSH elegí subir una clave propia y cargá id_ed25519.pub. Elegí el plan de 16 GB y nombrala devbox.

  3. Tené en cuenta que el plan de 16 GB es de ráfaga: sostiene alrededor del 40 % de sus 4 vCPU. Alcanza para una persona con un par de agentes; con varios compilando a la vez se agota el crédito y la CPU queda limitada. Pasar a un plan más grande se hace con snapshot e instancia nueva, y el disco no se achica.

  4. Pestaña Networking → Create static IP y asociala a devbox. Solo se usa para el acceso inicial y para el rescate; los clientes van a entrar por NetBird.

  5. En Networking → IPv4 Firewall dejá una sola regla: SSH (TCP 22) restringida a tu IP pública actual. Es temporal y se borra en el paso 4.

  6. Borrá la regla HTTP 80. En IPv6 Firewall borrá todas las reglas.

  7. Pestaña Snapshots → activá los snapshots automáticos diarios.

  8. Pestaña Metrics → creá alarmas con aviso por mail para uso de CPU, capacidad de ráfaga y fallas de chequeo de estado.

  9. Entrá y actualizá el sistema sin preguntas. Cambiá la zona horaria por la tuya:

    bash
    ssh ubuntu@IP_ESTATICA
    sudo DEBIAN_FRONTEND=noninteractive apt-get update
    sudo DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::=--force-confold -y full-upgrade
    sudo timedatectl set-timezone America/Argentina/Buenos_Aires
    sudo reboot

Paso 2

NetBird: cuenta, grupos, políticas y servidor

Al terminar, el servidor es un equipo de tu red NetBird dentro del grupo dev-servers, y solo el grupo dev-clients puede llegarle.

En el panel de NetBird

  1. Creá una cuenta de NetBird dedicada a este entorno.

  2. Access Control → Groups → Create Group: creá dev-servers, dev-clients y win-build.

  3. Access Control → Policies → Add policy: creá estas dos, unidireccionales:

    Política Source Destination Protocolo Puertos
    dev-access dev-clients dev-servers TCP 22 y 1433
    build-access dev-servers win-build TCP 22
  4. Borrá la política Default. Mientras exista, todos los equipos de la cuenta se ven entre sí y las políticas nuevas no restringen nada. En una cuenta dedicada no hay nada que dependa de ella.

  5. Settings → Setup Keys → Create Setup Key: tipo one-off, vencimiento de 1 día y grupo automático dev-servers. Copiá la clave.

En el servidor

  1. Subí el script:

    powershell
    scp 01-netbird-server.sh ubuntu@IP_ESTATICA:~/
  2. Ejecutalo con la setup key como argumento:

    bash
    chmod +x 01-netbird-server.sh
    sudo ./01-netbird-server.sh SETUP_KEY
  3. Al final muestra netbird status y la IP NetBird del servidor. Anotala: es IP_NETBIRD en el resto del tutorial.

  4. En el panel, Peers: el servidor aparece dentro de dev-servers. Renombralo a devbox para reconocerlo.

La setup key queda en el historial del shell, pero es de un solo uso y vence en un día: una vez usada no sirve. Los equipos registrados con setup key no tienen vencimiento de sesión, así que el servidor no vuelve a pedir login.

Script completo

01-netbird-server.sh
texto
#!/usr/bin/env bash
# 01-netbird-server.sh
# Instala NetBird en el servidor y lo registra en tu cuenta con una setup key.
# La setup key debe ser de un solo uso y asignar el grupo dev-servers.
#
# Uso: sudo ./01-netbird-server.sh <SETUP_KEY>
set -euo pipefail

if [[ $EUID -ne 0 ]]; then
  echo "Ejecuta este script con sudo." >&2
  exit 1
fi

SETUP_KEY="${1:-}"
if [[ -z "$SETUP_KEY" ]]; then
  echo "Uso: sudo $0 <SETUP_KEY>" >&2
  exit 1
fi

NB_IF="wt0"

if ! command -v netbird > /dev/null 2>&1; then
  export DEBIAN_FRONTEND=noninteractive
  apt-get update
  apt-get install -y curl ca-certificates
  curl -fsSL https://pkgs.netbird.io/install.sh | sh
fi

netbird up --setup-key "$SETUP_KEY"

# Espera hasta 30 segundos a que la interfaz tenga direccion.
NB_IP=""
for _ in $(seq 1 30); do
  NB_IP="$(ip -4 -o addr show "$NB_IF" 2> /dev/null | awk '{print $4}' | cut -d/ -f1)"
  [[ -n "$NB_IP" ]] && break
  sleep 1
done

echo
netbird status
echo
if [[ -n "$NB_IP" ]]; then
  echo "IP NetBird del servidor: ${NB_IP}"
  echo "Anotala: es la que usan las PC y el celular (IP_NETBIRD en el tutorial)."
else
  echo "La interfaz ${NB_IF} todavia no tiene direccion. Revisa 'netbird status' y la setup key." >&2
  exit 1
fi

Paso 3

Clientes NetBird en las dos PC y el celular

Cada equipo inicia sesión con tu usuario de NetBird y se suma a sus grupos.

PC oficina

  1. Instalá NetBird con el instalador de Windows que indica su guía de instalación.

  2. Abrí NetBird, tocá Connect, elegí NetBird Cloud e iniciá sesión en el navegador.

  3. En el panel, Peers → abrí la PC → Assigned Groups: agregá dev-clients y win-build. Anotá su IP: es IP_PC_OFICINA.

  4. Probá desde una terminal nueva de PowerShell:

    powershell
    netbird status
    Test-NetConnection IP_NETBIRD -Port 22    # TcpTestSucceeded debe dar True
    ssh ubuntu@IP_NETBIRD

No pruebes con ping: las políticas solo permiten TCP, así que no va a responder.

PC casa

  1. Instalá NetBird, iniciá sesión y asigná solo dev-clients.

  2. Generá la clave SSH de esa PC con ssh-keygen -t ed25519 -C "pc-casa". La clave pública no es secreta: mandátela por mail.

  3. Desde una sesión ya abierta en el servidor, autorizá esa clave para ubuntu. Si el paso 4 ya está hecho, autorizala también para dev:

    bash
    echo "ssh-ed25519 AAAA...resto-de-la-clave pc-casa" >> ~/.ssh/authorized_keys
  4. Probá ssh ubuntu@IP_NETBIRD desde casa.

Celular

  1. Instalá la app NetBird (iOS o Android) e iniciá sesión con el mismo usuario.
  2. En el panel, asignale el grupo dev-clients.

Sesiones y nombres

  • Los equipos que entran con login tienen vencimiento de sesión: por defecto NetBird pide iniciar sesión de nuevo cada 24 horas. Se cambia en Settings → Authentication, entre 1 hora y 180 días, o se desactiva por equipo en Peers.
  • El servidor también tiene un nombre, devbox.netbird.cloud. En Linux resuelve solo; en Windows y en el celular hay que configurar nameservers en la sección DNS del panel. Con la IP funciona sin configurar nada, y por eso el tutorial usa IP_NETBIRD.

Paso 4

Base del servidor y cierre de SSH

Después de este paso, el servidor no tiene ningún puerto abierto a Internet, el trabajo diario corre con el usuario dev sin sudo y hay 8 GB de swap.

NetBird decide quién entra; desde Internet no pasa nadano pasaTu PC o celularGrupo dev-clientsPolítica de NetBirdPasa: TCP 22 y 1433hacia dev-serversufw en el servidorSolo 22 y 1433,y solo por wt0SSH y SQLTCP 22 y 1433Otro equipoDe la misma cuenta,fuera de dev-clientsInternetCualquier IPFirewall de LightsailSin reglas de entrada:descarta todo

NetBird solo deja pasar al grupo dev-clients y solo a los puertos de la política; ufw repite esa lista en el servidor. Otro equipo de la cuenta no llega, y desde Internet el firewall de Lightsail descarta todo.

  1. Confirmá que entrás por NetBird con ssh ubuntu@IP_NETBIRD. No sigas hasta que funcione.

  2. Subí y ejecutá el script:

    powershell
    scp 02-endurecer.sh ubuntu@IP_NETBIRD:~/
    bash
    chmod +x 02-endurecer.sh
    sudo ./02-endurecer.sh
  3. En Lightsail, Networking → IPv4 Firewall: borrá la regla SSH 22. No debe quedar ninguna regla de entrada.

  4. Comprobá desde afuera. Desconectá NetBird en la PC y corré:

    powershell
    Test-NetConnection IP_ESTATICA -Port 22    # TcpTestSucceeded debe dar False
  5. Reconectá NetBird y verificá que entrás con ssh dev@IP_NETBIRD y con ssh ubuntu@IP_NETBIRD.

Desde ahora trabajás como dev y entrás como ubuntu solo para administrar. Para abrir otro puerto hacia NetBird (el rango de Mosh, o 443 o 7317 si sumás el anexo de Ovis), agregalo a NB_TCP_PORTS o NB_UDP_PORTS en el script, volvé a correrlo y sumalo a la política de NetBird.

Script completo

02-endurecer.sh
texto
#!/usr/bin/env bash
# 02-endurecer.sh
# Base del servidor: usuario de trabajo sin sudo, swap, firewall por puerto
# sobre NetBird y SSH solo con claves.
# Corre despues de comprobar que entras por NetBird (ssh ubuntu@IP_NETBIRD).
# Se puede volver a ejecutar: reconstruye las reglas desde las listas de abajo.
#
# Uso: sudo ./02-endurecer.sh
set -euo pipefail

if [[ $EUID -ne 0 ]]; then
  echo "Ejecuta este script con sudo." >&2
  exit 1
fi

NB_IF="wt0"
WORK_USER="dev"           # usuario para Claude Code, Herdr y los repos (sin sudo)
ADMIN_USER="ubuntu"       # usuario de administracion (con sudo)
NB_TCP_PORTS="22 1433"    # puertos TCP abiertos hacia NetBird; agrega 443 si usas Caddy
NB_UDP_PORTS=""           # ej. "60000:61000" si usas Mosh
SWAP_SIZE="8G"

if ! ip link show "$NB_IF" > /dev/null 2>&1; then
  echo "La interfaz ${NB_IF} no existe. Corre primero 01-netbird-server.sh." >&2
  exit 1
fi

export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y ufw

# --- Usuario de trabajo sin sudo --------------------------------------------
if ! id "$WORK_USER" > /dev/null 2>&1; then
  adduser --disabled-password --gecos "" "$WORK_USER"
fi
if id -nG "$WORK_USER" | tr ' ' '\n' | grep -qx sudo; then
  echo "El usuario ${WORK_USER} esta en el grupo sudo; sacalo antes de seguir." >&2
  exit 1
fi
install -d -m 700 -o "$WORK_USER" -g "$WORK_USER" "/home/${WORK_USER}/.ssh"
if [[ ! -s "/home/${WORK_USER}/.ssh/authorized_keys" ]]; then
  install -m 600 -o "$WORK_USER" -g "$WORK_USER" \
    "/home/${ADMIN_USER}/.ssh/authorized_keys" "/home/${WORK_USER}/.ssh/authorized_keys"
fi
loginctl enable-linger "$WORK_USER"

# --- Swap ---------------------------------------------------------------------
if ! swapon --show=NAME --noheadings | grep -qx /swapfile; then
  if [[ ! -f /swapfile ]]; then
    fallocate -l "$SWAP_SIZE" /swapfile
    chmod 600 /swapfile
    mkswap /swapfile
  fi
  swapon /swapfile
fi
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
echo 'vm.swappiness=10' > /etc/sysctl.d/99-devbox.conf
sysctl --system > /dev/null

# --- Firewall del host --------------------------------------------------------
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
for port in $NB_TCP_PORTS; do
  ufw allow in on "$NB_IF" to any port "$port" proto tcp comment 'NetBird'
done
for range in $NB_UDP_PORTS; do
  ufw allow in on "$NB_IF" to any port "$range" proto udp comment 'NetBird'
done
# SSH publico queda permitido en el host a proposito: el candado es el
# firewall de Lightsail. Si NetBird falla, reabris el 22 desde la consola
# de Lightsail restringido a tu IP y volves a entrar.
ufw allow 22/tcp comment 'SSH de rescate (lo cierra Lightsail)'
ufw --force enable

# --- SSH: solo claves, sin root -----------------------------------------------
cat > /etc/ssh/sshd_config.d/10-devbox.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
EOF
sshd -t
systemctl restart ssh

echo
echo "Listo."
echo "Usuario de trabajo: ${WORK_USER} (sin sudo). Administracion: ${ADMIN_USER}."
swapon --show
echo
ufw status verbose
echo
echo "Falta el paso manual: borrar la regla SSH (22) en el firewall de Lightsail. No debe quedar ninguna regla de entrada."

Si NetBird fallaEn la consola de Lightsail volvé a agregar SSH 22 restringido a tu IP, entrá por la IP pública, revisá netbird status y borrá la regla. Por eso ufw deja el 22 permitido. Si preferís dos capas también para SSH, corré sudo ufw delete allow 22/tcp, sabiendo que el rescate pasa a ser restaurar un snapshot.

Paso 5

Claude Code CLI en el servidor

Claude Code se instala para el usuario dev y el login se completa desde el navegador de tu PC.

  1. Como ubuntu, instalá git: sudo apt install -y git.

  2. Entrá como dev e instalá Claude Code con el instalador nativo, que se actualiza solo:

    bash
    ssh dev@IP_NETBIRD
    curl -fsSL https://claude.ai/install.sh | bash
    source ~/.bashrc
    claude --version
  3. Corré claude y elegí iniciar sesión con tu cuenta de claude.ai (plan Pro, Max, Team o Enterprise).

  4. El servidor no tiene navegador: presioná c para copiar la URL de login, abrila en tu PC y autorizá. El navegador muestra un código; pegalo en el prompt Paste code here if prompted.

  5. Verificá la instalación con claude doctor.

  6. Prepará los repos con una credencial limitada a los repos que va a usar Claude. En GitHub, un token fine-grained con acceso de lectura y escritura solo a esos repos; en otros proveedores, deploy keys por repo:

    bash
    git config --global user.name "Tu Nombre"
    git config --global user.email "tu@correo"
    git config --global credential.helper store
    mkdir -p ~/proyectos && cd ~/proyectos
    git clone https://github.com/TU_ORG/TU_REPO.git    # la primera vez pide usuario y token
  7. En cada repo, guardá las decisiones del proyecto en CLAUDE.md. Claude Code lo lee al iniciar cada sesión.

El login queda en ~/.claude/.credentials.json del usuario dev. Cuando se sumen los otros dos, cada uno entra con su propio usuario Linux y su propia cuenta.

Remote ControlEs lo que vas a usar desde el celular, en el paso 8. Pasa por los servidores de Anthropic, igual que todo el trabajo de Claude Code, y no abre puertos en el servidor.

Paso 6

Herdr y la integración con Claude Code

Herdr mantiene los paneles corriendo en el servidor aunque cierres el cliente, y con la integración relanza cada sesión de Claude después de un reinicio.

Los paneles viven en el servidor; las PC solo se conectan a ellosServidor Herdr en devbox · sigue corriendo sin clientesPC oficinaPC casaCelular (Moshi)Entran con herdr --remote o sshy salen con ctrl+b q.Workspace: proyecto APanel 1 · claudeTarea: funcionalidad nuevaPanel 2 · claudeTarea: corrección de un bugPanel 3 · shellTests, git y sqlcmdWorkspace: proyecto BPanel 1 · claudeTarea en otro repositorioPanel 2 · shellServidor de desarrolloPaneles con Claude: tras un reinicio se relanzan con claude --resume

Los proyectos A y B son ejemplos: cada workspace agrupa los paneles de un repo y cualquiera de los tres clientes ve el mismo estado.

  1. Como dev, instalá Herdr y la integración. La carpeta ~/.claude tiene que existir, por eso va después del paso 5:

    bash
    curl -fsSL https://herdr.dev/install.sh | sh
    source ~/.bashrc
    herdr integration install claude
    herdr integration status
  2. herdr integration status debe mostrar la integración de Claude Code en versión 6 o superior; es el mínimo para restaurar sesiones.

  3. Arrancá Herdr desde la carpeta del proyecto:

    bash
    cd ~/proyectos/TU_REPO
    herdr
  4. Armá un workspace por proyecto y, en cada panel, una sesión de Claude con nombre:

    bash
    claude -n nombre-de-la-tarea     # sesion nueva con nombre
    claude --continue                # retoma la ultima de este directorio
    claude --resume                  # selector de sesiones anteriores

Atajos de Herdr

Todos van después del prefijo ctrl+b.

Acción Tecla tras el prefijo
Crear workspace shift+n
Navegar entre workspaces w
Nueva pestaña c
Pestaña siguiente / anterior n / p
Dividir panel a la derecha v
Dividir panel hacia abajo -
Desconectar el cliente (todo sigue corriendo) q
Ver todos los atajos ?

Qué sobrevive en cada caso

Situación Procesos Layout Conversación de Claude
Desconectar y volver a conectar Siguen corriendo Se mantiene Sigue; nunca se detuvo
Reinicio de Herdr o de la instancia Se cortan Se restaura Herdr la relanza con claude --resume <id>

Tras un reinicio, Herdr vuelve cuando alguien ejecuta herdr; hasta entonces el celular no ve sesiones. Lo que Claude estaba ejecutando se corta: la conversación vuelve y tenés que pedirle que continúe.

  • Si las dos PC escriben en el mismo panel, los comandos se mezclan. Usá paneles distintos.
  • Si dos tareas editan el mismo repo en paralelo, separalas con git worktree add ../TU_REPO-tarea -b tarea y abrí cada panel en su carpeta.
  • Desde Herdr 0.9.4, cerrar la terminal que arrancó Herdr ya no detiene el servidor. Para detenerlo a propósito: herdr server stop.
  • Para actualizar: herdr update. Los plugins instalados desde GitHub se actualizan con herdr plugin update.

Paso 7

Conectarse desde las dos PC Windows

Con herdr --remote devbox cada PC dibuja la interfaz localmente y los paneles siguen viviendo en el servidor. Repetí estos pasos en la PC de la oficina y en la de casa.

  1. Usá Windows Terminal; es la terminal que documenta Herdr para Windows.

  2. Instalá el cliente Herdr desde PowerShell:

    powershell
    powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"
  3. Creá el archivo %USERPROFILE%\.ssh\config con este contenido:

    texto
    Host devbox
      HostName IP_NETBIRD
      User dev
      IdentityFile ~/.ssh/id_ed25519
    
    Host devbox-admin
      HostName IP_NETBIRD
      User ubuntu
      IdentityFile ~/.ssh/id_ed25519
  4. Cargá la clave en el agente SSH de Windows para que la passphrase se pida una sola vez. En PowerShell como administrador:

    powershell
    Get-Service ssh-agent | Set-Service -StartupType Automatic
    Start-Service ssh-agent
    ssh-add $env:USERPROFILE\.ssh\id_ed25519
  5. Con NetBird conectado, aceptá la huella del servidor con ssh devbox y después conectate:

    powershell
    herdr --remote devbox
  6. Salís con ctrl+b q y todo queda corriendo. En Windows Terminal se pega texto con ctrl+shift+v.

Alternativas sobre el mismo servidor:

  • ssh devbox y después herdr: Herdr corre entero en el servidor, como tmux. Es el camino que usa Moshi.
  • Claude Desktop → Environment → SSH → dev@IP_NETBIRD: interfaz gráfica sobre los mismos repos para trabajo corto. No cuentes con que esas sesiones sobrevivan al cierre de la app.

Paso 7b

La PC de la oficina como nodo de build Windows

Claude trabaja en el servidor y, para las apps que necesitan Windows, compila y corre tests en la PC de la oficina por SSH, dentro de NetBird. La PC usa un usuario local build sin privilegios de administrador.

El servidor edita; la PC de la oficina compila lo que necesita Windowsdevbox · usuario devClaude Code edita y hacepush de la ramaPC oficina · usuario buildC:\build\repo: pull,MSBuild y testsRemoto gitLa rama viaja por acá3. ssh winbuild 'msbuild …'4. salida del build y de los tests1. git push2. git pull

Claude no comparte disco con la PC: la rama viaja por git y el build se dispara por SSH. Por SSH no hay escritorio, así que sirve para compilar y para tests sin interfaz; las pruebas con GUI siguen siendo en la PC.

  1. En el servidor, como dev, generá la clave que va a usar el servidor para entrar a la PC. No lleva passphrase porque la usa Claude sin intervención:

    bash
    ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_winbuild -C "devbox-winbuild"
    cat ~/.ssh/id_winbuild.pub
  2. Copiá 06-nodo-windows.ps1 a la PC de la oficina y ejecutalo en PowerShell como administrador, con la clave pública del punto anterior. Pide una sola vez la clave del usuario build:

    powershell
    Set-ExecutionPolicy -Scope Process Bypass
    .\06-nodo-windows.ps1 -PublicKey "ssh-ed25519 AAAA...resto-de-la-clave devbox-winbuild"
  3. En el servidor, agregá esto a ~/.ssh/config del usuario dev y probá:

    texto
    Host winbuild
      HostName IP_PC_OFICINA
      User build
      IdentityFile ~/.ssh/id_winbuild
    bash
    chmod 600 ~/.ssh/config
    ssh winbuild 'hostname; whoami'
  4. Cloná cada repo en la PC con una credencial de solo lectura. La primera vez, entrá de forma interactiva para que git pida el token:

    bash
    ssh -t winbuild
    powershell
    git config --global --replace-all credential.helper ""
    git config --global --add credential.helper store
    git clone https://github.com/TU_ORG/TU_REPO.git C:\build\TU_REPO
  5. En el CLAUDE.md de cada repo que se compila en Windows, dejale a Claude las instrucciones. Ejemplo:

    markdown
    ## Build de Windows
    Este repo se compila en Windows, no en este servidor.
    1. Commit y push de la rama actual.
    2. ssh winbuild 'cd C:\build\TU_REPO; git fetch; git checkout RAMA; git pull'
    3. ssh winbuild 'cd C:\build\TU_REPO; & (& "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1) TU_SOLUCION.sln /p:Configuration=Release'
    4. Tests: ssh winbuild 'cd C:\build\TU_REPO; COMANDO_DE_TESTS'

Script completo

06-nodo-windows.ps1
texto
#Requires -RunAsAdministrator
<#
  06-nodo-windows.ps1
  Prepara una PC Windows como nodo de build para el servidor de desarrollo:
  OpenSSH Server, usuario local "build" sin privilegios de administrador,
  clave publica del servidor, SSH aceptado solo desde la red NetBird,
  PowerShell como shell remoto y sin suspension con corriente.

  Uso (PowerShell como administrador, en la PC):
    .\06-nodo-windows.ps1 -PublicKey "ssh-ed25519 AAAA... devbox-winbuild"
  Pide la clave del usuario build una sola vez.
#>
param(
  [Parameter(Mandatory = $true)][string]$PublicKey,
  [string]$User = "build",
  [string]$BuildDir = "C:\build",
  [string]$NetBirdRange = "100.64.0.0/10"
)
$ErrorActionPreference = "Stop"

# --- OpenSSH Server -------------------------------------------------------------------
$cap = Get-WindowsCapability -Online -Name "OpenSSH.Server*"
if ($cap.State -ne "Installed") {
  Add-WindowsCapability -Online -Name "OpenSSH.Server~~~~0.0.1.0" | Out-Null
}
Set-Service -Name sshd -StartupType Automatic
Start-Service sshd

# PowerShell como shell de las sesiones SSH
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell `
  -Value "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" `
  -PropertyType String -Force | Out-Null

# SSH solo desde la red NetBird
Set-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -RemoteAddress $NetBirdRange -Profile Any

# --- Usuario de build sin privilegios -----------------------------------------------
if (-not (Get-LocalUser -Name $User -ErrorAction SilentlyContinue)) {
  $password = Read-Host -AsSecureString "Clave para el usuario $User"
  New-LocalUser -Name $User -Password $password -PasswordNeverExpires -AccountNeverExpires `
    -Description "Builds remotos del servidor de desarrollo" | Out-Null
}
# Grupo Usuarios por SID (funciona con Windows en cualquier idioma)
$usersGroup = Get-LocalGroup -SID "S-1-5-32-545"
if (-not (Get-LocalGroupMember -Group $usersGroup -Member $User -ErrorAction SilentlyContinue)) {
  Add-LocalGroupMember -Group $usersGroup -Member $User
}

# --- Clave del servidor, fuera del perfil del usuario ----------------------------------
$keysFile = Join-Path $env:ProgramData "ssh\${User}_authorized_keys"
Set-Content -Path $keysFile -Value $PublicKey.Trim() -Encoding ascii
icacls.exe $keysFile /inheritance:r /grant "*S-1-5-32-544:F" /grant "*S-1-5-18:F" | Out-Null

$config = Join-Path $env:ProgramData "ssh\sshd_config"
$marker = "Match User $User"
if (-not (Select-String -Path $config -SimpleMatch $marker -Quiet)) {
  Add-Content -Path $config -Encoding ascii -Value @(
    "",
    $marker,
    "       AuthorizedKeysFile __PROGRAMDATA__/ssh/${User}_authorized_keys"
  )
}
Restart-Service sshd

# --- Carpeta de builds ------------------------------------------------------------------
New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
icacls.exe $BuildDir /grant "${User}:(OI)(CI)M" | Out-Null

# --- Sin suspension con corriente ---------------------------------------------------------
powercfg /change standby-timeout-ac 0
powercfg /change hibernate-timeout-ac 0

Write-Host ""
Write-Host "Listo. Usuario '$User' con SSH por clave, solo desde $NetBirdRange."
Write-Host "Carpeta de builds: $BuildDir"
Write-Host "Prueba desde el servidor: ssh winbuild 'hostname; whoami'"
  • La PC tiene que estar prendida y conectada a NetBird. Si no lo está, los builds de Windows esperan; el resto del trabajo sigue en el servidor.
  • El usuario build no es administrador: si un build necesita instalar algo, lo instalás vos con tu usuario.
  • vswhere encuentra MSBuild aunque no esté en el PATH. Si tu toolchain es otro, reemplazá ese comando por el que uses.

Paso 8

El celular con Remote Control y Moshi

Desde el celular usás Remote Control, la función oficial de Claude Code: la app de Claude muestra las sesiones que corren en el servidor, con chat, aprobaciones y avisos. No se instala nada en el servidor y no abre puertos: Claude Code solo sale por HTTPS hacia Anthropic. Moshi queda de respaldo, para cuando necesitás una terminal.

Mientras Remote Control está conectado, la transcripción de la sesión se guarda en los servidores de Anthropic para sincronizarla entre dispositivos. No suma un tercero nuevo: el trabajo de Claude Code ya pasa por Anthropic.

Remote Control

  1. Instalá la app de Claude en el celular y entrá con la misma cuenta que usaste en el paso 5. Remote Control necesita un plan Pro, Max, Team o Enterprise con login de claude.ai; con API key no funciona.
  2. En el servidor, como dev, abrí claude y corré /config. Activá Enable Remote Control for all sessions: así cada sesión de Claude que abras en Herdr se conecta sola. La primera vez pide una confirmación.
  3. En el mismo /config, activá Push when actions required, para que te avise cuando Claude pide permiso o hace una pregunta, y Push when Claude decides, para que avise cuando termina algo largo.
  4. En la app, tocá Code: las sesiones del servidor aparecen con el ícono de una computadora. Para una sesión puntual, /remote-control dentro de Claude muestra la dirección y un QR.
  5. En claude.ai/settings/account, activá Require trusted devices. Desde ahí, cada celular o navegador nuevo tiene que verificarse antes de poder manejar tus sesiones, y lo revocás desde la misma página si lo perdés.
  • Remote Control vive dentro del proceso claude. Como ese proceso corre en un panel de Herdr, sigue disponible aunque cierres todo en tus PC. Después de un reinicio, Herdr relanza la sesión y se vuelve a conectar sola.
  • Si el panel de Claude no aparece en la app, revisá que el servidor no tenga definida CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: esa variable apaga Remote Control.
  • Si los avisos no llegan y /config dice No mobile registered, abrí la app de Claude en el celular para que registre el dispositivo.

Moshi: terminal de respaldo

Moshi es una terminal SSH nativa para el celular. Sirve para lo que Remote Control no hace: correr comandos, mirar logs o entrar a un panel que no es Claude. La versión gratuita alcanza.

  1. Conectá NetBird en el celular.
  2. En Moshi, creá una conexión a IP_NETBIRD con usuario dev y autenticación por clave. Agregá la clave pública de la app a ~/.ssh/authorized_keys de dev.
  3. Con Herdr corriendo, al conectar aparece una pestaña Herdr con las sesiones. Tocá una para entrar.
  • No instales moshi-hook ni pagues Chat View: el chat, las aprobaciones y los avisos ya los da Remote Control.
  • Si Moshi no detecta Herdr, suele ser el PATH de las sesiones no interactivas. Como ubuntu: sudo ln -sf /home/dev/.local/bin/herdr /usr/local/bin/herdr.
  • Para conexiones inestables, Mosh: sudo apt install -y mosh, el rango 60000:61000 en NB_UDP_PORTS del paso 4 y el rango UDP 60000-61000 en la política de NetBird.

Paso 9

SQL Server de desarrollo

Un script instala SQL Server 2025 Standard Developer sin preguntas, con la collation de producción, 6 GB de memoria máxima y un login administrador propio. SQL Server 2016 no existe para Linux; tus bases de 2016 se restauran en el motor nuevo.

  1. En el SQL Server actual, averiguá la collation de producción:

    sql
    SELECT SERVERPROPERTY('Collation');
  2. Subí y ejecutá el script como ubuntu, con esa collation:

    powershell
    scp 05-sqlserver.sh devbox-admin:~/
    bash
    chmod +x 05-sqlserver.sh
    sudo ./05-sqlserver.sh Modern_Spanish_CI_AS
  3. Al final muestra edición, versión y collation. La edición tiene que decir Standard Developer: así no usás sin querer funciones que los clientes con Standard no tienen.

  4. Para entrar desde la PC en SSMS: servidor IP_NETBIRD,1433, autenticación SQL, usuario devadmin y la opción de confiar en el certificado del servidor. La clave la generó el script:

    powershell
    ssh devbox-admin "sudo cat /root/sql-devadmin.pass"

Script completo

05-sqlserver.sh
texto
#!/usr/bin/env bash
# 05-sqlserver.sh
# Instala SQL Server 2025 (Standard Developer) en Ubuntu 24.04 sin preguntas,
# crea el login administrador devadmin, desactiva sa e instala sqlcmd.
# Las claves se generan solas y quedan en /root/sql-sa.pass y
# /root/sql-devadmin.pass (permisos 600). Nadie tiene que tipearlas.
#
# Uso: sudo ./05-sqlserver.sh <COLLATION>
#   COLLATION: la de produccion, ej. Modern_Spanish_CI_AS o SQL_Latin1_General_CP1_CI_AS.
#   En el SQL actual se consulta con: SELECT SERVERPROPERTY('Collation');
set -euo pipefail

if [[ $EUID -ne 0 ]]; then
  echo "Ejecuta este script con sudo." >&2
  exit 1
fi

COLLATION="${1:-}"
if [[ -z "$COLLATION" ]]; then
  echo "Uso: sudo $0 <COLLATION>   (ej. Modern_Spanish_CI_AS)" >&2
  exit 1
fi

EDITION="StandardDeveloper"
MEMORY_LIMIT_MB="6144"
SA_FILE="/root/sql-sa.pass"
ADMIN_FILE="/root/sql-devadmin.pass"
WORK_USER="dev"
SQLCMD="/opt/mssql-tools18/bin/sqlcmd"

# Clave aleatoria que cumple la politica: mayusculas, minusculas, digitos y simbolo.
new_password() {
  local base
  base="$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9')"
  printf '%sAa1#' "${base:0:24}"
}
for f in "$SA_FILE" "$ADMIN_FILE"; do
  if [[ ! -s "$f" ]]; then
    install -m 600 -o root -g root /dev/null "$f"
    new_password > "$f"
  fi
done

export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y curl gnupg ca-certificates

# --- Repositorios de Microsoft --------------------------------------------------
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
  | gpg --batch --yes --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc > /etc/apt/trusted.gpg.d/microsoft.asc
curl -fsSL https://packages.microsoft.com/config/ubuntu/24.04/mssql-server-2025.list \
  > /etc/apt/sources.list.d/mssql-server-2025.list
curl -fsSL https://packages.microsoft.com/config/ubuntu/24.04/prod.list \
  > /etc/apt/sources.list.d/mssql-release.list
apt-get update

# --- Motor ------------------------------------------------------------------------
apt-get install -y mssql-server
if ! systemctl is-active --quiet mssql-server; then
  MSSQL_PID="$EDITION" ACCEPT_EULA=Y MSSQL_COLLATION="$COLLATION" \
    MSSQL_SA_PASSWORD="$(< "$SA_FILE")" /opt/mssql/bin/mssql-conf -n setup
fi
/opt/mssql/bin/mssql-conf set memory.memorylimitmb "$MEMORY_LIMIT_MB"
systemctl restart mssql-server

# --- Herramientas -------------------------------------------------------------------
ACCEPT_EULA=Y apt-get install -y mssql-tools18 unixodbc-dev
if ! grep -q 'mssql-tools18' "/home/${WORK_USER}/.bashrc" 2> /dev/null; then
  echo 'export PATH="$PATH:/opt/mssql-tools18/bin"' >> "/home/${WORK_USER}/.bashrc"
fi

# --- Espera a que el motor responda ---------------------------------------------------
LOGIN_USER="sa"
LOGIN_FILE="$SA_FILE"
for _ in $(seq 1 60); do
  if SQLCMDPASSWORD="$(< "$SA_FILE")" "$SQLCMD" -S localhost -U sa -C -b -Q "SELECT 1" > /dev/null 2>&1; then
    break
  fi
  if SQLCMDPASSWORD="$(< "$ADMIN_FILE")" "$SQLCMD" -S localhost -U devadmin -C -b -Q "SELECT 1" > /dev/null 2>&1; then
    LOGIN_USER="devadmin"
    LOGIN_FILE="$ADMIN_FILE"
    break
  fi
  sleep 2
done

# --- devadmin y sa desactivado (se puede volver a correr) ---------------------------
ADMIN_PASS="$(< "$ADMIN_FILE")"
SQLCMDPASSWORD="$(< "$LOGIN_FILE")" "$SQLCMD" -S localhost -U "$LOGIN_USER" -C -b -Q "
IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = N'devadmin')
BEGIN
  CREATE LOGIN [devadmin] WITH PASSWORD = N'${ADMIN_PASS}', CHECK_POLICY = ON;
  ALTER SERVER ROLE [sysadmin] ADD MEMBER [devadmin];
END;
ALTER LOGIN [sa] DISABLE;"

echo
SQLCMDPASSWORD="$ADMIN_PASS" "$SQLCMD" -S localhost -U devadmin -C -h -1 -W -Q \
  "SET NOCOUNT ON; SELECT CONCAT(SERVERPROPERTY('Edition'), ' | ', SERVERPROPERTY('ProductVersion'), ' | ', SERVERPROPERTY('Collation'));"
echo
echo "Login administrador: devadmin. Clave en ${ADMIN_FILE} (solo root)."
echo "sa quedo desactivado. Su clave queda en ${SA_FILE} por si hace falta reactivarlo."

Traer una base de SQL Server 2016

Copiá el .bak con scp MiBase.bak devbox-admin:/tmp/ y restauralo:

bash
sudo install -o mssql -g mssql -m 640 /tmp/MiBase.bak /var/opt/mssql/data/MiBase.bak
sql
RESTORE FILELISTONLY FROM DISK = N'/var/opt/mssql/data/MiBase.bak';
GO
RESTORE DATABASE MiBase FROM DISK = N'/var/opt/mssql/data/MiBase.bak'
  WITH MOVE 'NOMBRE_LOGICO_DATOS' TO '/var/opt/mssql/data/MiBase.mdf',
       MOVE 'NOMBRE_LOGICO_LOG' TO '/var/opt/mssql/data/MiBase_log.ldf';
GO
SELECT name, compatibility_level FROM sys.databases;
GO

Los nombres lógicos salen del primer comando. Una base restaurada en 2025 ya no vuelve a 2016. Hacé esta restauración con una base real como primera prueba y corré la aplicación contra ella.

  • Las credenciales de las bases no van en los repos. Guardalas fuera del repo con chmod 600 y cargalas como variables de entorno.
  • Si necesitás otra versión en paralelo, corré la imagen oficial en Docker en otro puerto, por ejemplo 14330 publicado en 127.0.0.1, y entrá por túnel SSH. El 1433 ya lo usa el SQL nativo, y Docker saltea las reglas de ufw.

Paso 10

Backups, monitoreo y ensayo de reinicio

Tres copias con destinos distintos, avisos cuando algo falla y un reinicio ensayado antes de depender del entorno.

Cada cosa que vive en devbox tiene su copia afueraEn devboxFuera de la instanciasnapshot automático diario03-backup-sql.sh, cada nochegit pushInstancia completaSistema, disco y ajustesBases de SQL ServerTodas las bases de usuarioRepositoriosCódigo y CLAUDE.mdSnapshots de LightsailRestaurar crea otra instanciaBucket S3 privadoArchivos .bak comprimidosRemoto gitHistorial completo del código

Lo que no está en ninguno de los tres destinos se pierde si la instancia falla.

Backups de SQL a S3

  1. Creá un bucket S3 privado en São Paulo con versionado activado y un usuario IAM que solo pueda escribir en él, con esta política:

    json
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": "s3:PutObject",
          "Resource": "arn:aws:s3:::NOMBRE_DEL_BUCKET/*"
        }
      ]
    }
  2. En el bucket, agregá una regla de ciclo de vida que borre los objetos después de los días que quieras retener.

  3. Como ubuntu, instalá el CLI de AWS y creá el archivo de configuración:

    bash
    sudo snap install aws-cli --classic
    sudo install -d -m 700 /etc/devbox
    sudo install -m 600 /dev/null /etc/devbox/backup.env
    sudo nano /etc/devbox/backup.env
  4. Contenido de /etc/devbox/backup.env. La clave de SQL no va acá: el script la lee de /root/sql-devadmin.pass.

    bash
    S3_BUCKET='NOMBRE_DEL_BUCKET'
    AWS_ACCESS_KEY_ID='CLAVE_DEL_USUARIO_IAM'
    AWS_SECRET_ACCESS_KEY='SECRETO_DEL_USUARIO_IAM'
    AWS_DEFAULT_REGION='sa-east-1'
    HEALTHCHECK_URL=''    # opcional: URL de un chequeo de healthchecks.io que avise si el backup falla
  5. Instalá el script, probalo a mano y programalo todas las noches a las 03:15. La salida queda en el journal:

    bash
    sudo install -m 750 03-backup-sql.sh /usr/local/sbin/03-backup-sql.sh
    sudo /usr/local/sbin/03-backup-sql.sh
    echo '15 3 * * * root /usr/local/sbin/03-backup-sql.sh 2>&1 | logger -t backup-sql' | sudo tee /etc/cron.d/backup-sql
    journalctl -t backup-sql -n 20 --no-pager

El script respalda las bases de usuario, master y msdb, así los logins y los jobs vuelven con el resto. Si una base falla sigue con las demás, y al final termina con error y avisa al chequeo, si lo configuraste.

Script completo

03-backup-sql.sh
texto
#!/usr/bin/env bash
# 03-backup-sql.sh
# Respalda las bases de SQL Server (las de usuario, master y msdb) y las sube a S3.
# Si una base falla, sigue con las demas y termina con error al final.
# Lee la configuracion de /etc/devbox/backup.env (permisos 600, dueño root)
# y la clave de devadmin de /root/sql-devadmin.pass, que crea 05-sqlserver.sh.
#
# Uso: sudo /usr/local/sbin/03-backup-sql.sh
set -euo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/opt/mssql-tools18/bin"

if [[ $EUID -ne 0 ]]; then
  echo "Ejecuta este script con sudo." >&2
  exit 1
fi

ENV_FILE="/etc/devbox/backup.env"
if [[ ! -r "$ENV_FILE" ]]; then
  echo "Falta ${ENV_FILE}." >&2
  exit 1
fi

set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a

SQL_BACKUP_USER="${SQL_BACKUP_USER:-devadmin}"
SQL_PASSWORD_FILE="${SQL_PASSWORD_FILE:-/root/sql-devadmin.pass}"
: "${S3_BUCKET:?Falta S3_BUCKET en ${ENV_FILE}}"
if [[ ! -r "$SQL_PASSWORD_FILE" ]]; then
  echo "Falta ${SQL_PASSWORD_FILE}." >&2
  exit 1
fi

BACKUP_DIR="${BACKUP_DIR:-/var/opt/mssql/backup}"
KEEP_LOCAL_DAYS="${KEEP_LOCAL_DAYS:-3}"
HEALTHCHECK_URL="${HEALTHCHECK_URL:-}"
STAMP="$(date +%Y%m%d-%H%M%S)"
SQLCMDPASSWORD="$(< "$SQL_PASSWORD_FILE")"
export SQLCMDPASSWORD

ping_healthcheck() {
  [[ -z "$HEALTHCHECK_URL" ]] && return 0
  curl -fsS -m 10 --retry 3 "${HEALTHCHECK_URL}${1:-}" > /dev/null || true
}

install -d -o mssql -g mssql -m 750 "$BACKUP_DIR"

mapfile -t DBS < <(
  sqlcmd -S localhost -U "$SQL_BACKUP_USER" -C -b -h -1 -W \
    -Q "SET NOCOUNT ON; SELECT name FROM sys.databases WHERE name NOT IN ('tempdb', 'model') AND state_desc = 'ONLINE';"
)

ok=0
failed=()
for db in "${DBS[@]}"; do
  db="${db//$'\r'/}"
  [[ -z "$db" ]] && continue
  sql_name="${db//]/]]}"
  file_name="${db//[^A-Za-z0-9_.-]/_}-${STAMP}.bak"
  file="${BACKUP_DIR}/${file_name}"
  sql_file="${file//\'/\'\'}"

  echo "Respaldando ${db} -> ${file}"
  if ! sqlcmd -S localhost -U "$SQL_BACKUP_USER" -C -b \
      -Q "BACKUP DATABASE [${sql_name}] TO DISK = N'${sql_file}' WITH COMPRESSION, CHECKSUM, INIT;"; then
    failed+=("$db (backup)")
    continue
  fi
  if ! aws s3 cp "$file" "s3://${S3_BUCKET}/sql/${file_name%-"${STAMP}".bak}/${file_name}" --only-show-errors; then
    failed+=("$db (subida a S3)")
    continue
  fi
  ok=$((ok + 1))
done

find "$BACKUP_DIR" -name '*.bak' -type f -mtime +"$KEEP_LOCAL_DAYS" -delete

echo "Backups subidos: ${ok}."
if (( ${#failed[@]} > 0 )); then
  printf 'Fallaron: %s\n' "${failed[@]}" >&2
  ping_healthcheck /fail
  exit 1
fi
ping_healthcheck

Snapshots, repos y cuenta

  • Los snapshots automáticos se borran si borrás la instancia. Una vez por mes creá un snapshot manual, que queda aunque la instancia desaparezca.
  • Restaurar un snapshot crea una instancia nueva que conserva la identidad de NetBird del servidor. Apagá la instancia vieja antes de encender la restaurada y volvé a asociarle la IP estática para el rescate.
  • Hacé push seguido. Lo que no está en el remoto git solo vive en el snapshot diario.
  • Activá MFA en la cuenta root de AWS y rotá cada año la clave del usuario IAM de backups: Lightsail no permite roles de instancia.

Actualizaciones y monitoreo

  • Ubuntu instala parches de seguridad solo. Cuando aparece /var/run/reboot-required hace falta reiniciar; elegí un momento fijo de la semana, porque el reinicio corta lo que estén ejecutando los agentes.
  • Las alarmas del paso 1 avisan por CPU, ráfaga y estado. Lightsail no mide el disco: revisá df -h / cuando revises los backups.

Ensayo de reinicio

Hacelo antes de depender del entorno, con una sesión de Claude abierta en Herdr:

bash
sudo reboot

Con NetBird conectado en la PC, entrá con ssh devbox y verificá:

bash
netbird status                      # debe decir Connected sin pedir login
systemctl is-active mssql-server    # debe decir active
swapon --show                       # debe listar /swapfile
herdr                               # vuelve el layout y los paneles de Claude se relanzan

Completá el ensayo bajando un .bak de S3 con tus credenciales de administrador y restaurándolo con otro nombre. Un backup que nunca se restauró no está probado.

Anexo: Ovis y Caddy (opcional)

Ovis, que hasta octubre de 2026 se llamaba herdr-web-ui, muestra las sesiones de Herdr en el navegador del celular o de la PC: el chat de Claude, pero también la terminal en vivo y los paneles que no son Claude. Sumalo solo si Remote Control (paso 8) no te alcanza. Agrega tres piezas: Ovis, Caddy y un dominio en Cloudflare.

Desde el celular Cómo funciona
Chat de Claude Transcripción nativa de cada sesión. Podés escribir mientras Claude trabaja: el mensaje queda para el próximo turno o lo mandás ya con Send now
Aprobaciones y preguntas Tarjetas que se tocan; antes de enviar, verifica que la pregunta siga vigente
Avisos Push cuando Claude termina o necesita algo, con la app cerrada. Solo con la opción B
Terminal A un toque del chat, con una barra de teclas (Esc, Tab, Ctrl, flechas) configurable
Capturas y archivos El botón + sube una captura a la carpeta del panel para que Claude la lea; tocar una ruta abre el archivo
Worktrees Desde el menú ⋯ de un workspace se crea un worktree con su agente
Dictado Usa el reconocimiento de voz del navegador o tu clave de OpenAI; en los dos casos el audio sale de NetBird hacia ese proveedor

Es un proyecto de la comunidad con licencia MIT, no parte oficial de Herdr, y saca versiones casi a diario. El cambio de nombre trajo un fallo de seguridad: las versiones 0.4.14 y 0.4.15 podían dejar entrar dispositivos sin emparejar después de actualizarse, y la 0.4.16 lo corrigió. Por eso el script instala una versión fija (OVIS_VERSION, hoy v0.4.18) y desactiva la actualización automática. Para subir de versión, leé las notas de la versión nueva, cambiá OVIS_VERSION en el script y corrélo con --reinstalar; después probá que siga pidiendo emparejamiento. No uses el botón Update de la app: instala la última versión, no la que fijaste. La terminal nativa y Mosh quedan para Moshi, en el paso 8.

A: HTTP plano B: HTTPS con Caddy (recomendada)
Chat, terminal y aprobaciones Sí Sí
Instalar como app en el celular No Sí
Avisos push No Sí
Qué necesita Abrir el 7317 en NetBird Dominio con DNS en Cloudflare y Caddy (443)

Instalar

El script 07-ovis.sh instala el plugin devswha/ovis, que agrega Bun para tu usuario sin sudo; exige Herdr 0.9.0 o superior. Deja definido el token (OVIS_TOKEN): sin token, Ovis deja entrar a cualquiera que alcance el puerto hasta que emparejás el primer dispositivo. También apaga la telemetría, que viene activada y manda a su autor conteos anónimos de uso, y la actualización automática. Al final muestra un código de emparejamiento.

  1. Como dev, con Herdr corriendo:

    powershell
    scp 07-ovis.sh devbox:~/
    bash
    chmod +x 07-ovis.sh
    ./07-ovis.sh local      # opcion B; para la A: ./07-ovis.sh netbird
  2. Cada código sirve una sola vez y se invalida con cinco intentos fallidos. Para otro dispositivo, ./07-ovis.sh emparejar. Los dispositivos se revocan en Settings → Devices; --nuevo-token rota el token.

La documentación de Ovis muestra un QR y un emparejamiento automático por Tailscale. Con NetBird no aplica: cada dispositivo se empareja con el código. No actives OVIS_TAILSCALE_SERVE_ONLY.

Ovis también tiene apps nativas en beta (TestFlight en iPhone, Google Play en Android). La guía de Ovis solo documenta la app web, así que este tutorial usa esa; si probás las nativas, conectalas a la misma dirección por NetBird.

Si ya habías instalado herdr-web-ui, el script te pide desinstalarlo primero: las instalaciones viejas siguen con el id devswha.herdr-web-ui.

Script completo

07-ovis.sh
texto
#!/usr/bin/env bash
# 07-ovis.sh
# Instala Ovis (antes herdr-web-ui) para el usuario dev, lo deja con token
# obligatorio, sin telemetria ni actualizaciones automaticas, escuchando en la
# direccion elegida; lo reinicia y muestra un codigo de emparejamiento.
# Se corre como dev, sin sudo, con Herdr 0.9.0 o superior ya instalado (paso 6).
# Se puede volver a ejecutar: conserva el token salvo que pidas uno nuevo.
# Instala una version fija (OVIS_VERSION). Para subir de version: lee las notas
# en https://github.com/devswha/ovis/releases, cambia OVIS_VERSION y corre
# el script con --reinstalar.
#
# Uso:
#   ./07-ovis.sh local                 # opcion B: 127.0.0.1, detras de Caddy (recomendada)
#   ./07-ovis.sh netbird               # opcion A: HTTP plano en la IP NetBird, puerto 7317
#   ./07-ovis.sh emparejar             # solo genera un codigo nuevo (un solo uso, 5 intentos)
#   ./07-ovis.sh local --nuevo-token   # rota el token
#   ./07-ovis.sh local --reinstalar    # instala OVIS_VERSION sobre la version actual
set -euo pipefail

OVIS_VERSION="v0.4.18"    # version fija; cambiala a proposito, despues de leer las notas
REPO="devswha/ovis"
PLUGIN="devswha.ovis"
OLD_PLUGIN="devswha.herdr-web-ui"
INSTALL_URL="https://ovis.sh/install.sh"
PORT="7317"
NB_IF="wt0"

MODE="${1:-}"
NEW_TOKEN="no"
REINSTALL="no"
for arg in "${@:2}"; do
  case "$arg" in
    --nuevo-token) NEW_TOKEN="si" ;;
    --reinstalar) REINSTALL="si" ;;
    *)
      echo "Opcion desconocida: ${arg}" >&2
      exit 1
      ;;
  esac
done

if [[ $EUID -eq 0 ]]; then
  echo "Corre este script como dev, sin sudo." >&2
  exit 1
fi
case "$MODE" in
  local | netbird | emparejar) ;;
  *)
    echo "Uso: $0 local|netbird|emparejar [--nuevo-token] [--reinstalar]" >&2
    exit 1
    ;;
esac

export PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH"
if ! command -v herdr > /dev/null 2>&1; then
  echo "No encuentro herdr. Instalalo primero (paso 6)." >&2
  exit 1
fi

plugin_installed() {
  herdr plugin action invoke "${1}.status" > /dev/null 2>&1
}

# Instalaciones previas al cambio de nombre siguen con el id viejo.
if ! plugin_installed "$PLUGIN" && plugin_installed "$OLD_PLUGIN"; then
  echo "Encontre la instalacion vieja (${OLD_PLUGIN}). Desinstalala y volve a correr este script:" >&2
  echo "  herdr plugin action invoke ${OLD_PLUGIN}.stop" >&2
  echo "  herdr plugin uninstall ${OLD_PLUGIN}" >&2
  exit 1
fi

plugin_root() {
  local root=""
  if command -v bun > /dev/null 2>&1; then
    root="$(herdr plugin list --json 2> /dev/null | bun -e \
      'const d = JSON.parse(await Bun.stdin.text()); console.log(d.result.plugins.find((p) => p.plugin_id === "devswha.ovis")?.plugin_root ?? "")' \
      2> /dev/null || true)"
  fi
  if [[ -z "$root" ]]; then
    root="$(ls -d "$HOME"/.config/herdr/plugins/github/${PLUGIN}-* 2> /dev/null | head -1 || true)"
  fi
  printf '%s' "$root"
}

pair() {
  local root
  root="$(plugin_root)"
  if [[ -z "$root" || ! -f "$root/scripts/plugin.ts" ]]; then
    echo "No encuentro el plugin ${PLUGIN} instalado." >&2
    exit 1
  fi
  echo
  echo "Codigo de emparejamiento (un solo uso; cinco intentos fallidos lo invalidan):"
  bun "$root/scripts/plugin.ts" pair
}

if [[ "$MODE" == "emparejar" ]]; then
  pair
  exit 0
fi

# --- Direccion de escucha ---------------------------------------------------------
if [[ "$MODE" == "netbird" ]]; then
  HOST_ADDR="$(ip -4 -o addr show "$NB_IF" 2> /dev/null | awk '{print $4}' | cut -d/ -f1)"
  if [[ -z "$HOST_ADDR" ]]; then
    echo "La interfaz ${NB_IF} no tiene direccion. Revisa 'netbird status'." >&2
    exit 1
  fi
else
  HOST_ADDR="127.0.0.1"
fi

# --- Instalacion de la version fija (Bun para tu usuario, sin sudo) -------------------
if ! plugin_installed "$PLUGIN"; then
  curl -fsSL "$INSTALL_URL" | OVIS_REF="$OVIS_VERSION" sh
  export PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH"
fi
if [[ "$REINSTALL" == "si" ]]; then
  herdr plugin install "$REPO" --ref "$OVIS_VERSION" --yes < /dev/null
fi

# --- Configuracion, sin duplicar lineas ------------------------------------------------
CONF_DIR="$(herdr plugin config-dir "$PLUGIN")"
mkdir -p "$CONF_DIR"
ENV_FILE="$CONF_DIR/env"
[[ -f "$CONF_DIR/.env" ]] && ENV_FILE="$CONF_DIR/.env"
touch "$ENV_FILE"
chmod 600 "$ENV_FILE"

TOKEN="$(grep -E '^(OVIS_TOKEN|HERDR_WEB_TOKEN)=' "$ENV_FILE" | tail -1 | cut -d= -f2- || true)"
if [[ -z "$TOKEN" || "$NEW_TOKEN" == "si" ]]; then
  TOKEN="$(openssl rand -hex 24)"
fi

TMP_FILE="$(mktemp "$CONF_DIR/env.XXXXXX")"
grep -v -E '^(HOST|PORT|OVIS_TOKEN|HERDR_WEB_TOKEN|OVIS_TELEMETRY|OVIS_AUTO_UPDATE|HERDR_WEB_AUTO_UPDATE|OVIS_TAILSCALE_SERVE_ONLY|HERDR_WEB_TAILSCALE_SERVE_ONLY)=' \
  "$ENV_FILE" > "$TMP_FILE" || true
{
  printf 'HOST=%s\n' "$HOST_ADDR"
  printf 'PORT=%s\n' "$PORT"
  printf 'OVIS_TOKEN=%s\n' "$TOKEN"
  printf 'OVIS_TELEMETRY=0\n'
  printf 'OVIS_AUTO_UPDATE=0\n'
} >> "$TMP_FILE"
chmod 600 "$TMP_FILE"
mv "$TMP_FILE" "$ENV_FILE"

# --- Reinicio y estado ----------------------------------------------------------------
herdr plugin action invoke "${PLUGIN}.stop" > /dev/null 2>&1 || true
herdr plugin action invoke "${PLUGIN}.start"
herdr plugin action invoke "${PLUGIN}.status"

echo
echo "Ovis ${OVIS_VERSION} escucha en ${HOST_ADDR}:${PORT} con token, sin telemetria ni actualizacion automatica."
echo "No actualices desde Settings -> Updates: instala la ultima, no la fija. Usa --reinstalar."
echo "Configuracion: ${ENV_FILE}"
if [[ "$MODE" == "netbird" ]]; then
  echo "Opcion A: agrega ${PORT} a NB_TCP_PORTS (paso 4) y a la politica dev-access."
  echo "Abri http://${HOST_ADDR}:${PORT} desde un equipo con NetBird."
else
  echo "Opcion B: falta Caddy (04-caddy-webui.sh) para abrirlo por HTTPS en NetBird."
fi
pair

Opción A: HTTP plano por NetBird

  1. Corré ./07-ovis.sh netbird.
  2. Agregá 7317 a NB_TCP_PORTS del paso 4, volvé a correr 02-endurecer.sh y sumá TCP 7317 a la política dev-access.
  3. Con NetBird conectado, abrí http://IP_NETBIRD:7317 e ingresá el código.

Opción B: HTTPS con Caddy y certificado propio

HTTPS dentro de NetBird: el certificado se valida por DNS, sin abrir puertosHTTPSHTTPcrea el registro de validaciónNavegadorPC o celular,con NetBird activoCaddyIP_NETBIRD, puerto 443,certificado del dominioOvis127.0.0.1:7317,solo localHerdrPanelesDNS de tu dominioRegistro A que apuntaa IP_NETBIRDLet’s EncryptValida por DNS,sin abrir puertos

El navegador entra por NetBird a Caddy, que es el único que escucha en la red; Ovis sigue solo en local. Caddy obtiene el certificado creando un registro en el DNS del dominio, por eso no hace falta abrir ningún puerto a Internet.

  1. Usá una zona de Cloudflare dedicada a este entorno, por ejemplo un dominio barato solo para esto. El token de Caddy puede editar toda la zona, y no conviene que alcance el DNS de la empresa.

  2. En esa zona, creá un registro A devbox.TU_DOMINIO que apunte a IP_NETBIRD, en modo DNS only, sin proxy. El nombre resuelve para cualquiera, pero solo responde dentro de NetBird.

  3. Creá un token de API de Cloudflare con permisos Zone:Read y DNS:Edit, limitado a esa zona, y guardalo como ubuntu en un archivo que solo lea root:

    bash
    sudo install -m 600 /dev/null /root/cf-token
    sudo nano /root/cf-token
  4. Corré ./07-ovis.sh local como dev. Agregá 443 a NB_TCP_PORTS del paso 4, volvé a correr 02-endurecer.sh y sumá TCP 443 a la política dev-access.

  5. Como ubuntu, subí y ejecutá el script de Caddy:

    powershell
    scp 04-caddy-webui.sh devbox-admin:~/
    bash
    chmod +x 04-caddy-webui.sh
    sudo ./04-caddy-webui.sh devbox.TU_DOMINIO /root/cf-token
    sudo rm /root/cf-token
  6. Verificá que el certificado salió y abrí https://devbox.TU_DOMINIO:

    bash
    sudo journalctl -u caddy -n 30 --no-pager    # busca "certificate obtained successfully"
  7. Emparejá cada dispositivo. En el celular, agregá la página a la pantalla de inicio: así se instala como app y recibe avisos.

Script completo

04-caddy-webui.sh
texto
#!/usr/bin/env bash
# 04-caddy-webui.sh
# Publica Ovis (antes herdr-web-ui) por HTTPS dentro de NetBird, con Caddy y un certificado
# real obtenido por desafio DNS de Cloudflare. No abre ningun puerto a Internet:
# Caddy escucha solo en la IP NetBird del servidor, puerto 443.
#
# Antes de correrlo:
#   - Registro A  <DOMINIO> -> IP NetBird del servidor, en modo "DNS only".
#   - Token de API de Cloudflare con permisos Zone:Read y DNS:Edit, limitado a una
#     zona dedicada a este entorno (no la zona principal de la empresa).
#   - El token guardado en un archivo de una sola linea, legible solo por root.
#   - Ovis instalado con 07-ovis.sh, escuchando en 127.0.0.1 y con token (anexo de Ovis).
#   - 443 agregado a NB_TCP_PORTS en 02-endurecer.sh, y ese script vuelto a correr.
#
# Uso: sudo ./04-caddy-webui.sh <DOMINIO> <ARCHIVO_TOKEN>
set -euo pipefail

if [[ $EUID -ne 0 ]]; then
  echo "Ejecuta este script con sudo." >&2
  exit 1
fi

DOMAIN="${1:-}"
TOKEN_FILE="${2:-}"
if [[ -z "$DOMAIN" || -z "$TOKEN_FILE" ]]; then
  echo "Uso: sudo $0 <DOMINIO> <ARCHIVO_TOKEN>   (ej. devbox.midominio.com /root/cf-token)" >&2
  exit 1
fi
if [[ ! -r "$TOKEN_FILE" ]]; then
  echo "No puedo leer ${TOKEN_FILE}." >&2
  exit 1
fi

NB_IF="wt0"
WEBUI_ADDR="127.0.0.1:7317"
ENV_FILE="/etc/caddy/cloudflare.env"
CADDYFILE="/etc/caddy/Caddyfile"
DNS_MODULE="github.com/caddy-dns/cloudflare"

NB_IP="$(ip -4 -o addr show "$NB_IF" 2> /dev/null | awk '{print $4}' | cut -d/ -f1)"
if [[ -z "$NB_IP" ]]; then
  echo "La interfaz ${NB_IF} no tiene direccion. Revisa 'netbird status'." >&2
  exit 1
fi

CF_API_TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
if [[ -z "$CF_API_TOKEN" ]]; then
  echo "El archivo ${TOKEN_FILE} esta vacio." >&2
  exit 1
fi

# --- Caddy desde el repositorio oficial ----------------------------------
export DEBIAN_FRONTEND=noninteractive
if ! command -v caddy > /dev/null 2>&1; then
  apt-get update
  apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl gnupg
  curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
    | gpg --dearmor --yes -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
  curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
    > /etc/apt/sources.list.d/caddy-stable.list
  chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
  chmod o+r /etc/apt/sources.list.d/caddy-stable.list
  apt-get update
  apt-get install -y caddy
fi

# --- Modulo DNS de Cloudflare ---------------------------------------------
# add-package reemplaza el binario por uno que incluye el modulo. Se fija el
# paquete para que apt no lo pise; para actualizar: sudo caddy upgrade.
if ! caddy list-modules | grep -q '^dns.providers.cloudflare$'; then
  systemctl stop caddy || true
  caddy add-package "$DNS_MODULE"
fi
apt-mark hold caddy > /dev/null

# --- Token, legible solo por root -----------------------------------------
install -m 600 -o root -g root /dev/null "$ENV_FILE"
printf 'CF_API_TOKEN=%s\n' "$CF_API_TOKEN" > "$ENV_FILE"

# --- Caddyfile --------------------------------------------------------------
if [[ -f "$CADDYFILE" ]] && ! grep -q "reverse_proxy ${WEBUI_ADDR}" "$CADDYFILE"; then
  cp "$CADDYFILE" "${CADDYFILE}.bak.$(date +%Y%m%d-%H%M%S)"
fi
cat > "$CADDYFILE" <<EOF
{
	# Sin redirecciones HTTP: no se abre el puerto 80 en ninguna interfaz.
	auto_https disable_redirects
}

${DOMAIN} {
	bind ${NB_IP}
	tls {
		dns cloudflare {env.CF_API_TOKEN}
	}
	# Caddy ya manda Host, X-Forwarded-For y X-Forwarded-Proto, y pasa WebSocket.
	reverse_proxy ${WEBUI_ADDR} {
		header_up -Tailscale-User-Login
	}
}
EOF

caddy validate --config "$CADDYFILE" --adapter caddyfile --envfile "$ENV_FILE" > /dev/null

# --- Servicio: lee el token y reintenta si NetBird todavia no levanto ------
install -d /etc/systemd/system/caddy.service.d
cat > /etc/systemd/system/caddy.service.d/devbox.conf <<EOF
[Unit]
After=netbird.service

[Service]
EnvironmentFile=${ENV_FILE}
Restart=on-failure
RestartSec=5s
EOF

systemctl daemon-reload
systemctl enable caddy > /dev/null
systemctl restart caddy

echo
echo "Caddy escucha en https://${DOMAIN} (${NB_IP}:443) y reenvia a ${WEBUI_ADDR}."
echo "Seguimiento del certificado:  sudo journalctl -u caddy -n 30 --no-pager"
echo "Falta en NetBird: agregar TCP 443 a la politica dev-access."
echo "Podes borrar ${TOKEN_FILE}: Caddy usa la copia de ${ENV_FILE}."
  • Con otro proveedor de DNS cambian el módulo y la línea dns del Caddyfile.
  • El Reverse Proxy de NetBird (beta) no reemplaza a Caddy en este esquema. En los clusters compartidos de NetBird Cloud el servicio queda publicado en Internet, protegido por login. El modo que lo deja solo dentro de NetBird exige correr tu propio cluster de proxy con Docker, dominio y registros DNS: más piezas que Caddy.
  • caddy add-package, el comando que agrega el módulo, está marcado como experimental en la documentación de Caddy. El paquete queda fijado con apt-mark hold y no recibe parches por apt: actualizalo cada tanto con sudo caddy upgrade && sudo systemctl restart caddy.
  • En iPhone, los avisos exigen iOS 16.4 o superior y la app agregada a la pantalla de inicio. Los avisos viajan cifrados por el servicio de push de Apple o Google.
  • Si el dominio no resuelve en alguna red, el router puede estar filtrando respuestas DNS con direcciones privadas. Probá con otra red o configurá el nombre en la sección DNS del panel de NetBird.

Checklist final y problemas frecuentes

El entorno está listo cuando estas comprobaciones dan bien.

0 de 11 comprobadas

Síntoma Causa probable Qué hacer
NetBird dice Connected pero ssh no llega El equipo no está en el grupo o la política no está activa Asigná el grupo en Peers y revisá Access Control. ping nunca responde: las políticas solo permiten TCP
La PC pide iniciar sesión en NetBird todos los días Vencimiento de sesión, 24 horas por defecto Ampliá el período en Settings → Authentication o desactivalo para ese equipo en Peers
Un puerto nuevo no responde por NetBird Falta en NB_TCP_PORTS o en la política Agregalo en los dos lugares y volvé a correr 02-endurecer.sh
ssh devbox rechaza la conexión La clave de esa PC no está en authorized_keys de dev Agregala desde una sesión de ubuntu
ssh winbuild pide clave o rechaza Falta el bloque Match User build o los permisos del archivo de claves Volvé a correr 06-nodo-windows.ps1 en la PC
El build de Windows no encuentra MSBuild No está en el PATH del usuario build Usá vswhere como en el ejemplo del paso 7b
herdr --remote pide la passphrase a cada rato El agente SSH de Windows no tiene la clave Repetí el punto 4 del paso 7
Tras un reinicio, un panel vuelve como shell y no como Claude Integración desactualizada o sesión sin registrar herdr integration install claude y en el panel claude --resume
claude pide iniciar sesión otra vez Credenciales vencidas Corré claude auth login y completá el login como en el paso 5
El celular no recibe avisos de Claude Avisos apagados o el celular no está registrado Activá los avisos en /config; si dice No mobile registered, abrí la app de Claude en el celular
Quedaste afuera porque NetBird no conecta Cliente caído, sesión vencida o política cambiada Rescate del paso 4: reabrí SSH 22 a tu IP desde la consola de Lightsail

Fuentes

Comandos y límites verificados contra estas páginas entre el 8 y el 26 de octubre de 2026. Los scripts de bash pasaron chequeo de sintaxis y shellcheck, y la estructura del Caddyfile se validó con Caddy; ningún script se ejecutó todavía contra una instancia real.

MSSQL_PID=StandardDeveloper sale de la tabla de ediciones de SQL Server 2025 y no está en los ejemplos de Microsoft: el script muestra la edición instalada para confirmarlo. La configuración de ufw, Lightsail y el script de Windows sale de práctica estándar y no se probó en una PC real.