Skip to main content
Version: v0.25.0 (Latest)

Local Multi-Domain Development

The single-port multi-tenant model needs a base domain with a wildcard so the operator host and the tenant hosts resolve. Locally this works with no DNS setup at all. To test with a real phone wallet on a separate device, expose the stack on a public wildcard domain. This page covers both.

Deployment repository boundary

The loopback default in the first section uses the Enterprise Development Kit Deployment repository: the Compose stack and the single-port Traefik gateway under compose/, with the local wildcard certificate from scripts/gen-local-wildcard-cert. For phone-wallet testing, bring your own tunnel or public DNS/TLS setup while keeping the same gateway contract.

Default: saas.localtest.me, No DNS

The default base domain for local runs is saas.localtest.me. The localtest.me domain and all of its subdomains resolve to 127.0.0.1 without any host-file edits or a local DNS server. So both the operator host and the tenant hosts work immediately:

  • https://platform.saas.localtest.me is the operator host.
  • https://acme.saas.localtest.me, https://globex.saas.localtest.me, and https://initech.saas.localtest.me are the three default tenants.

This is enough for browser-based development on the same machine that runs the stack. Follow Install on Docker to bring it up behind the Traefik gateway, generate the local wildcard certificate, and trust the local CA. Because localtest.me resolves to loopback, this path does not reach a phone or another device.

Public Overlay for a Real Phone Wallet

A real wallet on a phone needs two things the loopback default cannot give it: public DNS that resolves from the phone, and a certificate the phone trusts. The wildcard requirement does not change: the public domain still needs platform.<base-domain> and *.<base-domain> to reach the operator host and the tenant hosts on one port. Two tunnels provide this.

Configure your tunnel to terminate or pass through TLS for platform.<base-domain> and *.<base-domain>, forward traffic to the local gateway on 443, and preserve the original Host header. Set the deployment base domain in the deployment repository environment to the public base domain before bringing the stack up, then run the normal Compose files.

ngrok Wildcard

An ngrok wildcard endpoint gives a public *.{subdomain}.ngrok.dev style domain that tunnels to the local gateway on 443. Point the deployment base domain at the ngrok wildcard so the operator host and tenant hosts resolve publicly, and ngrok terminates TLS with a publicly trusted certificate, so the phone wallet trusts it with no local CA import. A reserved wildcard domain keeps the host stable across runs, which matters because issued credentials and advertised issuer URLs embed the host.

Cloudflare Tunnel

A Cloudflare Tunnel maps a wildcard hostname on a domain you control to the local gateway. Cloudflare serves a publicly trusted certificate for the wildcard, so the phone wallet trusts the connection without importing the local CA. Configure the tunnel to forward to the gateway on 443 and preserve the Host header so tenant resolution still reads the right tenant.

Either overlay keeps the single-port model intact: the tunnel is the public front, and behind it the gateway routes by host and path exactly as in Install on Docker.

Tier 3: Your Own Domain plus Let's Encrypt

The tunnel overlays put someone else's front in the path. If you control a domain and can point its public DNS at this machine, Traefik can obtain and renew a publicly trusted Let's Encrypt certificate directly, with no tunnel. The single-port model is unchanged: the operator host is platform.<base-domain>, tenants are *.<base-domain>, the tenant is identified by the inbound Host, and the gateway preserves that Host end to end.

Configure Traefik, your gateway, or your DNS provider to obtain certificates for the operator host and tenant hosts. For arbitrary tenants, use DNS-01 and issue a wildcard certificate for *.<base-domain> plus platform.<base-domain>. If you use per-host certificates, issue certificates for every tenant host before exposing that tenant.

The operator host platform.<base-domain> and the wildcard *.<base-domain> must both resolve publicly to this machine's external IP.

Certificate Modes

Two certificate models work:

  • Per-host certificates for the operator host and each named tenant host. This is useful for a fixed demo tenant set, but every new tenant needs certificate issuance before it can go live.
  • A wildcard certificate for *.<base-domain> plus the operator host platform.<base-domain>. This is the recommended model for arbitrary tenant subdomains. Use DNS-01 validation with your DNS provider when using ACME, because HTTP-01 cannot validate a wildcard SAN.

Pointing DNS at This Machine

Public DNS for platform.<base-domain> and *.<base-domain> must resolve to this machine's external IPv4 (an A record) and, if used, external IPv6 (an AAAA record). Keep any CDN or proxy mode off (orange-cloud off on Cloudflare) so the gateway sees the raw Host and terminates TLS itself. Tenant resolution reads the unrewritten Host, and TLS-ALPN requires the gateway to own the TLS handshake.

If you run dynamic DNS, update the records before starting the public test and confirm they resolve from outside your network.

Port Forwarding

Forward inbound TCP 443 from the external IP to this machine. The two challenge modes differ in what 443 is for:

  • tls-alpn rides 443 for both issuance and serving, so 443 is all it needs.
  • dns validates over DNS, so 443 is needed only to serve traffic, not to issue the certificate.

Forward :80 only if you want the web to websecure HTTP-to-HTTPS redirect reachable from outside. Neither challenge needs :80 for issuance, because this option does not use the HTTP-01 challenge.

The box reaching its own public hostnames through NAT needs router hairpin (loopback) support, which many home routers lack. If hairpin is unavailable, add a local hosts or dnsmasq entry mapping platform.<base-domain> and the tenant hosts to 127.0.0.1 or the box's LAN IP. The Let's Encrypt certificate stays valid either way, because certificate validity does not depend on how the name resolves locally. In-compose service-to-service calls already work through the Traefik network aliases, and because the certificate is publicly trusted, the services validate the gateway TLS with the JVM default truststore. This mode mounts no private CA truststore, unlike the self-signed overlay.

Staging First

When you use ACME automation, run against the ACME staging endpoint first to validate DNS and reachability without burning production rate limits. Switch to the production endpoint only after the gateway serves the staging certificate correctly.

Public Exposure Warning

This option binds the machine to the public internet on 443 and exposes the operator console and every tenant endpoint publicly. Use a disposable test domain, keep the machine patched, restrict the router forward to 443 only, and tear the stack down when you are done.

CA Trust

How you handle CA trust depends on the front:

  • With the loopback default and a self-signed local CA, import local-ca.crt into the device's trust store, or run mkcert -install when mkcert issued the certificate. See Install on Docker.
  • With an ngrok wildcard or a Cloudflare Tunnel, the public front terminates TLS with a publicly trusted certificate, so no local CA import is needed on the phone.
  • With your own domain plus Let's Encrypt, Traefik serves a publicly trusted certificate directly, so no local CA import is needed on the phone.

When the public overlay terminates TLS itself, the local wildcard certificate is no longer on the public path. It still serves any direct browser access on the loopback host during the same run.

Local Equals Production

The host scheme, the gateway contract, and the routing table are the same locally and in production. The only differences are the base domain and where TLS terminates. A flow that works at https://acme.saas.localtest.me works the same way at https://acme.example.com, because both resolve the tenant from the Host header and route by the same paths. See the deployment architecture for the contract that holds across every environment.

Next Steps