> ## Documentation Index
> Fetch the complete documentation index at: https://docs.celum.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Publish it on a public name

> Put a VM port or a service behind a hostname with TLS — through the shared Gateway, or on a LoadBalancer address of its own.

By the end of this page something you run answers on a real hostname, over HTTPS, with a certificate that was issued rather than self-signed — and you will know which of the two ways to publish costs you an address and which does not.

This page starts where [Run a VM](https://docs.celum.io/get-started/run-a-vm.md) and [Add a disk to a VM](https://docs.celum.io/get-started/add-a-disk.md) ended: a workload running in your project.

## What this changes on your cluster

Depends entirely on which path you take, and that is the decision this page is about:

| Path | What is created | Costs an LB address |
| - | - | - |
| **LoadBalancer** | a Service `<vm>-lb` of type LoadBalancer | **yes**, one per publication |
| **Gateway** | a ClusterIP Service `<vm>-gw` plus an `HTTPRoute` on a Gateway that already exists | **no** |

The Gateway path shares one address across every hostname published through it. The LoadBalancer path gives each publication its own. Neither is wrong — but on an estate where addresses are a finite, planned resource, the difference compounds.

```mermaid theme={null}
flowchart LR
    W[Workload] --> S1[Service vm-lb] --> L[(LB address, one each)]
    W --> S2[Service vm-gw] --> R[HTTPRoute] --> G[Gateway] --> L2[(LB address, shared)]
    C[cert-manager] -->|"TLS for the hostname"| G
    D[external-dns] -->|"A record"| G
```

## Before you start

* **Certificates**, from onboarding — cert-manager plus a ClusterIssuer. Without one the Gateway path publishes without TLS, which is the outcome it exists to avoid.
* **A domain on the supervisor.** Hostnames derive from it; this is the field set back on [Register a supervisor](https://docs.celum.io/get-started/register-a-supervisor.md).
* **DNS Publishing**, if you want the A record created for you. Without it the route works and nothing resolves — publish the name yourself, or install external-dns.
* **Permission** `vm:Expose` for a VM port, or `gateway:Create` for a platform route.

## The shape of it

**Pick the path → name the hostname → publish → prove it from outside.** The last step is the one that catches a missing DNS record, which is otherwise invisible from inside the cluster.

## Step 1 — Choose LoadBalancer or Gateway

**Take the Gateway when the thing speaks HTTP.** You get TLS terminated by cert-manager, an HTTP→HTTPS redirect, and no address consumed. This is the same mechanism the platform uses to publish Grafana and Prometheus.

**Take the LoadBalancer when it does not.** Gateway exposure is HTTP/TCP-shaped only; a non-TCP protocol is refused outright, and raw TCP — SSH, RDP — is explicitly not implemented: *"raw TCP via TCPRoute/TLSRoute is not implemented yet"*. Those need the LoadBalancer path and an address of their own.

> [tab] Dashboard
>
> The VM detail page → **Networking**. Each port the guest listens on has **Expose**, which publishes it on the VM's LoadBalancer, and **Gateway**, which opens a form naming the Gateway and the hostname it will use.

> [tab] API
>
> `POST /api/supervisors/{name}/vms/{vm}/expose-gateway?namespace=…` for the Gateway path, `POST .../expose-port` for a LoadBalancer.

> [tab] MCP
>
> `expose_vm_gateway` and `expose_vm_port`, reversible with `unexpose_vm_gateway` and `unexpose_vm_port`.

![The Expose port 80 via Gateway dialog with the Gateway set to the project's own envoy, the hostname team-k-web.prod.celum.example.com, an Advanced section, and the buttons Cancel and Expose via Gateway](https://mintcdn.com/celum-e0119be0/sA7S2Ajgv1l2CH1V/images/get-started/vm-gateway-expose.png?fit=max\&auto=format\&n=sA7S2Ajgv1l2CH1V\&q=85\&s=ba3c8cb00e4f5910c6784f10224e9d47)

Gateway on a VM port: the Gateway it lands on and the hostname, filled in for you

## Step 2 — Know which Gateway you land on

You may name one with `gatewayRef` as `namespace/name`. Leave it out and Celum resolves, in order:

1. **the project's dedicated Gateway**, `<project>-envoy`, when the project has one (`dedicatedGateway`). It replaces `tenant-gw`, and every later publication in the project lands on it;
2. otherwise **the project's own `tenant-gw`**, created if it does not exist yet;
3. failing that, **the platform's `shared-gateway`**.

> [!NOTE]
>
> **Each project gateway gets its own LoadBalancer address.** So the first publication in a project does consume one — and every publication after it, in that project, is free. The saving is per project, not per route.

The backing Service is deliberately separate: `<vm>-gw` is a ClusterIP selecting `vm.kubevirt.io/name=<vm>`, distinct from `<vm>-lb`. A VM can therefore be published both ways at once without the two interfering.

## Step 3 — Publish

For a VM port, send the `port`, the `hostname` as an FQDN, and optionally a `clusterIssuer` — it defaults to `letsencrypt-prod`.

For a Service that is not a VM, the platform route endpoint takes the same shape one level up: `POST /api/supervisors/{name}/networking/routes` with `hostname`, `serviceName`, `servicePort`, a `gatewayRef` of `namespace/name`, and a `clusterIssuer`.

Creating a Gateway of your own, rather than using one that exists, is `POST .../networking/gateways`: `gatewayClassName` defaults to `cilium` on a supervisor and is `envoy-gateway` inside a guest cluster — **each surface accepts only its own class**. `requestedIP` pins a specific address from the pool and is cilium-only; `datapath` (`cluster` or `daemonset`) is envoy-gateway-only.

**Result:** the route exists and the Gateway lists the hostname. That is not yet proof that anything answers.

## Verify — from outside, not from inside

Three things have to be true, and they fail independently:

1. **The route attached.** The Gateway reports the hostname on a listener.
2. **The certificate was issued.** A Certificate that stays Pending usually means the ClusterIssuer cannot solve DNS-01 — which is a certificates problem, not a routing one.
3. **The name resolves.** With DNS Publishing installed this happens on its own; without it, nothing does, and the route looks perfectly healthy while being unreachable.

Then probe it from outside the cluster, on the protocol you expect:

```bash theme={null}
curl -sS -o /dev/null -w '%{http_code}\n' https://<hostname>/
```

> [!WARNING]
>
> **Probe both protocols if you care about both.** An estate audit found the same app answering on HTTP on one surface and HTTPS on another — a bare LoadBalancer published `:80` with no TLS, while a Gateway published `:443` with no `:80` listener. Same application, opposite answers to "does https work here?", and neither surface showed anything wrong from the inside. A shared Gateway serving both ports is what removed the ambiguity.

## If you need to undo it

Both paths are reversible and neither destroys anything: `unexpose_vm_port` removes the LoadBalancer Service, `unexpose_vm_gateway` removes the HTTPRoute and its ClusterIP. Routes created at the platform level come off with `DELETE .../networking/routes/{route}` (`gateway:Delete`).

Deleting a Gateway is a wider action than deleting a route — every route still pointing at it stops working, and the shared address goes with it.

> [!NOTE]
>
> Removing a route does not necessarily remove its DNS record. Where external-dns manages the zone with policy `sync`, it reconciles the deletion. With `upsert-only`, or where it does not manage the zone, the name keeps resolving to an address that no longer answers.

## What commonly goes wrong

| Symptom | Cause |
| - | - |
| The Gateway expose is refused for a non-TCP port | Gateway exposure is HTTP/TCP-shaped. SSH and RDP need the LoadBalancer path |
| The route is healthy and the hostname does not resolve | no DNS record. DNS Publishing is an optional onboarding step |
| The certificate never leaves Pending | the ClusterIssuer cannot solve DNS-01. A certificates problem wearing a routing costume |
| Every publication consumes another address | the LoadBalancer path does that by design. Use the Gateway path for anything HTTP |
| A route created in a guest cluster is rejected by the class | `cilium` on a supervisor, `envoy-gateway` in a guest. Each surface accepts only its own |
| `requestedIP` is ignored | it is cilium-class only |
| HTTPS works and HTTP does not, or the reverse | the Gateway's listener set. One that serves both ports answers both |

## Next

> [card] [Gateways & Routes](https://docs.celum.io/networking/gateways-and-routes.md)
>
> Listeners, TLS, datapath modes, and publishing tenant APIs.

> [card] [VM networking](https://docs.celum.io/vms/networking.md)
>
> The VM-side view, including what the guest is actually listening on.

> [card] [Pools & IPAM](https://docs.celum.io/networking/pools-and-ipam.md)
>
> Where LoadBalancer addresses come from, and how full the pool is.

> [card] [Security overview](https://docs.celum.io/security/overview.md)
>
> Seeing who is actually reaching what you just published.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.