# Monta una red de Minecraft con Workers
Una red de juego tiene una puerta y muchas salas. Los jugadores se conectan a **una sola dirección pública**; detrás, un proxy (Velocity o BungeeCord) envía a cada jugador a un lobby, un minijuego o un servidor survival. En Zaroz son dos tipos de Worker en **un mismo proyecto**:

```text
jugadores ──TCP 25565──▶  Worker proxy   (1 unidad, un puerto público)
                             │  nombres privados, nunca en internet
                             ├──▶ u1.lobby:25565
                             ├──▶ u2.lobby:25565      Worker lobby  (N unidades, sin puerto público)
                             └──▶ u3.lobby:25565
                             └──▶ u1.games:25565      Worker games  (N unidades, sin puerto público)
```

- El **Worker proxy** ejecuta una unidad y posee el único puerto público.
- Cada **Worker backend** ejecuta tantas unidades como necesites. No tiene dirección pública: el proxy llega a cada unidad por su [nombre privado](/es/units/private-names), `<clave>.<slug>`.
- El proxy encuentra los backends **mediante la API de Units**, así que subir o bajar un backend no exige editar ninguna configuración.

Un Worker es el producto para cualquier programa de larga duración que no sea una web. Puede tener puertos públicos TCP y UDP raw y puede ejecutar muchas unidades. Los endpoints HTTPS son para webs y un Worker no los ofrece. Los términos se explican en [Units](/es/units), [Puertos públicos](/es/units/endpoints) y [Nombres privados](/es/units/private-names).

:::note{title="Antes de empezar"}
Necesitas una organización con un proyecto, una imagen de proxy de Minecraft y una imagen de backend en un registro (la [guía para desplegar un Workload](/es/guides/containers-deploy) explica cómo desplegar desde una imagen) y un token de API personal. Las llamadas de abajo usan el ayudante de la [introducción a la API de Units](/es/guides/units-api/overview): `$ZAROZ_API`, `$AUTH`, `$PROJECT`.
:::

## 1. Crea los dos Workers en un proyecto

