deploy.sh: escape the image-build loop vars for the remote string

This commit is contained in:
Frank
2026-08-22 04:04:24 -07:00
parent 1f4766f233
commit dc421efea4
5 changed files with 212 additions and 71 deletions
+32 -23
View File
@@ -1,41 +1,48 @@
# Network Architecture
Both hosts run Caddy as the single entry point. Every service is reachable by hostname; the router firewall governs external access. On Dankclaw, SearXNG's outbound traffic exits through the Gluetun VPN container.
One Caddy fronts both hosts. It runs as a container on Dankclaw (192.168.0.143) and terminates TLS for every service on the LAN. dnsmasq on Dankclaw answers the service names for LAN clients; Porkbun answers them for the WAN. The G36 router firewall governs external access. SearXNG outbound traffic exits through the Gluetun VPN container.
## Topology
```mermaid
graph TB
subgraph WAN[Router 192.168.0.1 firewall]
subgraph WAN["G36 router 192.168.0.1"]
EXT[Internet]
end
EXT -->|port forward| DS[Caddy Dankserver]
EXT -->|port forward| DC[Caddy Dankclaw]
subgraph Dankserver[192.168.0.235]
DS --> R[RomM :8085]
DS --> P[Copyparty :3923]
DS --> W[Calibre-Web :8083]
end
EXT -->|port forwards| C
subgraph Dankclaw[192.168.0.143]
DC --> O[OpenWebUI :3000]
DC --> G[Gitea :3001]
DC --> F[ComfyUI :8188]
DC --> GR[Grafana :3002]
DC --> PR[Prometheus :9090]
DC --> SE[SearXNG :8081]
C[Caddy :80 :443]
D[dnsmasq :53]
C -->|comfyui:8188| F[ComfyUI]
C -->|192.168.0.143:3000| G[Gitea quadlet]
C -->|192.168.0.143:8081| SE[SearXNG quadlet]
C -->|192.168.0.143:3005| O[OpenWebUI quadlet]
SE -->|network_mode service| GL[Gluetun VPN]
GL -->|tunnel| EXT
end
subgraph Dankserver[192.168.0.235]
C -->|192.168.0.235:8085| R[RomM]
C -->|192.168.0.235:3923| P[Copyparty]
C -->|192.168.0.235:8083| W[Calibre-Web]
C -->|192.168.0.235:32400| PL[Plex host service]
end
CLIENT[LAN clients] -->|DHCP DNS| D
```
Dankserver runs no proxy of its own. Its services expose published LAN ports, and the Dankclaw Caddy routes the names to those ports.
## Hostnames
| Host | Convention |
|------|------------|
| Dankserver | `*.dankserver.local` → Caddy → service |
| Dankclaw | `*.dankclaw.local` → Caddy → service |
| Scope | Convention | Resolved by |
|-------|------------|-------------|
| LAN | `romm.franksdank.cloud` → 192.168.0.143 | dnsmasq (`addn-hosts`) |
| WAN | `romm.franksdank.cloud` → 71.227.227.172 | Porkbun A record |
Users never type ports; Caddy maps names to upstreams.
Users type a name and nothing else. Caddy maps the name to the upstream. The split between the two resolvers removes the need for router NAT loopback. See [[LAN-DNS]] for the flow and the router hand-off.
## TLS
Caddy holds one Let's Encrypt certificate per vhost, issued and renewed through the DNS-01 challenge via the Porkbun module. No inbound port 80/443 is required. Details in [[Caddy]].
## Gluetun Path (SearXNG only)
@@ -43,12 +50,14 @@ SearXNG attaches to the Gluetun network namespace (`network_mode: service:gluetu
## Access Policy
- All services assume external access is possible; the **router firewall** governs what the WAN can reach.
- The **G36 router firewall** governs what the WAN can reach. Port forwards are manual router steps; the repo tracks no router state.
- Caddy adds HTTP basic auth on the homepage and on sensitive services.
- LAN subnet: `192.168.0.0/24`.
## See Also
- [[LAN-DNS]] — name resolution, dnsmasq, router hand-off
- [[Caddy]] — vhost map, TLS, basic auth
- [[Dankserver Stack]], [[Dankclaw Stack]]
- [[Caddy]], [[Gluetun]]
- Knowledge base: [[VPN Routing for SearXNG]], [[SearXNG Tuning]]
- [[Gluetun]]
- Knowledge base: [[VPN Routing for SearXNG]], [[SearXNG Tuning]]
+59 -43
View File
@@ -1,71 +1,87 @@
# Caddy
Caddy is the reverse proxy for both hosts. It terminates TLS, routes hostnames to containers, and protects the homepage with HTTP basic auth.
Caddy is the single reverse proxy for both hosts. It runs as a container on Dankclaw (192.168.0.143), terminates TLS with real Let's Encrypt certificates, routes hostnames to containers on both hosts, and protects the homepage and sensitive services with HTTP basic auth.
**Hosts:** Dankserver + Dankclaw — **Docs:** https://caddyserver.com/docs
**Docs:** https://caddyserver.com/docs
## Facts (from live inventory + knowledge base)
## Facts (live inventory)
| Item | Value |
|------|-------|
| Image | `docker.io/library/caddy:latest` |
| Config | `/etc/caddy/Caddyfile` (bind mount) |
| Data | `/root/.local/share/caddy` (certs, ACME state) |
| Ports | 80, 443 |
| Existing on Dankserver | systemd `caddy` service (native), config at `/etc/caddy/Caddyfile` — being migrated into this repo |
| Image | `franksdank/caddy-porkbun:latest` (local build, `dankclaw/caddy/Containerfile`) |
| Config | `/home/dank/Containers/caddy/Caddyfile` (bind mount, rendered by deploy) |
| Data | `/home/dank/Containers/caddy/data` (certs) and `.../config` (ACME state) |
| Ports | 80, 443 on 192.168.0.143 |
| TLS | Let's Encrypt, DNS-01 via the Porkbun module (one cert per vhost) |
| Runs on | Dankclaw only — Dankserver has no Caddy |
## Existing Dankserver Caddyfile (live, to migrate)
The Caddyfile in git (`dankclaw/Caddyfile`) carries a `REPLACE_WITH_HASH` placeholder. `scripts/deploy.sh` renders the real basicauth hash from the host `.env` (`CADDY_BASICAUTH_HASH`) into the mounted path before `up`. See [[Deployment Flow]].
The live Caddyfile proxies:
- `plex.franksdank.casa` / `plex.franksdank.cloud` / `plex.dankserver.local` → `localhost:32400` (Plex)
- `books.franksdank.cloud` → `localhost:8083` (Calibre-Web)
- `llms.franksdank.cloud` / `llms.dankserver.local` → `localhost:3000` (OpenWebUI — being moved to Dankclaw)
- `search.dankserver.local` → `localhost:8081` (SearXNG — being moved to Dankclaw)
## The porkbun image
## Hostname Convention
Every service gets a `.dankserver.local` or `.dankclaw.local` hostname so no one has to remember port numbers:
Stock Caddy cannot solve the ACME challenge because the router exposes no inbound port 80/443. The local image adds the `github.com/caddy-dns/porkbun` module, which answers the DNS-01 challenge by creating and deleting a `_acme-challenge` TXT record through the Porkbun API. No inbound port is needed. Rebuild only when Caddy or the module needs an update:
```
romm.dankserver.local
books.dankserver.local
files.dankserver.local
search.dankclaw.local
chat.dankclaw.local
gitea.dankclaw.local
comfyui.dankclaw.local
grafana.dankclaw.local
prometheus.dankclaw.local
podman build -t franksdank/caddy-porkbun:latest -f dankclaw/caddy/Containerfile .
```
## Basic Auth
Generate a bcrypt hash with `caddy hash-password`, then:
The Caddyfile references the credentials through the environment:
```
chat.dankclaw.local {
basicauth {
dank $2a$14$xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
}
reverse_proxy localhost:3000
acme_dns porkbun {
api_key {env.PORKBUN_API_KEY}
api_secret_key {env.PORKBUN_API_SECRET_KEY}
}
```
The homepage uses basic auth too; see [[Homepage]].
compose passes both variables from the host `.env`. Keep the real keys in `.env` only; `.env.example` holds placeholders. The Porkbun account holds only `franksdank.cloud`.
## Homepage
## Vhost map
A static page lists all services with live status (up/down) and a short description plus a link to each service's documentation. Served by Caddy with `file_server` and protected by basic auth.
Every vhost uses the flat `franksdank.cloud` convention. The full list lives in `dankclaw/Caddyfile`. Routing by host:
| Upstream | Reach |
|----------|-------|
| Dankclaw services (compose network) | by container name, e.g. `comfyui:8188` |
| Dankclaw quadlet services | by published LAN port, e.g. `192.168.0.143:3000` (Gitea) |
| Dankserver services | by published LAN port, e.g. `192.168.0.235:8085` (RomM) |
The bare-IP catch-all `:80` 302-redirects to `https://dankclaw.franksdank.cloud/`. The legacy name `dankserver.franksdank.cloud` 302-redirects there too. `plex.franksdank.casa` is the only plain-HTTP vhost; it cannot get a DNS-01 cert because `franksdank.casa` is not in the Porkbun account. The canonical Plex name is `plex.franksdank.cloud`.
## Basic auth
Generate a bcrypt hash, then reference it:
```
chat.franksdank.cloud {
basicauth {
dank $2a$14$xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
}
reverse_proxy 192.168.0.143:3005
}
```
The homepage also uses basic auth; see [[Homepage]].
## Operations
Validate before a reload:
```
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
```
Certificates and ACME state live in the caddy data volume. Back it up with the stack. Each vhost owns one certificate; renewals are automatic and also use DNS-01.
## Gotchas
- Caddy auto-provisions Let's Encrypt certs for public domains; `.local` names stay internal (no external DNS).
- Validate before reload: `caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile`.
- Certificates live in the caddy data volume; back it up with the stack.
- A new vhost needs three things: a dnsmasq `addn-hosts` line, a Porkbun A record, and the Caddyfile block. See [[LAN-DNS]].
- The G36 has no NAT loopback, so LAN clients must resolve the name to the LAN IP. That is the dnsmasq role in [[LAN-DNS]].
- Caddy serves HTTPS on 443 and HTTP on 80 (redirects). The router needs no 80/443 port forward for LAN use.
## See Also
- [[Dankserver Stack]]
- [[Dankclaw Stack]]
- [[LAN-DNS]] — how names resolve on the LAN and WAN
- [[Network Architecture]] — topology, access policy
- [[Homepage]]
- Knowledge base: [[Gitea Architecture]] (the existing caddy chain on Dankclaw)
- [[Dankclaw Stack]]
+115
View File
@@ -0,0 +1,115 @@
# LAN DNS
The homelab gives every service a name under `franksdank.cloud`. Users type a name; they never type a port number. This page covers how names reach users: the dnsmasq server, the G36 router hand-off, and the split between LAN and WAN resolution.
See [[Caddy]] for the reverse proxy that serves the names, and [[Network Architecture]] for the full topology.
## Components
| Component | Where | Role |
|-----------|-------|------|
| dnsmasq | Dankclaw quadlet (`dankclaw/dnsmasq/`) | Answers the flat service names for LAN clients |
| SURFboard G36 | 192.168.0.1 | Hands dnsmasq to clients as the DHCP DNS server |
| Porkbun | Registrar for `franksdank.cloud` | Answers the same names for the WAN |
| Caddy | Dankclaw (192.168.0.143:443) | Serves every name over HTTPS |
## How a name reaches a client
```mermaid
sequenceDiagram
participant C as LAN client (laptop, phone)
participant G as G36 router (192.168.0.1)
participant D as dnsmasq (192.168.0.143:53)
participant P as Porkbun (WAN)
participant X as Caddy (192.168.0.143:443)
C->>G: DHCP request
G-->>C: lease + DNS = 192.168.0.143
C->>D: dig romm.franksdank.cloud
D-->>C: 192.168.0.143
Note over D: addn-hosts answer<br/>(dankclaw/dnsmasq/addn-hosts)
C->>X: https://romm.franksdank.cloud
X-->>C: RomM (192.168.0.235:8085 upstream)
C->>P: dig franksdank.cloud (unknown name)
P-->>C: 71.227.227.172
```
dnsmasq answers only the names in `addn-hosts`. Every other query goes to the upstream resolvers `1.1.1.1` and `1.0.0.1`. The zone apex and unknown subdomains fall through to Porkbun. This avoids a wildcard override.
## Why split DNS
The G36 has no NAT loopback (hairpin). A LAN client that resolves a name to the WAN IP and then connects to it would time out. Split DNS removes the hairpin: LAN clients get the LAN IP, WAN clients get the public IP. Both use the same name and the same Let's Encrypt certificate. See [[Caddy]] for the TLS side.
## dnsmasq quadlet
- **Location:** `dankclaw/dnsmasq/` — `dnsmasq.container` (quadlet), `dnsmasq.conf`, `addn-hosts`, `Containerfile`
- **Port:** 53/udp + 53/tcp on 192.168.0.143
- **Host prerequisite 1:** `sysctl net.ipv4.ip_unprivileged_port_start=53` (persisted in `/etc/sysctl.d/99-dnsmasq.conf`)
- **Host prerequisite 2:** `DNSStubListener=no` in `/etc/systemd/resolved.conf` (the resolved stub would otherwise hold port 53 inside the container netns)
- **Rebuild + restart after a change:**
```
podman build -t dnsmasq:latest -f dankclaw/dnsmasq/Containerfile .
systemctl --user daemon-reload
systemctl --user restart dnsmasq
```
Add a new service name in two places:
1. `dankclaw/dnsmasq/addn-hosts` — `192.168.0.143 <name>.franksdank.cloud`
2. Porkbun — A record `<name> 71.227.227.172` (so WAN clients and the DNS-01 challenge work)
Then add the vhost to [[Caddy]] and run `./scripts/deploy.sh dankclaw`.
## G36 router DNS hand-off
The router must tell DHCP clients to use dnsmasq. Until that step is done, new clients get the router's own DNS relay and the flat names do not resolve for them. Existing clients that already use dnsmasq are unaffected.
**Manual step (web GUI at `https://192.168.0.1`):**
1. Open **Gateway → Connection → Local IP Network → IPv4**.
2. Uncheck **DNS Proxy** (disable the router DNS relay).
3. Set **DNS** to **Statically configured**.
4. Set **Primary DNS** to `192.168.0.143`.
5. Leave the secondary field empty or set `0.0.0.0`.
6. Apply, then re-lease DHCP on each client (`sudo dhclient -r && sudo dhclient`).
The router admin interface has no programmatic API in this firmware. The web GUI is the only control path. Keep the credentials out of this repo; the repo tracks no router secrets.
Verify after the change:
```
# on a LAN client
resolv.conf # nameserver 192.168.0.143
dig romm.franksdank.cloud +short
# expected: 192.168.0.143
```
## Dankclaw host DNS
The Dankclaw host itself resolves the flat names through dnsmasq:
- `/etc/systemd/resolved.conf`: `DNS=192.168.0.143 192.168.0.1` (dnsmasq first, router as fallback)
Without this, the host would resolve the names through the router to the WAN IP and time out (no hairpin).
## WAN records (Porkbun)
The Porkbun zone `franksdank.cloud` carries an A record for every vhost plus the apex, all pointing at `71.227.227.172`. TTL is 300. The zone has no wildcard record: unknown subdomains return NXDOMAIN. Caddy's DNS-01 challenges add and remove `_acme-challenge.*` TXT records through the same API during issuance. See [[Caddy]] for the TLS flow.
`franksdank.casa` is not in this Porkbun account. The `plex.franksdank.casa` vhost stays plain HTTP for that reason. The canonical Plex name is `plex.franksdank.cloud` (HTTPS).
## Failure modes
| Symptom | First check |
|---------|-------------|
| Name does not resolve on a new client | `resolv.conf` shows 192.168.0.1, not .143 — the G36 hand-off step is missing |
| Name resolves to 71.227.x and times out on the LAN | Client used the router DNS — same root cause |
| `dig @192.168.0.143` fails entirely | dnsmasq quadlet down: `systemctl --user status dnsmasq` |
| HTTPS works from the host but not the WAN | No port forward on the G36 for that service (manual router step) |
| Cert renewal fails in Caddy logs | Porkbun API credentials in the Dankclaw `.env`; test with the Porkbun API `/ping` endpoint |
## See Also
- [[Caddy]] — TLS, vhost list, DNS-01
- [[Network Architecture]] — topology, access policy
- [[Dankclaw Stack]] — the host that carries dnsmasq and Caddy
+1
View File
@@ -55,6 +55,7 @@ This repository tracks the container stacks for both hosts. Git is the single so
## Infrastructure
- [[LAN-DNS]] — name resolution, dnsmasq, router DNS hand-off
- [[Caddy]] — reverse proxy and TLS
- [[Homepage]] — password-protected service index
- [[Observability]] — Grafana, Prometheus, Loki, Alloy
+5 -5
View File
@@ -142,11 +142,11 @@ if [ '${COMPOSE_UP[$TARGET]}' = '1' ]; then
# Locally built images: caddy + porkbun module, and loki/alloy + a static
# busybox (their upstream images lack the wget the healthchecks need).
for dir in caddy loki alloy; do
img=franksdank/$dir
[ "$dir" = "caddy" ] && img=franksdank/caddy-porkbun
if ! podman image exists $img:latest; then
echo " $img image missing; building it..."
podman build -t $img:latest -f dankclaw/$dir/Containerfile .
img=franksdank/\$dir
[ \"\$dir\" = caddy ] && img=franksdank/caddy-porkbun
if ! podman image exists \$img:latest; then
echo \" \$img image missing; building it...\"
podman build -t \$img:latest -f dankclaw/\$dir/Containerfile .
fi
done
podman-compose -f '${COMPOSE[$TARGET]}' up -d