Relay gestionado: Railway y Kubernetes con Helm
Relay gestionado: Railway y Kubernetes con Helm
En el capítulo 5 montamos el relay sobre un
VPS con el bundle de Docker Compose de deploy/compose/: un nodo único, un proceso de relay,
Postgres/Redis/MinIO como contenedores vecinos y Caddy delante.
Este capítulo cubre los dos caminos que el repositorio ofrece cuando no quieres administrar una
máquina: el botón de Railway y el chart de Helm deploy/charts/buzz.
flowchart TD
B{"¿Quiero administrar un servidor?"}
B -- "No, y quiero lo más rápido" --> C["Railway: botón de un clic"]
B -- "No, pero ya tengo clúster" --> D["Kubernetes con el chart buzz"]
B -- "Sí, tengo un VPS" --> E["Docker Compose en deploy/compose"]
D --> F{"¿Cuántas réplicas?"}
F -- "Una" --> G["Perfil quickstart, evaluación"]
F -- "Dos o más" --> H["Perfil production con Redis obligatorio"]
Parte A. Railway: el botón de un clic
Qué dice exactamente el repositorio
El README.md tiene una sección titulada “I want my own hosted relay” con este contenido, y nada más:
To run a relay for your team without managing servers, you can deploy one to Railway in a click.
Ahí aparece el badge Deploy on Railway apuntando a https://railway.com/deploy/buzz-relay-block,
y un enlace a un artículo del blog de ingeniería de Block
(https://engineering.block.xyz/blog/run-your-own-buzz-relay) “para más detalles”.
La plantilla existe y está enlazada desde el README oficial, pero su definición no vive en el
repositorio: no hay railway.json, ni railway.toml, ni nixpacks.toml en el árbol de fuentes. Lo
que la plantilla crea, cómo la parametriza y qué planes usa se decide del lado de Railway. Por lo
tanto, cualquier paso operativo detallado de Railway que encuentres por ahí no está respaldado por
este repositorio. No es un defecto: es la naturaleza de un despliegue “de plantilla”. El botón te
entrega un relay corriendo; el precio es que la configuración canónica queda fuera de tu control de
versiones.
Lo único técnico que el repo sí documenta sobre Railway
El dato de Railway más concreto no está en el README, sino en la documentación de almacenamiento de objetos, y aparece dos veces.
1. En deploy/compose/README.md. El bundle de Compose fija en compose.yml
BUZZ_S3_ENDPOINT=http://minio:9000 y BUZZ_S3_ADDRESSING_STYLE=path, y no los expone en .env. La
razón es que el DNS de Docker resuelve minio, no <bucket>.minio. La consecuencia práctica está
escrita textualmente en ese README: el bundle no sirve para proveedores S3 que exigen
direccionamiento virtual, “como los nuevos Railway Storage Buckets”. Para esos hay que usar el chart
de Helm o una configuración de Compose propia.
2. En deploy/charts/buzz/README.md. Ahí está la tabla de estilos de direccionamiento:
s3.addressingStyle | Forma de la petición | Para qué |
|---|---|---|
path (por defecto) | https://endpoint/bucket/key | MinIO empaquetado y endpoints cuyo DNS no resuelve subdominios de bucket |
virtual | https://bucket.endpoint/key | Proveedores estilo AWS y los nuevos Railway Storage Buckets |
Y el mapeo sugerido para un Railway Storage Bucket, tal cual en el chart:
s3:
endpoint: "${{Object Storage.ENDPOINT}}"
bucket: "${{Object Storage.BUCKET}}"
region: "${{Object Storage.REGION}}"
addressingStyle: virtual
Con BUZZ_S3_ACCESS_KEY=${{Object Storage.ACCESS_KEY_ID}} y
BUZZ_S3_SECRET_KEY=${{Object Storage.SECRET_ACCESS_KEY}} guardados en el Secret al que apunta
secrets.existingSecret. Tres advertencias que el propio chart deja escritas: no metas el bucket dentro de s3.endpoint,
porque ENDPOINT y BUCKET van por separado y el ajuste cambia el ruteo de la petición y la
firma SigV4; la pestaña Credentials de Railway es la autoridad para buckets más antiguos, que
pueden seguir exigiendo path; y solo se aceptan path y virtual, de modo que un valor inválido
falla el render del chart y el arranque del relay, sin degradación silenciosa.
Y todo lo del capítulo 4 sigue aplicando: BUZZ_RELAY_PRIVATE_KEY es la identidad del relay y debe
ser estable entre reinicios y respaldada; RELAY_OWNER_PUBKEY, sin prefijo BUZZ_, debe ser hex
de 64 caracteres cuando el modo cerrado está activo. Ver
configuración.
Parte B. Kubernetes: el chart deploy/charts/buzz
Identidad y perfiles
deploy/charts/buzz/Chart.yaml declara apiVersion: v2, name: buzz, type: application,
version: 0.1.7 y appVersion: "0.1.0", con licencia Apache-2.0. Se publica como artefacto OCI en
oci://ghcr.io/block/buzz/charts/buzz y se versiona independientemente de la imagen del relay y
de la app de escritorio, con sus propios tags chart-v*. El encabezado de values.yaml y la tabla
del README del chart definen dos perfiles:
| Perfil | Cuándo | Qué obtienes |
|---|---|---|
| Production (por defecto) | Auto-hospedado multi-tenant, entorno regulado o gestión por GitOps | Postgres/Redis/S3 externos gestionados, secrets.existingSecret en todos lados, cero autogeneración del chart, capaz de HA con replicaCount >= 2 |
| Quickstart (evaluación) | Evaluación, nodo único, demo puntual | Postgres + Redis + MinIO dentro del cluster, secretos autogenerados por el chart, réplica única |
Un detalle que se malinterpreta constantemente: quickstart: false es solo un marcador de
intención que se muestra en NOTES.txt; por sí mismo no enciende ningún servicio.
Dependencias
Chart.yaml declara dos subcharts, ambos condicionales:
| Subchart | Declarada | En Chart.lock | Repositorio | Condición | Alias |
|---|---|---|---|---|---|
postgres | 0.19.x | 0.19.5 | oci://registry-1.docker.io/cloudpirates | postgresql.enabled | postgresql |
redis | 0.30.x | 0.30.3 | oci://registry-1.docker.io/cloudpirates | redis.enabled | — |
MinIO no es un subchart. Es un Deployment propio del chart
(templates/quickstart-minio.yaml más templates/quickstart-minio-init.yaml), con imágenes fijadas
en values.yaml: minio/minio:RELEASE.2025-09-07T16-13-09Z y minio/mc:RELEASE.2025-08-13T08-35-41Z,
y persistencia de 10Gi.
Instalación quickstart
helm install buzz oci://ghcr.io/block/buzz/charts/buzz --version 0.1.7 \
--create-namespace --namespace buzz \
--set quickstart=true \
--set postgresql.enabled=true \
--set redis.enabled=true \
--set minio.enabled=true \
--set relayUrl=wss://buzz.example.com \
--set ownerPubkey=<64-char-hex-pubkey>
Esto levanta todo dentro del cluster — Postgres, Redis y MinIO con su bucket creado por un Job
post-install — y compone BUZZ_S3_ENDPOINT junto con credenciales autogeneradas, sin servicios
externos. El propio README lo marca como solo evaluación: cada servicio empaquetado es una réplica
única sin HA. ci/quickstart-values.yaml es el conjunto exacto que CI instala con ct install contra
un cluster kind.
Entradas obligatorias
| Clave | Qué es | Cuándo es obligatoria |
|---|---|---|
relayUrl | URL pública wss:// a la que se conectan los clientes | Siempre |
ownerPubkey | Pubkey Nostr del operador, 64 caracteres hex en minúscula | Cuando relay.requireRelayMembership=true, que es el valor por defecto |
secrets.existingSecret | Nombre de un Secret pre-creado | Producción y GitOps |
externalPostgresql.url / externalRedis.url / s3.endpoint | URLs de servicios externos | Producción, cuando el servicio empaquetado correspondiente está desactivado |
Las nueve validaciones que fallan en tiempo de render
templates/_validate.tpl define buzz.validate, y todos los manifiestos lo incluyen en su
primera línea, de modo que una mala configuración salta sin importar qué plantilla renderice Helm
primero. Las guardas son:
relayUrlvacío falla conrelayUrl is required: set --set relayUrl=wss://your.domain.- Réplica mínima mayor que 1 sin fuente de Redis:
redis.enabled,externalRedis.urlosecrets.existingSecret. autoscaling.minReplicas < 1,maxReplicas < minReplicas, owebsocketMetricEnabledsinwebsocketMetricName.relay.requireRelayMembership: truesinownerPubkey.ownerPubkeyque no cumpla^[0-9a-f]{64}$; el mensaje informa cuántos caracteres recibió.pairingRelay.enabledsinpairingRelay.url.ingress.enabledyhttproute.enableda la vez: hay que elegir uno.- Sin fuente de Postgres:
postgresql.enabled,externalPostgresql.urloexistingSecret. - Sin fuente de S3:
minio.enabled,s3.endpointoexistingSecret.
Hay además una nota histórica valiosa en ese archivo: se eliminó la validación que exigía
persistence.git.accessMode=ReadWriteMany en multi-réplica. Su justificación original — el estado
git en disco tiene que compartirse entre réplicas — dejó de ser cierta cuando ese estado pasó a vivir
en object storage.
Secretos
El camino de producción es crear un Secret fuera de banda y apuntarle con secrets.existingSecret.
examples/secret-sample.yaml documenta el esquema completo, y recomienda gestionarlo con
SealedSecrets, SOPS, External Secrets o Vault para que la forma sin cifrar nunca entre a git:
apiVersion: v1
kind: Secret
metadata: { name: buzz-secrets, namespace: buzz }
type: Opaque
stringData:
BUZZ_RELAY_PRIVATE_KEY: "REPLACE_WITH_64_HEX"
BUZZ_GIT_HOOK_HMAC_SECRET: "REPLACE_WITH_RANDOM_64_CHARS"
DATABASE_URL: "postgres://buzz:[email protected]:5432/buzz?sslmode=require"
REDIS_URL: "redis://:[email protected]:6379"
BUZZ_S3_ACCESS_KEY: "REPLACE"
BUZZ_S3_SECRET_KEY: "REPLACE"
También acepta READ_DATABASE_URL para una réplica de lectura opcional; omitirla mantiene todas las
lecturas en el writer. En el Deployment, BUZZ_RELAY_PRIVATE_KEY, BUZZ_GIT_HOOK_HMAC_SECRET,
READ_DATABASE_URL y las dos claves S3 se montan con optional: true; DATABASE_URL no es
opcional y sin ella el pod no arranca; y REDIS_URL es opcional solo cuando la réplica mínima es 1 y
no hay Redis empaquetado ni externo.
Por qué existingSecret es obligatorio bajo GitOps. ArgoCD y Flux renderizan con
helm template. En ese modo la función lookup de Helm devuelve vacío, así que cualquier
randAlphaNum del chart regeneraría el secreto en cada sync. El Secret gestionado por el chart
(templates/secret-chart.yaml, que lleva helm.sh/resource-policy: keep) es seguro solo para
helm install y helm upgrade, y NOTES.txt imprime esa advertencia al detectarlo. El Deployment
lleva además una anotación checksum/secret calculada con sha256sum sobre el Secret renderizado,
para rotar los pods cuando el secreto cambia.
Exposición: Ingress o Gateway API
Son mutuamente excluyentes por validación. Con ingress.enabled: true y ingress.hosts vacío, el
chart deriva el host desde relayUrl y crea una regla / con pathType: Prefix apuntando a
service.port. Con httproute.enabled: true y httproute.rules vacío, genera una HTTPRoute de
gateway.networking.k8s.io/v1 con un match PathPrefix: / hacia el mismo Service.
El ejemplo examples/ingress-cert-manager.yaml es un archivo con dos documentos YAML y una
advertencia explícita: el parser de valores de Helm lee solo el primero; el segundo — el
ClusterIssuer de cert-manager con un solver HTTP-01 de Let’s Encrypt — se aplica una vez por cluster
con kubectl apply -f, porque es cluster-scoped y sobrevive a cualquier release. El fragmento de
valores:
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
hosts:
- host: buzz.example.com
paths: [{ path: /, pathType: Prefix }]
tls:
- { hosts: [buzz.example.com], secretName: buzz-tls }
Esos dos timeouts de una hora no son decorativos: las conexiones WebSocket del relay son de larga
vida. NOTES.txt lo repite cuando no hay ni Ingress ni HTTPRoute habilitados — las conexiones
WebSocket de larga vida requieren timeouts generosos de lectura y envío, de al menos una hora.
Si habilitas el pairing relay opcional (pairingRelay.enabled con su url), el chart no crea
Ingress ni HTTPRoute para él: hay que rutear el hostname público hacia <release>-buzz-pairing:5000
con la configuración de ingress de tu plataforma.
Réplicas, HA y autoscaling
replicaCount vale 1 por defecto. Subirlo por encima de 1 exige Redis para el fan-out de
buzz-pubsub, y el chart falla el template si se rompe ese invariante: sin degradación silenciosa.
Lo que no exige es almacenamiento compartido; el comentario de values.yaml es la explicación
canónica: el estado de refs y objetos git está respaldado por object storage — cada petición hidrata
un repo efímero desde S3, la serialización de escritores es el CAS del puntero en el object store, y
la unicidad de nombres de repo vive en Postgres. Cada réplica puede usar su propio volumen
ReadWriteOnce, o ninguno.
El HPA es opcional y escala por la mayor de dos recomendaciones, CPU o WebSockets activos por pod:
autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 15
targetCPUUtilizationPercentage: 65
websocketMetricEnabled: true
websocketMetricName: buzz_ws_connections_active
targetWebsocketConnections: 5000
El escalado por CPU requiere Metrics Server. El de WebSockets requiere además un adaptador de
métricas personalizado — por ejemplo Prometheus Adapter — que exponga el gauge
buzz_ws_connections_active como métrica de pod. El chart crea el HPA pero deliberadamente no
instala ni configura ese adaptador; websocketMetricEnabled: false deja un HPA solo de CPU. El
behavior por defecto es asimétrico a propósito: scaleUp con stabilizationWindowSeconds: 0,
políticas de 100 % cada 60 s y 4 pods cada 60 s, selectPolicy: Max; scaleDown con
stabilizationWindowSeconds: 600 y 1 pod cada 120 s, selectPolicy: Min. Bajar despacio da tiempo a
drenar las conexiones WebSocket de larga vida.
Complementos del modelo HA: el Deployment usa strategy: RollingUpdate con maxSurge: 1 y
maxUnavailable: 0; el PodDisruptionBudget se renderiza solo cuando la réplica mínima es mayor
que 1, por defecto con minAvailable: 1; y relay.drainJitterMs, junto a
terminationGracePeriodSeconds: 60, reparte los cierres de socket para no provocar una estampida de
reconexiones contra el pool de la base. Valores por encima de 20000 se topan a 20000.
Almacenamiento git
persistence.git tiene por defecto enabled: true, mountPath: /var/lib/buzz/git,
accessMode: ReadWriteOnce y size: 10Gi, más storageClass y existingClaim vacíos.
enabled: truemonta un PVC enmountPath: durable entre reinicios del pod, pero un único PVCReadWriteOncese ata a un nodo, así que no soporta planificar varios pods entre nodos.enabled: falsemonta unemptyDirpor pod, acotado porsize. Es la elección correcta para HA multi-réplica: cada pod tiene su espacio local, no se comparte nada y no hay volumen que multi-adjuntar. Es seguro porque las fuentes de verdad son el object store y Postgres.
Aparte del volumen git, el Deployment monta siempre un emptyDir git-pack-cache en
git.packCachePath (/var/cache/buzz/git-packs), dimensionado por git.packCacheVolumeSize (7Gi).
Migraciones y upgrades
Las migraciones de esquema están embebidas en el binario del relay vía sqlx::migrate! y se ejecutan
al arrancar, controladas por migrate.autoMigrate, que vale true por defecto. Varias réplicas
compiten de forma race-safe detrás de un advisory lock de Postgres, y helm upgrade es todo el
procedimiento de upgrade. Si prefieres desacoplarlas del servicio, migrate.autoMigrate=false; en
ese modo el chart no corre migraciones por ti: eres tú quien debe ejecutar buzz-admin migrate
— un Pod aparte o un Job one-shot — antes de cada helm install o helm upgrade. Las readiness probes
solo verifican conectividad a la base, no frescura del esquema, así que un pod puede parecer sano
contra un esquema sin migrar y fallar bajo carga; NOTES.txt imprime esa advertencia. El valor
migrate.preUpgradeJob.enabled existe pero está reservado: hoy no hace nada.
Observabilidad y salud
Los puertos del contenedor están nombrados: app (3000), health (service.healthPort, 8080) y
metrics (service.metricsPort, 9102). Las tres sondas apuntan al puerto health:
livenessProbe a /_liveness con initialDelaySeconds: 5, periodSeconds: 10, timeoutSeconds: 3
y failureThreshold: 3; readinessProbe a /_readiness con 5 / 5 / 3 / 3; y startupProbe
a /_liveness con failureThreshold: 60 y periodSeconds: 2, es decir unos 120 s de margen.
serviceMonitor.enabled: false por defecto; al activarlo el chart crea un ServiceMonitor de
Prometheus Operator que selecciona el Service por las etiquetas del chart y scrapea el puerto llamado
metrics con interval: 30s y scrapeTimeout: 10s. Todo esto se profundiza en
observabilidad. NOTES.txt da el atajo manual:
kubectl -n buzz port-forward svc/buzz 8080:8080
curl http://localhost:8080/_readiness
Seguridad del pod y puntos de extensión
relay.securityContext corre como no-root con uid, gid y fsGroup 65532 y
seccompProfile: RuntimeDefault; relay.containerSecurityContext desactiva
allowPrivilegeEscalation, hace capabilities.drop: [ALL] y deja readOnlyRootFilesystem: false
con el motivo escrito al lado: las escrituras de git necesitan una ruta escribible.
El chart expone puntos de extensión estrechos: extraInitContainers, extraVolumes,
relay.extraVolumeMounts, relay.command, relay.args, relay.extraEnv y relay.extraEnvFrom.
Todos se renderizan con toYaml, no con tpl, y el chart no valida relaciones entre campos:
los nombres no deben colisionar con lo que ya es del chart, los mounts deben referenciar volúmenes
existentes, y cada init container debe definir su securityContext y sus resources.
extraManifests crea recursos independientes pero no puede modificar el Deployment del relay.
GitOps: ArgoCD y Flux
examples/argocd-app.yaml incluye una lección aprendida a golpes, escrita como comentario: en ArgoCD
3.1 y superiores con fuentes OCI nativas, repoURL debe ser la ruta completa del artefacto. Con
la forma partida repoURL: .../charts más chart: buzz, el campo chart se ignora para URLs
oci:// y la descarga falla con un 403; el validador del spec sigue exigiendo path, así que se
usa ".".
source:
repoURL: oci://ghcr.io/block/buzz/charts/buzz
path: .
targetRevision: 0.1.7
helm:
releaseName: buzz
values: |
relayUrl: wss://buzz.example.com
replicaCount: 3
secrets: { existingSecret: buzz-secrets }
examples/flux-helmrelease.yaml hace lo equivalente con un HelmRepository de tipo oci hacia
oci://ghcr.io/block/buzz/charts y un HelmRelease de helm.toolkit.fluxcd.io/v2. Ambos ejemplos
usan persistence.git.accessMode: ReadWriteOnce con replicaCount: 3, y el comentario lo justifica:
RWO es válido a cualquier número de réplicas porque el git está respaldado por object storage.
Cómo se publica el chart
.github/workflows/helm-chart.yml lintea, corre tests unitarios de helm-unittest y hace
render-check en cada PR y cada push a main, pero solo un tag chart-v* publica: así un main
en progreso nunca puede sobrescribir una versión ya publicada. El repositorio OCI sale de la variable
CHART_REPO, con valor por defecto oci://ghcr.io/block/buzz/charts. Para cortar un release se
empuja una rama chart-release/<version> cuya <version> coincida con la de Chart.yaml; al mergear
se auto-taggea chart-v<version> y el job de publicación falla ruidosamente si ambas discrepan.
Limitaciones honestas que el chart declara
El README del chart tiene una sección titulada “Honest limitations (v1)”:
- MinIO empaquetado es solo evaluación. Réplica única, sin HA, credenciales autogeneradas con
lookup, no GitOps-safe y no pensado para tráfico de producción. - El “modo mínimo” todavía no está soportado.
BUZZ_PUBSUB=localy las rutas de media sobre sistema de archivos son trabajo en curso upstream; incluso el quickstart levanta Redis y S3 reales. La búsqueda full-text sí corre ya en Postgres, así que no se aprovisiona servicio de búsqueda aparte. - La firma Cosign del chart publicado está pendiente. La imagen del relay sí lleva atestación vía
actions/attest-build-provenance; el chart todavía no.
Parte C. El testbed local: deploy/local/
El repositorio trae un testbed reproducible para el Kubernetes de Docker Desktop.
deploy/local/quickstart-ha-values.yaml es el quickstart de CI con la forma HA encima:
quickstart: true
postgresql: { enabled: true }
redis: { enabled: true }
minio: { enabled: true }
relayUrl: wss://buzz.test.local
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000001"
relay: { requireRelayMembership: false }
podDisruptionBudget: { enabled: false }
replicaCount: 3
persistence:
git:
enabled: false
image:
repository: buzz-relay
tag: mesh-local
pullPolicy: IfNotPresent
Dos líneas cargan con todo el peso del HA, y el archivo las comenta: replicaCount: 3 obliga al chart
a exigir Redis (que el quickstart provee, y el HMAC de hooks git se autogenera del lado del chart), y
persistence.git.enabled: false cambia el PVC ReadWriteOnce por un emptyDir por pod. Con el valor
por defecto true, un único PVC RWO no puede multi-adjuntarse a 3 pods y dejaría 2 de 3 atascados.
deploy/local/build-and-deploy.sh orquesta el ciclo completo:
flowchart TD
A["Verificar current-context igual a docker-desktop"] --> B["docker build buzz-relay:mesh-local"]
A -- "Otro contexto" --> Z["Aborta sin tocar el clúster"]
B --> C["helm dependency build"]
C --> D["helm upgrade --install --timeout 5m sin --wait"]
D --> E["kubectl rollout status --timeout=4m"]
E --> F["curl a /_readiness en cada pod"]
F --> G["Exige el literal status ready"]
Los detalles que importan:
- Guardarraíl de contexto. El script compara
kubectl config current-contextcondocker-desktopy se niega a tocar cualquier otro cluster. - Namespace
buzz-mesh, releasebuzz, imagenbuzz-relay:mesh-local, 3 réplicas. Docker Desktop comparte el almacén de imágenes con Kubernetes, así que un tag construido localmente máspullPolicy: IfNotPresentno necesita push a un registro nikind load. - Sin
--waitenhelm upgrade --install. El comentario del script explica por qué: la sonda de conformidad S3 A3 del relay es fatal al arranque, así que los relays entran en CrashLoopBackOff unas cuantas veces hasta que el Job de init concurrente crea el bucket.helm --waitcompite con ese transitorio y puede abortar antes de tiempo; el script se hace cargo conrollout statusmás sondas por pod, que sí toleran los reinicios. - Verificación por pod, no agregada. Además de exigir
readyReplicas == 3, haceexecen cada pod de relay y comprueba quehttp://127.0.0.1:8080/_readinessresponda con el literal"status":"ready". La evidencia queda en/tmp/mesh-build/deploy-evidence-<timestamp>. - Variables útiles:
SKIP_BUILD=1,IMAGE_TAG=...,NSyRELEASE.
Ese mismo transitorio del bucket es la razón del initContainer wait-for-bucket que el chart inyecta
cuando minio.enabled está activo: con la imagen mc, espera con mc alias set hasta que MinIO
responda y con mc stat hasta que el bucket exista, y solo entonces deja arrancar al relay.
Parte D. El chart secundario: buzz-push-gateway
deploy/charts/buzz-push-gateway empaqueta un componente separado: el gateway APNs público de
último salto, descrito en su Chart.yaml como “Public capability-gated APNs last-hop gateway for
Buzz”, versión 0.1.0. Su documentación propia es docs/push-gateway-deployment.md, cuya primera
línea es la regla de oro: se construye con Dockerfile.push-gateway, y no se corre dentro de la
imagen del relay ni se dan credenciales APNs a los relays.
Defaults relevantes de su values.yaml:
| Clave | Default |
|---|---|
replicaCount | 2 |
image.repository / tag | ghcr.io/block/buzz-push-gateway / main |
existingSecret / migration.existingSecret | buzz-push-gateway / buzz-push-gateway-migrations |
migration.runtimeDatabaseRole | buzz_push_gateway_runtime |
publicDeliveryUrl | https://push.buzz.xyz/v1/deliveries/apns |
maxGrantLifetimeSeconds | 2592000, es decir 30 días |
appAttestAppId | TEAMID.xyz.buzz — es un ejemplo, producción DEBE sobrescribirlo |
service.port / httpRoute.enabled / networkPolicy.enabled | 8080 / false / true |
Cuatro decisiones de diseño antes de desplegarlo:
- Separación de privilegios en la base de datos. El
DATABASE_URLde runtime debe tener privilegios solo DML. Las credenciales con DDL las usa únicamente el Job de migración pre-install/pre-upgrade, víamigration.existingSecret. Ese Job, tras migrar, revocaCREATEde base y de esquema al rol de runtime y le concede soloCONNECT,USAGEySELECT, INSERT, UPDATE, DELETEsobre las seis tablas del gateway. - Base de datos dedicada, no la del relay. SQLx guarda su historial
_sqlx_migrationsenpublic, así que compartir base colisionaría con el historial de migraciones de otra aplicación. - Métricas en un puerto privado. El listener público es
8080;/metricsy las sondas viven en8081.httpRoute.enabled: falseevita que una instalación genérica reclame una ruta sin adjuntar;podMonitoryprometheusRuleestán apagados, y el primero exige ademásnetworkPolicy.monitoringhabilitado. - NetworkPolicy con placeholders deliberados.
apnsEgressCidrsviene como[0.0.0.0/0]ypostgresEgressCidrscomo[10.0.0.0/8], ambos con comentarios diciendo que hay que estrecharlos. Kubernetes NetworkPolicy no admite nombres DNS: hay que traducirlos a los CIDR reales de tu red.
Parte E. Cuándo conviene cada modelo
| Criterio | Compose en VPS | Railway | Helm en Kubernetes |
|---|---|---|---|
| Requiere administrar máquina | Sí | No | El cluster, sí |
| Multi-réplica | No | No documentado en el repo | Sí, con Redis obligatorio |
| Autoscaling | No | No documentado en el repo | HPA opcional, requiere Metrics Server |
| Configuración versionable | .env propio | Vive en la plantilla de Railway | values.yaml y GitOps |
| S3 externo | No: fija endpoint y path | Storage Buckets con virtual | s3.* completo, path o virtual |
| TLS | Caddy con compose.caddy.yml | Lo aporta la plataforma | Ingress más cert-manager, o Gateway API |
| Validación de configuración | run.sh aborta con CHANGE_ME pendientes | — | 9 guardas que fallan el render |
| Secretos | Archivo .env | Variables de la plataforma | existingSecret con SealedSecrets, SOPS, External Secrets o Vault |
Criterios de decisión:
- Railway si quieres un relay para tu equipo hoy y no te importa que la definición del despliegue
viva fuera de tu control de versiones. Con Storage Buckets nuevos, recuerda
virtual. - Compose en un VPS si tienes una máquina, una comunidad y quieres el mínimo de piezas móviles: es el camino del capítulo 5.
- El chart de Helm cuando necesites más de una réplica (con la condición dura de Redis), object
storage externo gestionado, o gestión declarativa por ArgoCD o Flux. Y nunca el perfil quickstart
en producción: el MinIO empaquetado es réplica única sin HA, con credenciales autogeneradas por
lookup, y no es GitOps-safe.
Sea cual sea el camino, la lista de respaldo es la misma y NOTES.txt la imprime en cada
helm install: BUZZ_RELAY_PRIVATE_KEY (rotarla es cambiar de identidad y los pares de federación
dejarán de reconocer al relay), la base PostgreSQL, el bucket S3 con los blobs de media, el PVC de git
y la clave privada del owner, que la tiene el operador y no el chart. Más en
backups y actualizaciones.
Resumen
- El botón Deploy on Railway existe y está enlazado desde el
README.mdhaciahttps://railway.com/deploy/buzz-relay-block, pero la definición de la plantilla no vive en el repositorio: no hayrailway.json,railway.tomlninixpacks.toml. - Los Storage Buckets nuevos de Railway exigen
s3.addressingStyle: virtual, y el bundle de Compose no sirve porque fijapath. - El chart
deploy/charts/buzzestá en la versión0.1.7, se publica enoci://ghcr.io/block/buzz/charts/buzzy tiene dos perfiles: production por defecto y quickstart solo para evaluación, este último activado servicio por servicio conpostgresql.enabled,redis.enabledyminio.enabled— Postgres y Redis son subcharts de CloudPirates (0.19.5y0.30.3enChart.lock), MinIO es un Deployment del propio chart. templates/_validate.tpldefine nueve guardas que fallan en tiempo de render:relayUrl, Redis en multi-réplica, coherencia del autoscaling,ownerPubkeyy su formato hex,pairingRelay.url, exclusión mutua de Ingress y HTTPRoute, y las fuentes de Postgres y de S3.replicaCount > 1exige Redis y el chart falla el template si no lo hay, pero no exige almacenamientoReadWriteMany: el estado git vive en object storage, así queemptyDirpor pod es la elección correcta en HA.- En producción y bajo GitOps,
secrets.existingSecretes obligatorio: ArgoCD y Flux renderizan conhelm template, dondelookupdevuelve vacío y los secretos autogenerados rotarían en cada sync. migrate.autoMigrate: truecorre las migraciones sqlx al arrancar tras un advisory lock de Postgres, yhelm upgradees todo el procedimiento de upgrade. Confalse, el chart no migra por ti.deploy/local/build-and-deploy.shlevanta el testbed HA de 3 réplicas sobre Docker Desktop, correhelm upgrade --installsin--waitpor la sonda S3 fatal al arranque, y verifica/_readinesspod por pod. El chartbuzz-push-gateway(0.1.0) es un componente aparte, con base de datos dedicada, privilegios DML en runtime y métricas en el puerto privado8081.- Limitaciones declaradas por el propio chart: MinIO empaquetado es solo evaluación, el modo mínimo no está soportado todavía, y la firma Cosign del chart publicado está pendiente.
Siguiente: Identidad y autenticación: claves, NIP-42 y NIP-98