add neuron-web phase-1 deploy on Legion

Stand up the elb-built native El landing server on Legion k3s alongside
the existing Cloud Run prod marketing site. Image is built on
gitea-runner-1 from elb-emitted .c files + native cc and pushed to
registry.neuralplatform.ai/neuron-web:dev-dfe41234. Deployment exposes
web-stage.neuralplatform.ai via Traefik + Cloudflare tunnel.

The point is to have a working local instance we can iterate against
while we redesign the CI/CD pipeline; nothing here replaces the Cloud
Run deploy yet.

* k8s/neuron-technologies/web/ — namespace, deployment, service, ingress,
  registry-pull-secret + externalsecret pulling anthropic/supabase keys
  from Vault
* apps/neuron-web.yaml — Argo CD Application
* dns-neuralplatform.tf, main.tf — CNAME + tunnel ingress for
  web-stage.neuralplatform.ai
This commit is contained in:
Will Anderson
2026-05-03 10:45:18 -05:00
parent b3609d6401
commit 3daf615dd2
10 changed files with 305 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
# neuron-web — El landing page server (Phase 1 deploy on Legion)
#
# Sibling to neuron-marketing (Next.js prod) and neuron-stage (Next.js stage).
# This deployment runs the elb-built native El binary at
# web-stage.neuralplatform.ai. Coexists with the Cloud Run prod marketing
# site; nothing here replaces that yet.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: neuron-web
namespace: argocd
spec:
project: default
source:
repoURL: http://gitea.git.svc.cluster.local:3000/will/infrastructure.git
targetRevision: main
path: servers/legion/k8s/neuron-technologies/web
destination:
server: https://kubernetes.default.svc
namespace: neuron-web
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
+12
View File
@@ -11,3 +11,15 @@ resource "cloudflare_record" "np_vpn" {
proxied = true
ttl = 1
}
# web-stage.neuralplatform.ai — Phase 1 Legion deploy of neuron-web (the El
# native landing server). Coexists with the Cloud Run prod marketing site
# at neurontechnologies.ai; this lets us iterate on the native build.
resource "cloudflare_record" "np_web_stage" {
zone_id = local.zone_neuralplatform_ai
name = "web-stage"
type = "CNAME"
content = "${var.cloudflare_tunnel_id}.cfargotunnel.com"
proxied = true
ttl = 1
}
@@ -0,0 +1,91 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: neuron-web
namespace: neuron-web
labels:
app: neuron-web
env: stage
spec:
replicas: 1
selector:
matchLabels:
app: neuron-web
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
metadata:
labels:
app: neuron-web
env: stage
spec:
imagePullSecrets:
- name: registry-pull-secret
containers:
- name: neuron-web
image: registry.neuralplatform.ai/neuron-web:dev-dfe41234
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
env:
- name: NODE_ENV
value: production
- name: PORT
value: "8080"
- name: NEURON_PORT
value: "7772"
- name: LANDING_ROOT
value: /srv/landing
# Public Cloud Run env that the El runtime reads at startup.
- name: NEURON_LLM_0_FORMAT
value: anthropic
- name: NEURON_LLM_0_MODEL
value: claude-sonnet-4-5
- name: NEURON_LLM_0_URL
value: https://api.anthropic.com/v1/messages
- name: GCS_SHARE_BUCKET
value: neuron-shares-prod
- name: NEURON_ORIGIN
value: https://web-stage.neuralplatform.ai
envFrom:
- secretRef:
name: neuron-web-secrets
# The El runtime fs_writes index.html, about.html, terms.html,
# enterprise-terms.html into LANDING_ROOT at startup. The image
# ships a baseline /srv/landing layout (assets, llms.txt) but
# the HTML files are runtime-generated. Mount a writable
# emptyDir over /srv/landing so the runtime can write, while
# keeping the image's static layout via subPath copies — but
# since the image already chowns /srv/landing to the landing
# user, no overlay is needed for normal operation. The
# readOnlyRootFilesystem flag is intentionally NOT set so
# /srv/landing stays writable.
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 10
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
@@ -0,0 +1,79 @@
# neuron-web-secrets — runtime secrets pulled from Vault by ESO.
#
# Phase 1 deployment of neuron-web on Legion. Only ANTHROPIC_API_KEY and
# SUPABASE_ANON_KEY are critical for the page to render and the chat
# widget to talk to Claude. Stripe + Resend + DocuSeal are pulled from
# the existing marketing-test path so checkout flows can be exercised in
# stage; webhook + DocuSeal tokens are present but the public webhook
# routes still go to the Cloud Run prod URL until we cut over.
#
# Vault paths used:
# secret/neuron-technologies/anthropic api_key
# secret/neuron-technologies/supabase anon_key, service_role_key, project_url
# secret/neuron-technologies/marketing-test stripe_*, etc.
# secret/neuron-technologies/notifications resend_api_key
#
# To add a new secret without changing the ExternalSecret manifest:
# vault kv patch secret/neuron-technologies/<path> key="value"
# then add a new data: entry below.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: neuron-web-secrets
namespace: neuron-web
spec:
refreshInterval: 1h
secretStoreRef:
name: vault
kind: ClusterSecretStore
target:
name: neuron-web-secrets
creationPolicy: Owner
data:
# Critical — chat widget + page render
- secretKey: ANTHROPIC_API_KEY
remoteRef:
key: secret/data/neuron-technologies/anthropic
property: api_key
- secretKey: NEURON_LLM_0_KEY
remoteRef:
key: secret/data/neuron-technologies/anthropic
property: api_key
- secretKey: SUPABASE_ANON_KEY
remoteRef:
key: secret/data/neuron-technologies/supabase
property: anon_key
- secretKey: SUPABASE_SERVICE_KEY
remoteRef:
key: secret/data/neuron-technologies/supabase
property: service_role_key
- secretKey: SUPABASE_URL
remoteRef:
key: secret/data/neuron-technologies/supabase
property: project_url
# Stripe — test/stage keys
- secretKey: STRIPE_PUBLISHABLE_KEY
remoteRef:
key: secret/data/neuron-technologies/marketing-test
property: next_public_stripe_publishable_key
- secretKey: STRIPE_SECRET_KEY
remoteRef:
key: secret/data/neuron-technologies/marketing-test
property: stripe_secret_key
- secretKey: STRIPE_WEBHOOK_SECRET
remoteRef:
key: secret/data/neuron-technologies/marketing-test
property: stripe_webhook_secret
- secretKey: STRIPE_PRICE_PROFESSIONAL
remoteRef:
key: secret/data/neuron-technologies/marketing-test
property: stripe_price_professional
- secretKey: STRIPE_PRICE_FOUNDING
remoteRef:
key: secret/data/neuron-technologies/marketing-test
property: stripe_price_founding
# Notifications
- secretKey: RESEND_API_KEY
remoteRef:
key: secret/data/neuron-technologies/notifications
property: resend_api_key
@@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: neuron-web
namespace: neuron-web
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: traefik
tls:
- hosts:
- web-stage.neuralplatform.ai
secretName: neuron-web-tls
rules:
- host: web-stage.neuralplatform.ai
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: neuron-web
port:
number: 8080
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- registry-pull-secret.yaml
- externalsecret.yaml
- service.yaml
- deployment.yaml
- ingress.yaml
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: neuron-web
labels:
managed-by: argo-cd
tier: web
@@ -0,0 +1,30 @@
# registry-pull-secret — credentials for registry.neuralplatform.ai
# pulled from Vault by ESO. Lets the deployment fetch
# registry.neuralplatform.ai/neuron-web:* without baking creds into the manifest.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: registry-pull-secret
namespace: neuron-web
spec:
refreshInterval: 1h
secretStoreRef:
name: vault
kind: ClusterSecretStore
target:
name: registry-pull-secret
creationPolicy: Owner
template:
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: |
{"auths":{"registry.neuralplatform.ai":{"username":"{{ .registry_user }}","password":"{{ .registry_docker_token }}","auth":"{{ list .registry_user .registry_docker_token | join ":" | b64enc }}"}}}
data:
- secretKey: registry_user
remoteRef:
key: secret/data/mudcraft
property: registry_user
- secretKey: registry_docker_token
remoteRef:
key: secret/data/mudcraft
property: registry_docker_token
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: neuron-web
namespace: neuron-web
labels:
app: neuron-web
spec:
selector:
app: neuron-web
ports:
- name: http
port: 8080
targetPort: 8080
type: ClusterIP
+11
View File
@@ -265,6 +265,17 @@ resource "cloudflare_zero_trust_tunnel_cloudflared_config" "legion" {
service = "http://headscale.headscale.svc.cluster.local:8080"
}
# web-stage.neuralplatform.ai — Phase 1 Legion deploy of neuron-web
# (native El landing server). Routes via Traefik to the neuron-web
# service in the neuron-web namespace.
ingress_rule {
hostname = "web-stage.neuralplatform.ai"
service = "https://traefik.kube-system.svc:443"
origin_request {
no_tls_verify = true
}
}
# Catch-all — must be last
ingress_rule {
service = "http_status:404"