diff --git a/docs/manuals/spaces/howtos/self-hosted/ingress-nginx-migration.md b/docs/manuals/spaces/howtos/self-hosted/ingress-nginx-migration.md
new file mode 100644
index 000000000..34ecfaa4f
--- /dev/null
+++ b/docs/manuals/spaces/howtos/self-hosted/ingress-nginx-migration.md
@@ -0,0 +1,667 @@
+---
+title: Migrate away from ingress-nginx
+sidebar_position: 7
+description: A guide on how to migrate from ingress-nginx
+tier: "business"
+---
+
+import GlobalLanguageSelector, { CodeBlock } from '@site/src/components/GlobalLanguageSelector';
+
+
+
+`ingress-nginx` is deprecated and will reach end-of-life in March 2026. This
+guide covers migration options for existing Spaces deployments.
+
+For help choosing an exposure method, see [Exposing Spaces Externally][expose].
+
+
+
+## Prerequisites
+
+
+Set environment variables used throughout this guide:
+
+```bash
+export SPACES_VERSION= # Example: 1.16.0
+export SPACES_ROUTER_HOST= # Example: proxy.example.com
+```
+
+Export your current Helm values to a file (or use an existing version-controlled
+file):
+
+```bash
+helm get values spaces -n upbound-system -o yaml > values.yaml
+```
+
+You'll merge new configuration into this file throughout the migration.
+
+
+
+## Migrate current Spaces version before March 2026
+
+
+Choose your migration option:
+
+| Option | When to use |
+|--------|-------------|
+| [Gateway API](#gateway-api-spaces-110) | Already using Gateway API or need shared gateway |
+| [Traefik](#traefik-or-alternative-ingress-controller) | Migrate from nginx Ingress to alternative controller |
+
+Export your current Helm values to a file (or use your existing values file if
+stored in Git):
+
+```bash
+helm get values spaces -n upbound-system -o yaml > values.yaml
+```
+
+
+### Gateway API (Spaces 1.10+)
+
+
+Gateway API support has been available since Spaces 1.10. See [Gateway API
+Configuration][gateway-api-config] for detailed setup instructions.
+
+:::note
+Pre-1.16 Spaces doesn't support running Ingress and Gateway API
+simultaneously. This migration requires switching over in a single upgrade,
+which causes brief downtime during DNS propagation.
+:::
+
+**1. Remove existing ingress resources**
+
+Delete the Ingress resource and ingress-nginx controller:
+
+```bash
+kubectl -n upbound-system delete ingress mxe-router-ingress
+helm -n ingress-nginx delete ingress-nginx
+```
+
+:::warning
+This step forces downtime for API access through spaces-router until the
+Gateway API configuration is complete.
+:::
+
+**2. Install a gateway API controller**
+
+Install a Gateway API implementation that supports TLS passthrough and
+`TLSRoute`.
+
+The following example uses Envoy Gateway:
+
+```bash
+export ENVOY_GATEWAY_VERSION= # Example: v1.2.4
+
+helm -n envoy-gateway-system upgrade --install --wait --wait-for-jobs \
+ --timeout 300s --create-namespace envoy-gateway \
+ oci://docker.io/envoyproxy/gateway-helm \
+ --version "${ENVOY_GATEWAY_VERSION}"
+```
+
+**3. Create GatewayClass resource**
+
+Create a `GatewayClass` resource.
+
+
+```bash
+kubectl apply -f - --server-side <
+Configure Traefik's Service with NLB annotations. See
+[Cloud-specific annotations][expose-annotate].
+
+
+**2. Validate before switching DNS**
+
+```bash
+# Get Traefik load balancer address
+TRAEFIK_LB=$(kubectl get svc -n traefik traefik -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
+
+# Test connectivity using --connect-to to route to Traefik
+curl --connect-to "${SPACES_ROUTER_HOST}:443:${TRAEFIK_LB}:443" "https://${SPACES_ROUTER_HOST}/version"
+# Expected: 401 Unauthorized (routing works, auth required)
+```
+
+**3. Update DNS to point to Traefik**
+
+```bash
+kubectl get svc -n traefik traefik -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
+```
+
+Update your DNS record to this address. For gradual migration, use weighted DNS routing.
+
+**4. Preserve the nginx IngressClass before uninstalling ingress-nginx**
+
+```bash
+helm upgrade ingress-nginx ingress-nginx \
+ --repo https://kubernetes.github.io/ingress-nginx \
+ --namespace ingress-nginx \
+ --reuse-values \
+ --set-json 'controller.ingressClassResource.annotations={"helm.sh/resource-policy": "keep"}'
+```
+
+**5. Uninstall ingress-nginx**
+
+```bash
+helm uninstall ingress-nginx --namespace ingress-nginx
+```
+
+Keep `ingress.provision: true` so the Spaces chart continues to manage the
+Ingress resource. Traefik picks it up via the nginx provider.
+
+
+## Verification
+
+After migration, verify connectivity:
+
+```bash
+curl -v "https://${SPACES_ROUTER_HOST}/version"
+# Expected: 401 Unauthorized
+```
+
+[envoy-install]: https://gateway.envoyproxy.io/docs/install/
+[spaces-install]: /manuals/spaces/howtos/self-hosted/self-hosted-spaces-deployment/
+[traefik-migrate]: https://doc.traefik.io/traefik/migrate/nginx-to-traefik/
+[spaces-deploy]: /manuals/spaces/howtos/self-hosted/self-hosted-spaces-deployment/
+[k8s-announce]: https://www.kubernetes.dev/blog/2025/11/12/ingress-nginx-retirement/
+[expose]: /manuals/spaces/howtos/self-hosted/ingress/
+[expose-annotate]: /manuals/spaces/howtos/self-hosted/ingress/#cloud-specific-annotations
+[gateway-api]: https://gateway-api.sigs.k8s.io/
+[gateway-api-config]: /manuals/spaces/howtos/self-hosted/ingress/#gateway-api
diff --git a/docs/manuals/spaces/howtos/self-hosted/ingress.md b/docs/manuals/spaces/howtos/self-hosted/ingress.md
new file mode 100644
index 000000000..44b47a773
--- /dev/null
+++ b/docs/manuals/spaces/howtos/self-hosted/ingress.md
@@ -0,0 +1,137 @@
+---
+title: Exposing Spaces externally
+sidebar_position: 5
+description: Options for exposing Spaces externally
+---
+
+import { CodeBlock } from '@site/src/components/GlobalLanguageSelector';
+
+
+You can expose Spaces externally using of three options:
+
+| Option | When to use |
+|--------|-------------|
+| LoadBalancer Service | Simplest setup, recommended for most deployments |
+| Gateway API | Organization already using Gateway API, or need shared gateway across services |
+| Ingress | Organization already using Ingress, or need shared load balancer across services |
+
+## LoadBalancer Service
+
+
+Upbound recommends a LoadBalancer Service to expose `spaces-router`.
+
+
+:::important
+Use a Network Load Balancer (L4), not an Application Load Balancer (L7). Spaces
+uses long-lived connections for watch traffic that L7 load balancers may
+timeout.
+:::
+
+### Configuration
+
+```yaml
+externalTLS:
+ host: proxy.example.com # Externally routable hostname for TLS certificates
+
+router:
+ proxy:
+ service:
+ type: LoadBalancer
+ annotations:
+ # AWS NLB (see Cloud-Specific Annotations for other clouds)
+ service.beta.kubernetes.io/aws-load-balancer-type: external
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
+```
+
+See [Cloud-Specific Annotations](#cloud-specific-annotations) for GCP and Azure.
+
+
+### Get the LoadBalancer address
+
+
+After installation:
+
+```bash
+kubectl get svc -n upbound-system spaces-router \
+ -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
+```
+
+Create or update a DNS record pointing your `externalTLS.host` to this address.
+
+## Ingress
+
+Use Ingress if you need to share a load balancer across multiple services or
+have specific networking requirements.
+
+### Requirements
+
+- TLS passthrough support in your Ingress controller
+- Network Load Balancer (L4) strongly recommended for long-lived connections
+
+Configure your Ingress controller's Service with [NLB annotations](#cloud-specific-annotations).
+
+
+### Configuration
+
+```yaml
+ingress:
+ provision: true
+ host: proxy.example.com
+ ingressClassName: ""
+ # Annotations to add to the Ingress resource
+ annotations: {}
+ # Pod labels of the Ingress controller - used for network policy
+ podLabels: {}
+ # Namespace labels of the Ingress controller - used for network policy
+ namespaceLabels: {}
+```
+
+### Traefik (with nginx provider)
+
+Traefik can use the [kubernetesIngressNGINX provider][traefik-provider] to
+handle nginx-style Ingress resources with TLS passthrough.
+
+```yaml
+ingress:
+ provision: true
+ host: proxy.example.com
+ ingressClassName: nginx
+ annotations:
+ nginx.ingress.kubernetes.io/ssl-passthrough: "true"
+ nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
+ podLabels:
+ app.kubernetes.io/name: traefik
+ namespaceLabels:
+ kubernetes.io/metadata.name: traefik
+```
+
+## Gateway API
+
+Spaces supports the [Gateway API][gateway-api-docs]. Use this option if your
+organization is already using Gateway API or needs a shared gateway across
+multiple services.
+
+### Requirements
+
+- A Gateway API controller (for example, Envoy Gateway, Cilium, or Traefik)
+- Gateway API CRDs installed in your cluster
+- TLS passthrough support
+- Network Load Balancer (L4) strongly recommended
+
+## Cloud-specific annotations
+
+Network Load Balancers (L4) are strongly recommended. Spaces uses long-lived
+watch connections (hours or days) for kubectl and ArgoCD. L7 load balancers may
+timeout these connections. Use these annotations on the LoadBalancer Service
+(spaces-router, Ingress controller, or Gateway).
+
+| Cloud | Annotations |
+|-------|-------------|
+| **AWS** | `service.beta.kubernetes.io/aws-load-balancer-type: external`
`service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing`
`service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip` |
+| **GCP** | `cloud.google.com/l4-rbs: enabled` |
+| **Azure** | None required (L4 by default) |
+
+
+[traefik-provider]: https://doc.traefik.io/traefik/reference/install-configuration/providers/kubernetes/kubernetes-ingress-nginx/
+[gateway-api-docs]: https://gateway-api.sigs.k8s.io/
diff --git a/docs/manuals/spaces/howtos/self-hosted/self-hosted-spaces-deployment.md b/docs/manuals/spaces/howtos/self-hosted/self-hosted-spaces-deployment.md
index e549e3939..e24b95ab8 100644
--- a/docs/manuals/spaces/howtos/self-hosted/self-hosted-spaces-deployment.md
+++ b/docs/manuals/spaces/howtos/self-hosted/self-hosted-spaces-deployment.md
@@ -1,7 +1,7 @@
---
title: Deployment Workflow
sidebar_position: 3
-description: A quickstart guide for Upbound Spaces
+description: A quickstart guide for Upbound Spaces
tier: "business"
---
import GlobalLanguageSelector, { CodeBlock } from '@site/src/components/GlobalLanguageSelector';
@@ -249,59 +249,135 @@ helm install aws-load-balancer-controller aws-load-balancer-controller --namespa
-### Install ingress-nginx
+### Install Envoy Gateway
-Starting with Spaces v1.10.0, you need to configure the ingress-nginx
-controller to allow SSL-passthrough mode. You can do so by passing the
-`--enable-ssl-passthrough=true` command-line option to the controller.
-The following Helm install command enables this with the `controller.extraArgs`
-parameter:
+Starting with Spaces v1.10.0, Upbound recommends using the [Gateway API] for
+routing traffic to Spaces. Gateway API is the official Kubernetes standard for
+ingress and replaces the legacy Ingress API.
+
+This guide uses Envoy Gateway as the Gateway API controller and replaces
+ingress-nginx previously recommended.
+
+:::info
+If you need to continue to use ingress-nginx temporarily, use the [ingress-nginx
+migration guide][migration guide].
+
+The Kubernetes community announced that ingress-nginx will be retired in March
+2026 and you should plan to migrate to Gateway API before then.
+:::
+
+
+First, install Envoy Gateway with Helm:
+
+```bash
+helm -n envoy-gateway-system upgrade --install --wait --wait-for-jobs \
+ --timeout 360s --create-namespace envoy-gateway \
+ oci://docker.io/envoyproxy/gateway-helm \
+ --version "v1.2.4"
+```
+
+Next, create the Gateway API resources for your cloud provider:
+Create EnvoyProxy configuration for AWS load balancer
+
```bash
-helm upgrade --install ingress-nginx ingress-nginx \
- --create-namespace --namespace ingress-nginx \
- --repo https://kubernetes.github.io/ingress-nginx \
- --version 4.12.1 \
- --set 'controller.service.type=LoadBalancer' \
- --set 'controller.extraArgs.enable-ssl-passthrough=true' \
- --set 'controller.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-type=external' \
- --set 'controller.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-scheme=internet-facing' \
- --set 'controller.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-nlb-target-type=ip' \
- --set 'controller.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-healthcheck-protocol=http' \
- --set 'controller.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-healthcheck-path=/healthz' \
- --set 'controller.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-healthcheck-port=10254' \
- --wait
+kubectl apply -f - --server-side <
+```bash
+Create EnvoyProxy configuration for Azure load balancer:
```bash
-helm upgrade --install ingress-nginx ingress-nginx \
- --create-namespace --namespace ingress-nginx \
- --repo https://kubernetes.github.io/ingress-nginx \
- --version 4.12.1 \
- --set 'controller.service.type=LoadBalancer' \
- --set 'controller.extraArgs.enable-ssl-passthrough=true' \
- --set 'controller.service.annotations.service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path=/healthz' \
- --wait
+kubectl apply -f - --server-side <
+Create GatewayClass:
```bash
-helm upgrade --install ingress-nginx ingress-nginx \
- --create-namespace --namespace ingress-nginx \
- --repo https://kubernetes.github.io/ingress-nginx \
- --version 4.12.1 \
- --set 'controller.service.type=LoadBalancer' \
- --set 'controller.extraArgs.enable-ssl-passthrough=true' \
- --wait
+kubectl apply -f - --server-side <
@@ -375,7 +451,10 @@ kubectl get ingress \
-If the preceding command doesn't return a load balancer address then your provider may not have allocated it yet. Once it's available, add a DNS record for the `ROUTER_HOST` to point to the given load balancer address. If it's an IPv4 address, add an A record. If it's a domain name, add a CNAME record.
+If the preceding command doesn't return a load balancer address then your
+provider may not have allocated it yet. Once it's available, add a DNS record
+for the `ROUTER_HOST` to point to the given load balancer address. If it's an
+IPv4 address, add an A record. If it's a domain name, add a CNAME record.
## Configure the up CLI
@@ -435,7 +514,12 @@ kubectl wait controlplane ctp1 --for condition=Ready=True --timeout=360s
## Connect to your control plane
-Connect to your control plane with the `up ctx` command. With your kubeconfig still pointed at the Kubernetes cluster where you installed the Upbound Space, run the following:
+
+Connect to your control plane with the up ctx command. With your kubeconfig
+still pointed at the Kubernetes cluster where you installed the Upbound Space,
+run the following:
+
+
```bash
up ctx ./default/ctp1
diff --git a/utils/vale/styles/Upbound/spelling-exceptions.txt b/utils/vale/styles/Upbound/spelling-exceptions.txt
index 80f6e139c..ec6468956 100644
--- a/utils/vale/styles/Upbound/spelling-exceptions.txt
+++ b/utils/vale/styles/Upbound/spelling-exceptions.txt
@@ -37,6 +37,7 @@ configmaps
CRDs
Crossplane
Crossplane's
+ctx
Datadog
declaratively
downscaling
@@ -144,4 +145,12 @@ XRDs
XRs
Zendesk
Upjet
+GCP
+nginx
+Traefik
+Traefik's
+hostname
+HTTPRoute
+TLSRoute
+passthrough