Skip to content

Expose a service to the internet

This guide walks you from “service running internally” to “publicly reachable on https://api.example.com” using Rune’s built-in ingress controller and ACME orchestrator. It assumes you already have a single-node cluster running (see Quick start) on a host with a public IP.

If you’re starting from scratch on a fresh VM, skip to the DigitalOcean deploy guide which covers the host-side setup as well.

  1. A runed node reachable from the public internet on :80 and :443.
  2. The node started with --node-role=edge (this enables the ingress controller and the ACME orchestrator on that node). Tip: if you’re on a laptop using --dev-mode, edge is enabled automatically — no separate flag needed.
  3. A DNS A record pointing your hostname at the node’s public IP. ACME’s HTTP-01 challenge requires this — the certificate authority will hit http://<host>/.well-known/acme-challenge/<token> and expect to land on your node.
  4. An acme.email configured. Let’s Encrypt rejects accounts without a contact address.

The minimum extra flags on top of a normal startup:

Terminal window
runed \
--data-dir=/var/lib/rune \
--node-role=edge \

Or, equivalently, in /etc/rune/runefile.toml:

data_dir = "/var/lib/rune"
[node]
role = "edge"
[acme]
# directory = "" # defaults to Let's Encrypt production

Restart runed. You’ll see two new lines in the log:

Ingress + ACME enabled (edge node) http=:80 https=:443
acme orchestrator started

If the ports fail to bind (permission denied), either run runed as root, give the binary the cap_net_bind_service capability, or run in --dev-mode for laptop testing.

service:
name: api
namespace: default
image: ghcr.io/example/api:1.4.0
scale: 2
ports:
- { name: http, port: 8080 }
expose:
host: api.example.com
port: http
tls:
auto: true # use ACME (default Let's Encrypt prod)

Apply it:

Terminal window
rune cast api.yaml

That’s the entire developer-facing change. The orchestrator publishes endpoints, the edge node adds the route to its ingress router, and the ACME orchestrator queues a certificate request.

Terminal window
$ rune get ingresses
NAMESPACE SERVICE HOST TLS CERT EXPIRES
default api api.example.com acme pending -
# … 10–30 seconds later …
$ rune get ingresses
NAMESPACE SERVICE HOST TLS CERT EXPIRES
default api api.example.com acme ready 89d

For more detail (including the last error if a request failed):

Terminal window
rune get ingress api -n default -o yaml

The orchestrator retries on failure with exponential backoff, capped at the acme_renewal_window (default: 30 days before expiry). Existing certificates keep serving traffic while a renewal is in flight, so a temporary issuance error never causes a public-facing outage.

Terminal window
curl -I https://api.example.com/healthz
# HTTP/2 200
# server: rune-ingress

Server: rune-ingress confirms the request went through Rune’s edge proxy. The TLS handshake will use the freshly issued certificate.

If you manage certificates out of band — wildcard certs from your CA, Cloudflare Origin Certs, mTLS rigs, etc. — set tls.mode: manual and reference a Secret containing the cert and key:

service:
name: api
expose:
host: api.example.com
port: http
tls:
mode: manual
secret: api-tls # Secret must contain `tls.crt` and `tls.key`

tls.secret accepts three shapes — same convention as other typed refs in Rune:

ShapeResolves to
api-tlsservice’s own namespace
shared/cf-origincross-namespace shorthand
secret:cf-origin.shared.runeFQDN secret ref

The cross-namespace forms are the right pick for a single wildcard cert (or a Cloudflare Origin Cert) you want to share across services in many namespaces — park it once in shared, reference it everywhere.

Create the secret as a normal Rune secret containing tls.crt and tls.key:

secret:
name: api-tls
namespace: default
data:
tls.crt: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
tls.key: |
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
Terminal window
rune cast api-tls.yaml

Rotate by updating the secret — the ingress controller observes the new Secret.Version on its next reconcile tick (default 2 s) and pushes the new cert into the loader. No service restart needed.

  • DNS not propagated yet. The ACME provider must reach your node. If dig +short api.example.com doesn’t return your edge node’s IP from a public DNS server, the challenge will fail. Wait, then the orchestrator will retry on its own.
  • Port 80 blocked upstream. HTTP-01 needs port 80 specifically. Cloud firewalls and security groups must allow inbound 80 and 443. (If only 443 is allowed, switch to manual TLS.)
  • acme.directory left at default in CI. Tests should point at a Pebble URL — Let’s Encrypt’s production endpoint has rate limits that you will hit.
  • Multiple edge nodes. Phase 1 ships single-node ingress. Multi-node leader election lands with Phase 2; for now run one edge node and front it with your cloud’s L4 load balancer if you need HA.