Repository navigation
feat(infra): serve domain mode through Caddy with HTTPS - #111
Merged
Merged
Conversation
Domain mode documented a Caddyfile assembled from each stacklet's
`caddy.snippet`, but nothing wrote it. The snippets were never read and
the proxy container mounted a file that did not exist.
In domain mode, `stack up`, `stack down` and `stack destroy` now write
`{data_dir}/infra/Caddyfile` from the snippets of the stacklets that are
up, and have a running Caddy reload it once the containers have changed.
- A crash-looping stacklet keeps its routes.
- A host no running stacklet claims gets a 404 from a `*.<domain>` site.
- Without TLS every site is served as plain HTTP on port 80.
- A reload Caddy rejects leaves it on its previous routes and prints a
warning; the command itself still succeeds.
- Port mode writes nothing and reloads nothing.
Snippets now use `{$STACK_DOMAIN}`, the variable the Caddy container is
given. `{$FAMSTACK_DOMAIN}` was never set there.
Domain mode could only serve plain HTTP. With `[core] dns_provider` set
to `hetzner` or `cloudflare`, Caddy gets two certificates from Let's
Encrypt: a wildcard for `*.<domain>`, which every stacklet's subdomain
uses, and one for the bare domain, which a wildcard does not cover. It
proves ownership with a DNS-01 challenge through the provider's API, so
nothing on the LAN has to be reachable from the internet.
- `stack infra dns-token` stores the provider's API token as a secret,
and replaces it. The first `stack up infra` asks for it in a
terminal. The token is never printed, only its length.
- `stack up infra` refuses an unsupported provider or a missing token,
and names the command that fixes it.
- An unsupported provider leaves the current Caddyfile in place for
every other stacklet, with a warning.
- The URLs the stack renders (`{url}`, `{<id>_url}`, `{home_url}`) are
https once a provider is set.
Hetzner needs a Hetzner Cloud API token with Read & Write access,
Cloudflare a token with Zone.Zone:Read and Zone.DNS:Edit on the zone.
Caddy needs the provider's plugin compiled in, which the infra image
gains in a separate change.
The fixture's command was a string, which compose splits itself, and the splitting dropped the backslashes of `\r\n`. The response arrived as one line with no header end, which the health probe tolerated and a real HTTP client such as Caddy's reverse proxy rejects. The command is now a list, passed to the container as written, and the response closes the connection after the body.
A certificate through a DNS-01 challenge needs the DNS provider's plugin compiled into Caddy, and the stock image has none, so the proxy could not obtain certificates for `[core] dns_provider`. The infra stacklet now builds its Caddy image with xcaddy on the Caddy 2 line, with both plugins, so switching providers is a config change and not a rebuild. The Hetzner plugin is v2, which uses the Hetzner Cloud API; v1 used the deprecated DNS Console API. `stack up infra` rebuilds the image, and Watchtower leaves it alone because there is nothing to pull.
The infra stacklet published DNS on 53 and the proxy on 80 and 443 through `PORT_BIND_IP`, which the framework sets to 127.0.0.1 in domain mode, the only mode infra runs in. No other device could resolve a name through AdGuard or reach a service through Caddy. These three ports now bind all interfaces. `PORT_BIND_IP` still keeps every service behind the proxy, and AdGuard's own admin pages, on the loopback.
AdGuard's first-run wizard was published on host port 3000, where development servers live. With one running, `stack up infra` failed on a port conflict that named the port and not the stacklet. The wizard is now on 42081, next to the admin panel on 42080, inside the 42xxx range every other stacklet uses. A new test checks that range for every published port, and that no two stacklets publish the same one. An instance that has finished the wizard is not affected.
AdGuard ran `:latest`, which Watchtower replaces with whatever is published, a new minor line included. AdGuard publishes exact versions only, with no line tag to follow, so the image is pinned to the current release and moves with a bump in the compose file.
In domain mode the stack gives every stacklet the URL `<id>.<domain>`,
in hints, welcome messages and links other stacklets build, but several
snippets routed elsewhere:
- photos and chatai proxied to container names no compose file uses
(`immich_server`, `chatai`), so both answered 502.
- messages served Synapse at `messages.<domain>` and Element at
`element.<domain>`, while the stack sends browsers and Element's
homeserver setting to `messages.<domain>`. It is now one host: Element
for browsers, Synapse under `/_matrix` and `/_synapse`, and
`/.well-known/matrix/client` and `/server` built from the request so
they are right with and without TLS.
- memory served the wiki at `wiki.<domain>`, while its hint and core's
`/go` links point at `memory.<domain>`. The wiki's own absolute links
move with it.
- code had no snippet at all.
A new test checks every snippet's upstreams against the container names
in the compose files, and that every stacklet showing `{url}` has a site
at `<id>.<domain>`.
Clients on the LAN can reach the AI stacklet's endpoints through the proxy: `stt.<domain>` for speech to text, `tts.<domain>` for text to speech and `openai.<domain>` for the OpenAI-compatible LLM API. whisper-server and oMLX stay bound to the Mac's loopback. The proxy reaches them through Docker's host gateway, which the Caddy container now maps as `host.docker.internal`, as every other container that talks to the host already does.
The infra README pointed at `dns.<domain>` for AdGuard's admin pages, but no snippet served it. The pages are there once the setup wizard has run; before that the host answers 502.
Services that are not stacklets, such as a dashboard or a Docker UI,
had no place in domain mode. Only one proxy can own ports 80 and 443, so
a hand-written Caddyfile cannot run beside the one the stack assembles.
`{data_dir}/infra/Caddyfile.local` is now appended after the stacklet
snippets every time the Caddyfile is written. The stack reads it and
never writes it; `stack up infra` applies an edit. Its sites get the
wildcard certificate like any other subdomain. A host that a running
stacklet also serves makes Caddy reject the reload, with a warning.
The docs said domain mode needs "wildcard DNS on your router". It needs two records to the Mac's LAN IP, `*.<domain>` and `<domain>`, because a wildcard covers neither the bare name's DNS nor its certificate. They belong in AdGuard's DNS rewrites, and at the DNS provider for devices that bypass AdGuard. - The infra README covers enabling domain mode, the DNS records, TLS (which token each provider needs, where it is kept, how to rotate it), `Caddyfile.local`, and the wizard on 42081. - The admin guide explains `dns_provider` and the wiki's new host. - The operator reference lists infra's ports and `stack infra dns-token`.
Domain mode now assembles its routes and can serve HTTPS, but HTTPS has not yet run against a real DNS provider, and how TLS and the admin's own sites are configured can still change. `stack list` now marks infra as beta and `stack up infra` says so, with a reminder to back up first.
The family wiki is served at `memory.<domain>`, the address the stack hands out for the memory stacklet, but `wiki.<domain>` is the name people type. It now redirects to the same page on the memory host, path and query included. The redirect is temporary (302). Browsers keep a permanent one indefinitely, which would get in the way if the family-facing name changes again.
Pointing the router's DNS at the Mac makes every device in the house depend on it, and setting domain mode up takes networking knowledge no other stacklet asks for. Nothing said so. - The first `stack up infra` now warns, once, before anything is set up: what depends on it, what knowledge it takes, and to note the router's current DNS setting first. - The infra README and the admin guide say the same up front, and suggest port mode to anyone for whom this is new. - The operator reference lists changing the router's DNS, and stopping or destroying infra, as danger zones for agents.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
<id>.<domain>and core at the bare domain, through the infra stacklet's Caddy.stack up,stack downandstack destroyrebuild the routes from each running stacklet'scaddy.snippetand reload Caddy.wiki.<domain>redirects to the family wiki, and your own sites go in~/famstack-data/infra/Caddyfile.local.stack upwarns that it becomes DNS for the whole network.Setup, DNS records and token scopes:
stacklets/infra/README.md. Routing and reload are tested end to end on a throwaway instance. HTTPS is tested against a real Caddy build, but no certificate has been issued with a real provider token yet.