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.
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:
ubuntuadministra y tiene sudo;devtrabaja sin sudo. Claude Code, Herdr y los repos corren comodev, 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_NETBIRDes la dirección NetBird del servidor eIP_PC_OFICINAla de la PC de la oficina. Las dos están en el rango100.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.
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.
-
En la PC de la oficina, generá tu clave SSH en PowerShell y ponele passphrase:
powershellssh-keygen -t ed25519 -C "pc-oficina" Get-Content $env:USERPROFILE\.ssh\id_ed25519.pub -
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 nombraladevbox. -
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.
-
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. -
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.
-
Borrá la regla HTTP 80. En IPv6 Firewall borrá todas las reglas.
-
Pestaña Snapshots → activá los snapshots automáticos diarios.
-
Pestaña Metrics → creá alarmas con aviso por mail para uso de CPU, capacidad de ráfaga y fallas de chequeo de estado.
-
Entrá y actualizá el sistema sin preguntas. Cambiá la zona horaria por la tuya:
bashssh 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
-
Creá una cuenta de NetBird dedicada a este entorno.
-
Access Control → Groups → Create Group: creá
dev-servers,dev-clientsywin-build. -
Access Control → Policies → Add policy: creá estas dos, unidireccionales:
Política Source Destination Protocolo Puertos dev-accessdev-clientsdev-serversTCP 22 y 1433 build-accessdev-serverswin-buildTCP 22 -
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.
-
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
-
Subí el script:
powershellscp 01-netbird-server.sh ubuntu@IP_ESTATICA:~/ -
Ejecutalo con la setup key como argumento:
bashchmod +x 01-netbird-server.sh sudo ./01-netbird-server.sh SETUP_KEY -
Al final muestra
netbird statusy la IP NetBird del servidor. Anotala: esIP_NETBIRDen el resto del tutorial. -
En el panel, Peers: el servidor aparece dentro de
dev-servers. Renombralo adevboxpara 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
#!/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
fiPaso 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
-
Instalá NetBird con el instalador de Windows que indica su guía de instalación.
-
Abrí NetBird, tocá Connect, elegí NetBird Cloud e iniciá sesión en el navegador.
-
En el panel, Peers → abrí la PC → Assigned Groups: agregá
dev-clientsywin-build. Anotá su IP: esIP_PC_OFICINA. -
Probá desde una terminal nueva de PowerShell:
powershellnetbird 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
-
Instalá NetBird, iniciá sesión y asigná solo
dev-clients. -
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. -
Desde una sesión ya abierta en el servidor, autorizá esa clave para
ubuntu. Si el paso 4 ya está hecho, autorizala también paradev:bashecho "ssh-ed25519 AAAA...resto-de-la-clave pc-casa" >> ~/.ssh/authorized_keys -
Probá
ssh ubuntu@IP_NETBIRDdesde casa.
Celular
- Instalá la app NetBird (iOS o Android) e iniciá sesión con el mismo usuario.
- 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 usaIP_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 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.
-
Confirmá que entrás por NetBird con
ssh ubuntu@IP_NETBIRD. No sigas hasta que funcione. -
Subí y ejecutá el script:
powershellscp 02-endurecer.sh ubuntu@IP_NETBIRD:~/bashchmod +x 02-endurecer.sh sudo ./02-endurecer.sh -
En Lightsail, Networking → IPv4 Firewall: borrá la regla SSH 22. No debe quedar ninguna regla de entrada.
-
Comprobá desde afuera. Desconectá NetBird en la PC y corré:
powershellTest-NetConnection IP_ESTATICA -Port 22 # TcpTestSucceeded debe dar False -
Reconectá NetBird y verificá que entrás con
ssh dev@IP_NETBIRDy conssh 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
#!/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.
-
Como
ubuntu, instalá git:sudo apt install -y git. -
Entrá como
deve instalá Claude Code con el instalador nativo, que se actualiza solo:bashssh dev@IP_NETBIRD curl -fsSL https://claude.ai/install.sh | bash source ~/.bashrc claude --version -
Corré
claudey elegí iniciar sesión con tu cuenta de claude.ai (plan Pro, Max, Team o Enterprise). -
El servidor no tiene navegador: presioná
cpara copiar la URL de login, abrila en tu PC y autorizá. El navegador muestra un código; pegalo en el promptPaste code here if prompted. -
Verificá la instalación con
claude doctor. -
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:
bashgit 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 -
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 proyectos A y B son ejemplos: cada workspace agrupa los paneles de un repo y cualquiera de los tres clientes ve el mismo estado.
-
Como
dev, instalá Herdr y la integración. La carpeta~/.claudetiene que existir, por eso va después del paso 5:bashcurl -fsSL https://herdr.dev/install.sh | sh source ~/.bashrc herdr integration install claude herdr integration status -
herdr integration statusdebe mostrar la integración de Claude Code en versión 6 o superior; es el mínimo para restaurar sesiones. -
Arrancá Herdr desde la carpeta del proyecto:
bashcd ~/proyectos/TU_REPO herdr -
Armá un workspace por proyecto y, en cada panel, una sesión de Claude con nombre:
bashclaude -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 tareay 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 conherdr 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.
-
Usá Windows Terminal; es la terminal que documenta Herdr para Windows.
-
Instalá el cliente Herdr desde PowerShell:
powershellpowershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex" -
Creá el archivo
%USERPROFILE%\.ssh\configcon este contenido:textoHost devbox HostName IP_NETBIRD User dev IdentityFile ~/.ssh/id_ed25519 Host devbox-admin HostName IP_NETBIRD User ubuntu IdentityFile ~/.ssh/id_ed25519 -
Cargá la clave en el agente SSH de Windows para que la passphrase se pida una sola vez. En PowerShell como administrador:
powershellGet-Service ssh-agent | Set-Service -StartupType Automatic Start-Service ssh-agent ssh-add $env:USERPROFILE\.ssh\id_ed25519 -
Con NetBird conectado, aceptá la huella del servidor con
ssh devboxy después conectate:powershellherdr --remote devbox -
Salís con
ctrl+b qy todo queda corriendo. En Windows Terminal se pega texto conctrl+shift+v.
Alternativas sobre el mismo servidor:
ssh devboxy despuésherdr: 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.
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.
-
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:bashssh-keygen -t ed25519 -N '' -f ~/.ssh/id_winbuild -C "devbox-winbuild" cat ~/.ssh/id_winbuild.pub -
Copiá
06-nodo-windows.ps1a 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 usuariobuild:powershellSet-ExecutionPolicy -Scope Process Bypass .\06-nodo-windows.ps1 -PublicKey "ssh-ed25519 AAAA...resto-de-la-clave devbox-winbuild" -
En el servidor, agregá esto a
~/.ssh/configdel usuariodevy probá:textoHost winbuild HostName IP_PC_OFICINA User build IdentityFile ~/.ssh/id_winbuildbashchmod 600 ~/.ssh/config ssh winbuild 'hostname; whoami' -
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:
bashssh -t winbuildpowershellgit 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 -
En el
CLAUDE.mdde 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
#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
buildno es administrador: si un build necesita instalar algo, lo instalás vos con tu usuario. vswhereencuentra 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
- 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.
- En el servidor, como
dev, abríclaudey 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. - 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. - En la app, tocá Code: las sesiones del servidor aparecen con el ícono de una computadora. Para una sesión puntual,
/remote-controldentro de Claude muestra la dirección y un QR. - 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
/configdice 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.
- Conectá NetBird en el celular.
- En Moshi, creá una conexión a
IP_NETBIRDcon usuariodevy autenticación por clave. Agregá la clave pública de la app a~/.ssh/authorized_keysdedev. - Con Herdr corriendo, al conectar aparece una pestaña Herdr con las sesiones. Tocá una para entrar.
- No instales
moshi-hookni 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 rango60000:61000enNB_UDP_PORTSdel 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.
-
En el SQL Server actual, averiguá la collation de producción:
sqlSELECT SERVERPROPERTY('Collation'); -
Subí y ejecutá el script como
ubuntu, con esa collation:powershellscp 05-sqlserver.sh devbox-admin:~/bashchmod +x 05-sqlserver.sh sudo ./05-sqlserver.sh Modern_Spanish_CI_AS -
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.
-
Para entrar desde la PC en SSMS: servidor
IP_NETBIRD,1433, autenticación SQL, usuariodevadminy la opción de confiar en el certificado del servidor. La clave la generó el script:powershellssh devbox-admin "sudo cat /root/sql-devadmin.pass"
Script completo
#!/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:
sudo install -o mssql -g mssql -m 640 /tmp/MiBase.bak /var/opt/mssql/data/MiBase.bakRESTORE 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;
GOLos 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 600y 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 deufw.
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.
Lo que no está en ninguno de los tres destinos se pierde si la instancia falla.
Backups de SQL a S3
-
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/*" } ] } -
En el bucket, agregá una regla de ciclo de vida que borre los objetos después de los días que quieras retener.
-
Como
ubuntu, instalá el CLI de AWS y creá el archivo de configuración:bashsudo 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 -
Contenido de
/etc/devbox/backup.env. La clave de SQL no va acá: el script la lee de/root/sql-devadmin.pass.bashS3_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 -
Instalá el script, probalo a mano y programalo todas las noches a las 03:15. La salida queda en el journal:
bashsudo 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
#!/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_healthcheckSnapshots, 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-requiredhace 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:
sudo rebootCon NetBird conectado en la PC, entrá con ssh devbox y verificá:
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 relanzanCompletá 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.
-
Como
dev, con Herdr corriendo:powershellscp 07-ovis.sh devbox:~/bashchmod +x 07-ovis.sh ./07-ovis.sh local # opcion B; para la A: ./07-ovis.sh netbird -
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-tokenrota 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
#!/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
pairOpción A: HTTP plano por NetBird
- Corré
./07-ovis.sh netbird. - Agregá 7317 a
NB_TCP_PORTSdel paso 4, volvé a correr02-endurecer.shy sumá TCP 7317 a la políticadev-access. - Con NetBird conectado, abrí
http://IP_NETBIRD:7317e ingresá el código.
Opción B: HTTPS con Caddy y certificado propio
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.
-
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.
-
En esa zona, creá un registro A
devbox.TU_DOMINIOque apunte aIP_NETBIRD, en modo DNS only, sin proxy. El nombre resuelve para cualquiera, pero solo responde dentro de NetBird. -
Creá un token de API de Cloudflare con permisos Zone:Read y DNS:Edit, limitado a esa zona, y guardalo como
ubuntuen un archivo que solo lea root:bashsudo install -m 600 /dev/null /root/cf-token sudo nano /root/cf-token -
Corré
./07-ovis.sh localcomodev. Agregá 443 aNB_TCP_PORTSdel paso 4, volvé a correr02-endurecer.shy sumá TCP 443 a la políticadev-access. -
Como
ubuntu, subí y ejecutá el script de Caddy:powershellscp 04-caddy-webui.sh devbox-admin:~/bashchmod +x 04-caddy-webui.sh sudo ./04-caddy-webui.sh devbox.TU_DOMINIO /root/cf-token sudo rm /root/cf-token -
Verificá que el certificado salió y abrí
https://devbox.TU_DOMINIO:bashsudo journalctl -u caddy -n 30 --no-pager # busca "certificate obtained successfully" -
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
#!/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
dnsdel 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 conapt-mark holdy no recibe parches por apt: actualizalo cada tanto consudo 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.
- Lightsail: nuevas regiones, incluida São Paulo
- Lightsail: rendimiento base de CPU
- Lightsail: snapshots automáticos
- Lightsail: precios
- Anthropic: términos para consumidores
- NetBird: instalación en Linux
- NetBird: instalación en Windows
- NetBird: setup keys
- NetBird: grupos y políticas de acceso
- NetBird: puertos y firewalls
- NetBird: DNS
- NetBird: vencimiento de sesión
- NetBird: rango de direcciones
- NetBird: precios
- Herdr: instalación
- Herdr: integraciones
- Herdr: inicio rápido y atajos
- Herdr: cómo trabajar
- Herdr: persistencia y acceso remoto
- Herdr: estado y restauración de sesiones
- Herdr: soporte en Windows
- Claude Code: instalación
- Claude Code: autenticación
- Claude Code: sesiones
- Claude Code: Remote Control
- Claude Code: requisitos de red
- Moshi: instalar moshi-hook
- Moshi: Chat View
- Moshi: Herdr
- Ovis: sitio
- Ovis: repositorio
- Ovis: guía
- Ovis: versiones
- Herdr: versiones
- NetBird: versiones
- Claude Code: cambios
- Caddy: instalación
- Caddy: línea de comandos
- Caddy: opciones globales
- Caddy: reverse_proxy
- Caddy: módulo DNS de Cloudflare
- Microsoft: SQL Server en Linux, guía de instalación
- Microsoft: instalar SQL Server en Ubuntu
- Microsoft: variables de entorno de SQL Server en Linux
- Microsoft: OpenSSH en Windows
- Microsoft: claves de OpenSSH en Windows
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.