Crea ambos Workers **en el mismo proyecto**, porque los nombres privados solo funcionan dentro de un proyecto. Dale a cada uno un slug que empiece por una letra, ya que el slug pasa a ser el nombre (mira [las reglas](/es/units/private-names#las-reglas)):

| Worker | slug | unidades | público |
|---|---|---|---|
| Proxy Velocity | `proxy` | 1 | TCP 25565 (y UDP 19132 para Bedrock) |
| Lobbies | `lobby` | 4 | ninguno |

Elige un plan cuya memoria quepa en la JVM: un proxy Velocity es ligero, un servidor Paper necesita su heap más margen. Zaroz define `MEMORY_LIMIT_MIB` en el entorno de cada unidad, y tu comando de arranque puede usarlo para dimensionar el heap.

Exporta los dos ids de servicio, que aparecen en la URL de la consola de cada Worker:

```bash
export PROXY="<uuid de la instancia de servicio del proxy>"
export LOBBY="<uuid de la instancia de servicio de lobby>"
```

## 2. Abre el único puerto público en el proxy

Abre un puerto TCP raw. `internal_port` es el puerto en el que escucha Velocity **dentro** del contenedor; Zaroz asigna la dirección y el número públicos y los devuelve.

**curl**

```bash
curl -sS -X POST "$ZAROZ_API/projects/$PROJECT/service-instances/$PROXY/endpoints" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"kind":"port","internal_port":25565,"protocol":"tcp"}'
# {"id": "...", "public_ip": "203.0.113.10", "public_port": 30421, "state": "active", ...}
```

**TypeScript**

```ts
const port = await call("POST", `/projects/${PROJECT}/service-instances/${PROXY}/endpoints`, {
  kind: "port", internal_port: 25565, protocol: "tcp",
});
console.log(`${port.body.public_ip}:${port.body.public_port}`, port.body.state);
```

**Python**

```python
status, port = call("POST", f"/projects/{PROJECT}/service-instances/{PROXY}/endpoints",
                    {"kind": "port", "internal_port": 25565, "protocol": "tcp"})
print(f"{port['public_ip']}:{port['public_port']}", port["state"])
```

Los jugadores entran por `public_ip:public_port`. Cada puerto raw cuesta 1 EUR al mes, facturado por las horas que se mantiene (mira [la factura](#la-factura)). La petición se rechaza con `not_ready` hasta que la unidad del proxy esté en marcha, así que despliega primero y abre el puerto después.

:::note{title="Estado de la API"}
Las rutas de endpoints las usa la consola y son solo de consola en la referencia de la API: aceptan tu token de sesión o un token de API personal, y rechazan a un service principal. Haz este paso tú, o desde la consola, no desde el proxy.
:::

Los **backends no tienen ningún endpoint**. Siguen siendo privados.

## 3. Escala el backend a N unidades

Una política fija ejecuta exactamente N unidades. Sus claves son `u1` a `uN`, y sus nombres privados `u1.lobby` a `uN.lobby`:

**curl**

```bash
curl -sS -X PATCH "$ZAROZ_API/projects/$PROJECT/service-instances/$LOBBY/units/policy" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"mode":"fixed","min_units":4,"max_units":4}'
```

**TypeScript**

```ts
await call("PATCH", `/projects/${PROJECT}/service-instances/${LOBBY}/units/policy`,
  { mode: "fixed", min_units: 4, max_units: 4 });
```

**Python**

```python
call("PATCH", f"/projects/{PROJECT}/service-instances/{LOBBY}/units/policy",
     {"mode": "fixed", "min_units": 4, "max_units": 4})
```

Si quieres nombres que elijas tú, como `survival` y `skyblock`, o una unidad por partida de minijuego, usa el [modo `code`](/es/guides/units-api/keys-and-lifecycle) y crea las unidades por clave: una unidad creada con la clave `skyblock` es accesible como `skyblock.lobby`. El resto de esta guía funciona igual en ambos casos, porque el proxy lee los nombres de la API.

## 4. Apunta Velocity a los backends

La configuración más simple del proxy nombra los backends de forma estática. En `velocity.toml`:

```toml
bind = "0.0.0.0:25565"
player-info-forwarding-mode = "modern"

[servers]
u1 = "u1.lobby:25565"
u2 = "u2.lobby:25565"
u3 = "u3.lobby:25565"
u4 = "u4.lobby:25565"
try = ["u1", "u2", "u3", "u4"]
```

Cada backend debe escuchar en el 25565 dentro de su contenedor y confiar en el secreto de reenvío de Velocity (`forwarding.secret` en el proxy, `[proxies.velocity]` en la configuración global de Paper). Como los backends no tienen dirección pública, `online-mode=false` en ellos es seguro: solo tu propio proyecto puede conectarse.

El nombre `lobby` a secas también es válido y llega a todas las unidades listas, pero un proxy de Minecraft necesita una dirección por servidor para poder enviar a un jugador a uno concreto, así que lista los nombres con clave.

## 5. Descubre los backends dinámicamente

Una lista estática se rompe en cuanto escalas a cinco. En su lugar, dale al Worker proxy un [service principal](/es/guides/units-api/principals) con `units.read` sobre el Worker lobby, y deja que un plugin o un script auxiliar en el proxy liste las unidades y registre un servidor de Velocity por cada unidad lista.

### Crea el principal y la concesión

**curl**

```bash
P="$ZAROZ_API/projects/$PROJECT/service-instances/$PROXY/principal"
curl -sS -X PUT "$P" -H "$AUTH"
curl -sS -X PUT "$P/grants" -H "$AUTH" -H "Content-Type: application/json" -d "{
  \"grants\": [{\"target_service_instance_id\": \"$LOBBY\", \"capability\": \"units.read\"}]
}"
```

**TypeScript**

```ts
const P = `/projects/${PROJECT}/service-instances/${PROXY}/principal`;
await call("PUT", P);
await call("PUT", `${P}/grants`, {
  grants: [{ target_service_instance_id: LOBBY, capability: "units.read" }],
});
```

**Python**

```python
P = f"/projects/{PROJECT}/service-instances/{PROXY}/principal"
call("PUT", P)
call("PUT", f"{P}/grants", {"grants": [
    {"target_service_instance_id": LOBBY, "capability": "units.read"},
]})
```

`units.read` basta para descubrir backends. Usa `units.control` solo si el proxy también debe arrancar y parar servidores (por ejemplo, despertar una unidad de minijuego parada cuando un jugador entra en cola). Activar un principal cambia la plantilla del Worker, así que se aplica cuando la unidad del proxy se reinicia.

Cada unidad del proxy tiene ahora `ZAROZ_API_URL` y `ZAROZ_PROJECT_ID` en su entorno y un token en `/zaroz/credentials/token`. El token rota: **lee el archivo en cada petición**.

### Lista las unidades

`GET .../units` devuelve de cada unidad su `key`, `private_name`, `state`, `labels` y `accepting`. Un backend es utilizable cuando tiene `private_name` y está en `running`:

**curl**

```bash
curl -sS "$ZAROZ_API_URL/projects/$ZAROZ_PROJECT_ID/service-instances/$LOBBY/units" \
  -H "Authorization: Bearer $(cat /zaroz/credentials/token)" \
  | jq -r '.data[] | select(.state == "running" and .private_name != null) | "\(.key) \(.private_name)"'
# u1 u1.lobby
# u2 u2.lobby
```

**TypeScript**

```ts
import { readFileSync } from "node:fs";

const token = () => readFileSync("/zaroz/credentials/token", "utf8").trim();
const base = `${process.env.ZAROZ_API_URL}/projects/${process.env.ZAROZ_PROJECT_ID}`;

type Unit = { key: string; private_name: string | null; state: string; labels: Record<string, string> };

export async function backends(lobbyId: string): Promise<{ name: string; address: string }[]> {
  const res = await fetch(`${base}/service-instances/${lobbyId}/units`, {
    headers: { Authorization: `Bearer ${token()}` },
  });
  if (!res.ok) throw new Error(`units: ${res.status}`);
  const { data } = (await res.json()) as { data: Unit[] };
  return data
    .filter((u) => u.state === "running" && u.private_name)
    .map((u) => ({ name: u.key, address: `${u.private_name}:25565` }));
}
```

**Python**

```python
import os
import requests

def token() -> str:
    with open("/zaroz/credentials/token") as f:
        return f.read().strip()

def backends(lobby_id: str) -> list[dict]:
    base = f"{os.environ['ZAROZ_API_URL']}/projects/{os.environ['ZAROZ_PROJECT_ID']}"
    r = requests.get(f"{base}/service-instances/{lobby_id}/units",
                     headers={"Authorization": "Bearer " + token()}, timeout=10)
    r.raise_for_status()
    return [
        {"name": u["key"], "address": f"{u['private_name']}:25565"}
        for u in r.json()["data"]
        if u["state"] == "running" and u["private_name"]
    ]
```

Filtra por etiqueta para separar tipos de servidor: dale a las unidades lobby `{"kind":"lobby"}` (mira las [etiquetas](/es/guides/units-api/labels-and-user-data)) y lista con `?label=kind:lobby`.

### Reacciona a los cambios

Consulta la lista cada pocos segundos, o sigue el [flujo de eventos](/es/guides/units-api/events) con `unit.ready`, `unit.draining`, `unit.stopped` y `unit.destroyed` (con `wait=30` la petición vuelve en cuanto ocurre algo), y vuelve a listar cuando llegue uno. El flujo encaja mejor que un [webhook](/es/guides/units-api/webhooks) aquí, porque los webhooks se entregan a una URL pública y el proxy solo expone su puerto de juego. En cada cambio, compara la lista con los servidores registrados y llama a `registerServer` / `unregisterServer` de Velocity para la diferencia. `unit.ready` incluye el nombre privado de la unidad, así que ni siquiera necesitas una segunda lectura.

:::note{title="Librería auxiliar"}
`zaroz-minecraft-helper`, un plugin de Velocity y Paper que hace los pasos 4 y 5 por ti, llegará próximamente. Hasta que se publique, las llamadas de arriba son todo lo que hace.
:::

## Jugadores de Bedrock con Geyser

Geyser traduce a los jugadores de Bedrock a Java, así que usan los mismos backends. Ejecútalo como plugin en el proxy (Geyser-Velocity) y abre un segundo puerto en el **Worker proxy**:

```bash
curl -sS -X POST "$ZAROZ_API/projects/$PROJECT/service-instances/$PROXY/endpoints" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"kind":"port","internal_port":19132,"protocol":"udp"}'
```

Es un puerto raw más, 1 EUR al mes. (`tcp_udp` es un endpoint que lleva ambos protocolos por un único número público, para cuando Java y Bedrock deban compartir número de puerto.)

El tráfico de Bedrock es UDP, y UDP no tiene conexión, así que el borde mantiene a cada jugador en el mismo backend por flujo: un flujo nuevo elige un backend a partir del último byte de la IPv4 del jugador, y un flujo ya iniciado se queda donde está hasta que termina, aunque cambie el conjunto de backends. Para un juego esto significa dos cosas: **mantén el proxy en una unidad**, que es lo que incluye el plan del Worker y lo que hace irrelevante la cuestión, y no esperes que un jugador UDP se mueva entre unidades a mitad de sesión. Entre el proxy y tus backends Java no hay UDP en absoluto, porque Geyser habla el protocolo Java por TCP con ellos.

## Cuando los jugadores deben llegar a un servidor concreto

A veces un servidor necesita su propia dirección pública: un servidor creativo que comparte un streamer, un servidor de torneo cuya dirección repartes. Un nombre privado no puede, porque nunca es público. Abre en su lugar un **endpoint por unidad** (`kind: unit`) en el Worker backend:

```bash
curl -sS -X POST "$ZAROZ_API/projects/$PROJECT/service-instances/$LOBBY/endpoints" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"kind":"unit","internal_port":25565,"protocol":"tcp"}'
```

Cada unidad del Worker recibe entonces su propia dirección y puerto públicos para ese puerto del contenedor, que llegan solo a esa unidad, y una unidad nueva recibe los suyos al añadirse. El campo `public_endpoints` de cada unidad (`public_ip`, `public_port`, `protocol`, `internal_port`) te dice la dirección, y se mantiene igual entre reinicios de la unidad. Se factura como un puerto **por unidad**, así que cuatro unidades son cuatro puertos; cerrar el endpoint quita el puerto de todas las unidades. Úsalo para los pocos servidores que lo necesiten, no para todos los backends: el proxy más los nombres privados es más barato y mantiene los backends fuera de internet.

## La factura

Cada cifra de abajo sale de las reglas de [Lo que cuestan las unidades](/es/units/billing) y de las [reglas de endpoints](/es/units/endpoints). **Los precios de plan son provisionales** (el precio del Worker aún se está fijando; es el mismo plan de ejemplo de 7,20 EUR al mes de la página de facturación, con una unidad incluida); lee el precio real de tu plan en su página o en el endpoint de cotización. La estructura de la factura no cambia cuando cambia el precio.

Las reglas:

1. El precio del plan de un Worker cubre su unidad **incluida** (una) y se paga por adelantado.
2. Cada unidad por encima de las incluidas es una **unidad extra**, facturada por las horas que realmente funciona al precio mensual del plan / 720, sin recargo, y con un mínimo de 5 minutos por arranque.
3. Cada puerto público raw cuesta **1 EUR al mes**, facturado por las horas que se mantiene (1 EUR / 720 por hora), sin puertos gratis en un Worker.
4. Los nombres privados, la API de Units, los eventos y los service principals son gratis. Los backends sin puerto público no pagan puerto.

La red de arriba, durante un mes entero de 30 días (720 horas):

| línea | cálculo | EUR |
|---|---|---|
| plan del proxy (1 unidad incluida) | precio de plan provisional | 7,20 |
| puerto TCP 25565 del proxy | 1 puerto x 1,00 EUR | 1,00 |
| plan de lobby (1 unidad incluida) | precio de plan provisional | 7,20 |
| 3 unidades lobby extra | 3 x 720 h x (7,20 / 720) | 21,60 |
| **total** | | **37,00** |

Añade Bedrock y el total es 38,00 EUR (un puerto más). Haz público un servidor creativo con un endpoint por unidad en un Worker lobby de cuatro unidades y son cuatro puertos más, 4,00 EUR. Si escalas el lobby entre 1 y 4 con una política que mantiene tres unidades extra durante 100 horas en el mes, las unidades extra cuestan `3 x 100 x 0,01 = 3,00 EUR` ese mes en lugar de 21,60 EUR. Una unidad parada no cuesta nada, así que una unidad de minijuego parada entre rondas solo cuesta las horas que funcionó.

Comprueba la cifra real antes de comprometerte: `POST .../units/policy/quote` devuelve lo máximo que podría costar una política, sin guardarla.

## Solución de problemas

| síntoma | causa |
|---|---|
| `private_name` es `null` | el slug del Worker empieza por un dígito, renómbralo |
| `u2.lobby` no se resuelve | esa unidad aún no está lista, o el proxy está en otro proyecto |
| Velocity dice "connection refused" | el backend no escucha en el puerto que escribiste, o solo escucha en `127.0.0.1`; enlaza `0.0.0.0` |
| abrir el puerto responde `not_ready` | la unidad del proxy aún no está en marcha |
| `ports_not_offered` | el producto elegido no ofrece puertos públicos; elige un Worker |
| la lista de unidades se rechaza | el principal no tiene la concesión `units.read` sobre ese Worker (mira [Service principals](/es/guides/units-api/principals)) |
| falta el archivo del token | el principal se acaba de activar; se aplica cuando la unidad del proxy se reinicia |
