Documentation/Exposure Model

Exposure Model#

Luma separates the control plane from the data plane.

Roles#

Tailscale is not the default business data plane. It only carries data traffic when a service explicitly chooses exposure: tailscale-relay.

Exposure modes#

cn-edge#

Primary domestic public services.

User -> Cloudflare DNS -> CN Traefik -> CN service

Use for:

Manifest:

name: app
image: ghcr.io/your-org/app:latest
region: cn
exposure: cn-edge
domain: app.example.com
port: 3000
replicas: 2

Luma generates:

By default, public traffic enters through one Traefik instance constrained to ${meta.region}=cn and ${meta.ingress}=true. If you have multiple CN nodes, DNS still points to the configured edge target; Traefik receives the request there and forwards to matching service allocations discovered through the Nomad provider.

tailscale-relay#

Home or custom-region services exposed through the existing edge over Tailscale. For a custom region, run luma region update render --exposure tailscale-relay before selecting that exposure in the app manifest. See the upgrade and rollout guide.

User -> Cloudflare DNS -> CN Traefik -> Tailscale -> home service

Use for:

Do not use for:

Manifest:

name: home-panel
image: ghcr.io/your-org/home-panel:latest
region: home
exposure: tailscale-relay
domain: panel.example.com
port: 8080
publishPort: 8080
replicas: 1
relay:
  host: home-1.your-tailnet.ts.net

Luma generates:

Operational requirement:

tcp-relay#

Public native TCP services through a dedicated Traefik TCP entrypoint.

Client -> Cloudflare DNS -> edge Traefik TCP entrypoint -> task host port

Use for:

Do not use for:

Manifest:

name: ledger-db
image: mysql:8.4.9
region: home
node: lab
exposure: tcp-relay
domain: ledger-db.example.net
port: 3306
publishPort: 3306

Luma generates:

Ordinary MySQL clients do not start with an HTTP Host header, and should not be assumed to provide reliable TLS SNI before the server handshake. Treat tcp-relay as port-exclusive: one published port routes to one TCP service.

cloudflare-tunnel#

Home or private services exposed directly through Cloudflare Tunnel.

User -> Cloudflare -> cloudflared -> service

Use for:

Manifest:

name: home-tool
image: ghcr.io/your-org/home-tool:latest
region: home
exposure: cloudflare-tunnel
domain: tool.example.com
port: 8080
replicas: 1
tunnel:
  tokenEnv: CLOUDFLARE_TUNNEL_TOKEN

Luma generates:

Cloudflare Tunnel public hostname configuration is still managed in Cloudflare. Luma skips normal DNS A-record sync for this mode.

external-edge#

Global services with their own overseas/public edge.

User -> Cloudflare DNS -> external/global edge -> global service

Use for:

Manifest:

name: ai-gateway
image: ghcr.io/your-org/ai-gateway:latest
region: global
exposure: external-edge
domain: ai.example.com
port: 3000
replicas: 1
dns:
  target: 198.51.100.10

Luma generates:

Use dns.target to choose the public/global edge IP for this service. Without a separate global edge target, a global workload can still be scheduled in region: global, but public HTTP traffic will not magically enter through every global node.

none#

Internal services and workers.

No public route

Use for:

Manifest:

name: fetch-worker
image: ghcr.io/your-org/fetch-worker:latest
region: global
exposure: none
replicas: 1
env:
  QUEUE_URL: redis://redis:6379/0

Luma generates:

Decision rule#