Flatten proxy config to a bool (drop the caddy key)
CaddySpec had only `enable` left (path_prefix/host removed; extra_snippets was
unused), so `proxy: { caddy: {} }` was three levels of nesting for a checkbox —
and it invited drift (supabase silently reverted to proxy.caddy.host). Replace it
with a plain `ServiceSpec.proxy: bool`.
- Model: remove ProxySpec/CaddySpec; `proxy: bool = False`. Readers simplified to
`bool(svc.proxy)` (generator, castle-api summaries, CLI info).
- CLI create / dashboard editor + create form: write `proxy: true` / a checkbox.
- Configs migrated to `proxy: true` (and supabase's stale proxy.caddy.host block
removed).
- Docs (registry, dns-and-tls, design, stack guides) + tests/fixtures updated;
also fixed lingering path-model staleness in the stack guides.
This commit is contained in:
@@ -74,7 +74,7 @@ export function CreateDeploymentForm({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (port && expose) base.proxy = { caddy: {} }
|
if (port && expose) base.proxy = true
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
|||||||
const run = obj(m.run)
|
const run = obj(m.run)
|
||||||
const internal = obj(obj(obj(m.expose).http).internal)
|
const internal = obj(obj(obj(m.expose).http).internal)
|
||||||
const httpExpose = obj(obj(m.expose).http)
|
const httpExpose = obj(obj(m.expose).http)
|
||||||
const caddyRaw = obj(m.proxy).caddy
|
|
||||||
|
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [saved, setSaved] = useState(false)
|
const [saved, setSaved] = useState(false)
|
||||||
@@ -29,8 +28,8 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
|||||||
)
|
)
|
||||||
const [port, setPort] = useState(internal.port != null ? String(internal.port) : "")
|
const [port, setPort] = useState(internal.port != null ? String(internal.port) : "")
|
||||||
const [health, setHealth] = useState((httpExpose.health_path as string) ?? "")
|
const [health, setHealth] = useState((httpExpose.health_path as string) ?? "")
|
||||||
// Exposed at <service-name>.<gateway.domain> when proxy.caddy is present + enabled.
|
// Exposed at <service-name>.<gateway.domain> when proxy is true.
|
||||||
const [expose, setExpose] = useState(caddyRaw !== undefined && obj(caddyRaw).enable !== false)
|
const [expose, setExpose] = useState(m.proxy === true)
|
||||||
|
|
||||||
const { element: envEditor, merged } = useEnvSecrets(obj(obj(m.defaults).env) as Record<string, string>)
|
const { element: envEditor, merged } = useEnvSecrets(obj(obj(m.defaults).env) as Record<string, string>)
|
||||||
|
|
||||||
@@ -62,7 +61,7 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
|||||||
delete config.expose
|
delete config.expose
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expose) config.proxy = { caddy: {} }
|
if (expose) config.proxy = true
|
||||||
else delete config.proxy
|
else delete config.proxy
|
||||||
|
|
||||||
const env = merged()
|
const env = merged()
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ def _summary_from_service(
|
|||||||
if svc.expose and svc.expose.http:
|
if svc.expose and svc.expose.http:
|
||||||
port = svc.expose.http.internal.port
|
port = svc.expose.http.internal.port
|
||||||
health_path = svc.expose.http.health_path
|
health_path = svc.expose.http.health_path
|
||||||
subdomain = name if (svc.proxy and svc.proxy.caddy and svc.proxy.caddy.enable) else None
|
subdomain = name if svc.proxy else None
|
||||||
|
|
||||||
managed = bool(svc.manage and svc.manage.systemd and svc.manage.systemd.enable)
|
managed = bool(svc.manage and svc.manage.systemd and svc.manage.systemd.enable)
|
||||||
|
|
||||||
@@ -278,7 +278,7 @@ def _service_from_spec(name: str, svc: ServiceSpec, config: object) -> ServiceSu
|
|||||||
port = svc.expose.http.internal.port
|
port = svc.expose.http.internal.port
|
||||||
health_path = svc.expose.http.health_path
|
health_path = svc.expose.http.health_path
|
||||||
# Exposed at <name>.<domain> when the proxy checkbox is on.
|
# Exposed at <name>.<domain> when the proxy checkbox is on.
|
||||||
subdomain = name if (svc.proxy and svc.proxy.caddy and svc.proxy.caddy.enable) else None
|
subdomain = name if svc.proxy else None
|
||||||
|
|
||||||
managed = bool(svc.manage and svc.manage.systemd and svc.manage.systemd.enable)
|
managed = bool(svc.manage and svc.manage.systemd and svc.manage.systemd.enable)
|
||||||
systemd_info = _make_systemd_info(name) if managed else None
|
systemd_info = _make_systemd_info(name) if managed else None
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
|
|||||||
"health_path": "/health",
|
"health_path": "/health",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxy": {"caddy": {"path_prefix": "/test-svc"}},
|
"proxy": True,
|
||||||
"manage": {"systemd": {}},
|
"manage": {"systemd": {}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,14 +8,12 @@ import subprocess
|
|||||||
from castle_cli.config import REPOS_DIR, load_config, save_config
|
from castle_cli.config import REPOS_DIR, load_config, save_config
|
||||||
from castle_cli.manifest import (
|
from castle_cli.manifest import (
|
||||||
BuildSpec,
|
BuildSpec,
|
||||||
CaddySpec,
|
|
||||||
DefaultsSpec,
|
DefaultsSpec,
|
||||||
ExposeSpec,
|
ExposeSpec,
|
||||||
HttpExposeSpec,
|
HttpExposeSpec,
|
||||||
HttpInternal,
|
HttpInternal,
|
||||||
ManageSpec,
|
ManageSpec,
|
||||||
ProgramSpec,
|
ProgramSpec,
|
||||||
ProxySpec,
|
|
||||||
RunPython,
|
RunPython,
|
||||||
ServiceSpec,
|
ServiceSpec,
|
||||||
SystemdSpec,
|
SystemdSpec,
|
||||||
@@ -121,7 +119,7 @@ def run_create(args: argparse.Namespace) -> int:
|
|||||||
health_path="/health",
|
health_path="/health",
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
proxy=ProxySpec(caddy=CaddySpec()), # expose at <name>.<gateway.domain>
|
proxy=True, # expose at <name>.<gateway.domain>
|
||||||
manage=ManageSpec(systemd=SystemdSpec()),
|
manage=ManageSpec(systemd=SystemdSpec()),
|
||||||
# python-fastapi scaffold reads env_prefix MY_SERVICE_ — map castle's
|
# python-fastapi scaffold reads env_prefix MY_SERVICE_ — map castle's
|
||||||
# computed port/data dir to those vars (explicit, no hidden injection).
|
# computed port/data dir to those vars (explicit, no hidden injection).
|
||||||
|
|||||||
@@ -11,14 +11,12 @@ import argparse
|
|||||||
|
|
||||||
from castle_cli.config import load_config, save_config
|
from castle_cli.config import load_config, save_config
|
||||||
from castle_cli.manifest import (
|
from castle_cli.manifest import (
|
||||||
CaddySpec,
|
|
||||||
DefaultsSpec,
|
DefaultsSpec,
|
||||||
ExposeSpec,
|
ExposeSpec,
|
||||||
HttpExposeSpec,
|
HttpExposeSpec,
|
||||||
HttpInternal,
|
HttpInternal,
|
||||||
JobSpec,
|
JobSpec,
|
||||||
ManageSpec,
|
ManageSpec,
|
||||||
ProxySpec,
|
|
||||||
RunCommand,
|
RunCommand,
|
||||||
RunPython,
|
RunPython,
|
||||||
ServiceSpec,
|
ServiceSpec,
|
||||||
@@ -62,7 +60,7 @@ def run_service_create(args: argparse.Namespace) -> int:
|
|||||||
run = _run_spec(args.runner, args.run or args.program or name, name)
|
run = _run_spec(args.runner, args.run or args.program or name, name)
|
||||||
|
|
||||||
expose = None
|
expose = None
|
||||||
proxy = None
|
proxy = False
|
||||||
if args.port is not None:
|
if args.port is not None:
|
||||||
expose = ExposeSpec(
|
expose = ExposeSpec(
|
||||||
http=HttpExposeSpec(
|
http=HttpExposeSpec(
|
||||||
@@ -70,9 +68,8 @@ def run_service_create(args: argparse.Namespace) -> int:
|
|||||||
health_path=args.health,
|
health_path=args.health,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if not args.no_proxy:
|
|
||||||
# Expose at <name>.<gateway.domain> (the subdomain is the service name).
|
# Expose at <name>.<gateway.domain> (the subdomain is the service name).
|
||||||
proxy = ProxySpec(caddy=CaddySpec())
|
proxy = not args.no_proxy
|
||||||
|
|
||||||
config.services[name] = ServiceSpec(
|
config.services[name] = ServiceSpec(
|
||||||
id=name,
|
id=name,
|
||||||
@@ -90,7 +87,7 @@ def run_service_create(args: argparse.Namespace) -> int:
|
|||||||
print(f" runs: {args.runner} ({args.run or args.program or name})")
|
print(f" runs: {args.runner} ({args.run or args.program or name})")
|
||||||
if expose:
|
if expose:
|
||||||
print(f" port: {args.port}")
|
print(f" port: {args.port}")
|
||||||
if proxy and proxy.caddy:
|
if proxy:
|
||||||
print(f" subdomain: {name}.<gateway.domain>")
|
print(f" subdomain: {name}.<gateway.domain>")
|
||||||
print(f"\nNext: castle service deploy {name} && castle service start {name}")
|
print(f"\nNext: castle service deploy {name} && castle service start {name}")
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ def run_info(args: argparse.Namespace) -> int:
|
|||||||
print(f" {BOLD}port{RESET}: {http.internal.port}")
|
print(f" {BOLD}port{RESET}: {http.internal.port}")
|
||||||
if http.health_path:
|
if http.health_path:
|
||||||
print(f" {BOLD}health{RESET}: {http.health_path}")
|
print(f" {BOLD}health{RESET}: {http.health_path}")
|
||||||
if service.proxy and service.proxy.caddy and service.proxy.caddy.enable:
|
if service.proxy:
|
||||||
print(f" {BOLD}subdomain{RESET}: {name}.<gateway.domain>")
|
print(f" {BOLD}subdomain{RESET}: {name}.<gateway.domain>")
|
||||||
if service.manage and service.manage.systemd:
|
if service.manage and service.manage.systemd:
|
||||||
sd = service.manage.systemd
|
sd = service.manage.systemd
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
from castle_core.manifest import * # noqa: F401, F403
|
from castle_core.manifest import * # noqa: F401, F403
|
||||||
from castle_core.manifest import ( # noqa: F401 — explicit re-exports for type checkers
|
from castle_core.manifest import ( # noqa: F401 — explicit re-exports for type checkers
|
||||||
BuildSpec,
|
BuildSpec,
|
||||||
CaddySpec,
|
|
||||||
Capability,
|
Capability,
|
||||||
CommandsSpec,
|
CommandsSpec,
|
||||||
DefaultsSpec,
|
DefaultsSpec,
|
||||||
@@ -15,7 +14,6 @@ from castle_core.manifest import ( # noqa: F401 — explicit re-exports for typ
|
|||||||
JobSpec,
|
JobSpec,
|
||||||
ManageSpec,
|
ManageSpec,
|
||||||
ProgramSpec,
|
ProgramSpec,
|
||||||
ProxySpec,
|
|
||||||
ReadinessHttpGet,
|
ReadinessHttpGet,
|
||||||
RestartPolicy,
|
RestartPolicy,
|
||||||
RunBase,
|
RunBase,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ def service_proxy_targets(name: str, svc: ServiceSpec) -> ProxyTargets:
|
|||||||
port = None
|
port = None
|
||||||
if svc.expose and svc.expose.http:
|
if svc.expose and svc.expose.http:
|
||||||
port = svc.expose.http.internal.port
|
port = svc.expose.http.internal.port
|
||||||
expose = bool(svc.proxy and svc.proxy.caddy and svc.proxy.caddy.enable)
|
expose = bool(svc.proxy)
|
||||||
base_url = getattr(svc.run, "base_url", None)
|
base_url = getattr(svc.run, "base_url", None)
|
||||||
return expose, port, base_url
|
return expose, port, base_url
|
||||||
|
|
||||||
|
|||||||
@@ -145,18 +145,6 @@ class ExposeSpec(BaseModel):
|
|||||||
http: HttpExposeSpec | None = None
|
http: HttpExposeSpec | None = None
|
||||||
|
|
||||||
|
|
||||||
class CaddySpec(BaseModel):
|
|
||||||
# The "expose" checkbox: when present and enabled, the gateway routes
|
|
||||||
# <service-name>.<gateway.domain> to this service (whole host → backend root).
|
|
||||||
# The subdomain is always the service name — rename the service to change it.
|
|
||||||
enable: bool = True
|
|
||||||
extra_snippets: list[str] = Field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
class ProxySpec(BaseModel):
|
|
||||||
caddy: CaddySpec | None = None
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------
|
# ---------------------
|
||||||
# Build spec
|
# Build spec
|
||||||
# ---------------------
|
# ---------------------
|
||||||
@@ -277,7 +265,9 @@ class ServiceSpec(BaseModel):
|
|||||||
run: RunSpec
|
run: RunSpec
|
||||||
|
|
||||||
expose: ExposeSpec | None = None
|
expose: ExposeSpec | None = None
|
||||||
proxy: ProxySpec | None = None
|
# Expose this service at <service-name>.<gateway.domain> through the gateway
|
||||||
|
# (the subdomain is the service name). False → reachable only at its host:port.
|
||||||
|
proxy: bool = False
|
||||||
manage: ManageSpec | None = None
|
manage: ManageSpec | None = None
|
||||||
defaults: DefaultsSpec | None = None
|
defaults: DefaultsSpec | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -58,9 +58,7 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
|
|||||||
"health_path": "/health",
|
"health_path": "/health",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": True,
|
||||||
"caddy": {"path_prefix": "/test-svc"},
|
|
||||||
},
|
|
||||||
"manage": {
|
"manage": {
|
||||||
"systemd": {},
|
"systemd": {},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ from castle_core.generators.caddyfile import (
|
|||||||
)
|
)
|
||||||
from castle_core.manifest import (
|
from castle_core.manifest import (
|
||||||
BuildSpec,
|
BuildSpec,
|
||||||
CaddySpec,
|
|
||||||
ExposeSpec,
|
ExposeSpec,
|
||||||
HttpExposeSpec,
|
HttpExposeSpec,
|
||||||
HttpInternal,
|
HttpInternal,
|
||||||
ProgramSpec,
|
ProgramSpec,
|
||||||
ProxySpec,
|
|
||||||
RunPython,
|
RunPython,
|
||||||
ServiceSpec,
|
ServiceSpec,
|
||||||
)
|
)
|
||||||
@@ -151,7 +149,7 @@ def _service(port: int, *, expose: bool) -> ServiceSpec:
|
|||||||
return ServiceSpec(
|
return ServiceSpec(
|
||||||
run=RunPython(runner="python", program="svc"),
|
run=RunPython(runner="python", program="svc"),
|
||||||
expose=ExposeSpec(http=HttpExposeSpec(internal=HttpInternal(port=port))),
|
expose=ExposeSpec(http=HttpExposeSpec(internal=HttpInternal(port=port))),
|
||||||
proxy=ProxySpec(caddy=CaddySpec()) if expose else None,
|
proxy=expose,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class TestLoadConfig:
|
|||||||
"""Service has correct proxy spec."""
|
"""Service has correct proxy spec."""
|
||||||
config = load_config(castle_root)
|
config = load_config(castle_root)
|
||||||
svc = config.services["test-svc"]
|
svc = config.services["test-svc"]
|
||||||
assert svc.proxy.caddy.enable is True # exposed at <name>.<gateway.domain>
|
assert svc.proxy is True # exposed at <name>.<gateway.domain>
|
||||||
|
|
||||||
def test_service_run_spec(self, castle_root: Path) -> None:
|
def test_service_run_spec(self, castle_root: Path) -> None:
|
||||||
"""Service has correct RunSpec."""
|
"""Service has correct RunSpec."""
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ from __future__ import annotations
|
|||||||
import pytest
|
import pytest
|
||||||
from castle_core.manifest import (
|
from castle_core.manifest import (
|
||||||
BuildSpec,
|
BuildSpec,
|
||||||
CaddySpec,
|
|
||||||
ProgramSpec,
|
ProgramSpec,
|
||||||
ExposeSpec,
|
ExposeSpec,
|
||||||
HttpExposeSpec,
|
HttpExposeSpec,
|
||||||
HttpInternal,
|
HttpInternal,
|
||||||
JobSpec,
|
JobSpec,
|
||||||
ManageSpec,
|
ManageSpec,
|
||||||
ProxySpec,
|
|
||||||
RunCommand,
|
RunCommand,
|
||||||
RunPython,
|
RunPython,
|
||||||
RunRemote,
|
RunRemote,
|
||||||
@@ -92,9 +90,9 @@ class TestServiceSpec:
|
|||||||
s = ServiceSpec(
|
s = ServiceSpec(
|
||||||
id="svc",
|
id="svc",
|
||||||
run=RunPython(runner="python", program="svc"),
|
run=RunPython(runner="python", program="svc"),
|
||||||
proxy=ProxySpec(caddy=CaddySpec()),
|
proxy=True,
|
||||||
)
|
)
|
||||||
assert s.proxy.caddy.enable is True
|
assert s.proxy is True
|
||||||
|
|
||||||
def test_service_with_manage(self) -> None:
|
def test_service_with_manage(self) -> None:
|
||||||
"""Service with systemd management."""
|
"""Service with systemd management."""
|
||||||
@@ -185,10 +183,10 @@ class TestModelSerialization:
|
|||||||
internal=HttpInternal(port=9001), health_path="/health"
|
internal=HttpInternal(port=9001), health_path="/health"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
proxy=ProxySpec(caddy=CaddySpec()),
|
proxy=True,
|
||||||
manage=ManageSpec(systemd=SystemdSpec()),
|
manage=ManageSpec(systemd=SystemdSpec()),
|
||||||
)
|
)
|
||||||
data = s.model_dump(exclude_none=True, exclude={"id"})
|
data = s.model_dump(exclude_none=True, exclude={"id"})
|
||||||
assert data["run"]["runner"] == "python"
|
assert data["run"]["runner"] == "python"
|
||||||
assert data["expose"]["http"]["internal"]["port"] == 9001
|
assert data["expose"]["http"]["internal"]["port"] == 9001
|
||||||
assert data["proxy"]["caddy"]["enable"] is True
|
assert data["proxy"] is True
|
||||||
|
|||||||
@@ -170,9 +170,7 @@ services:
|
|||||||
http:
|
http:
|
||||||
internal: { port: 9001 }
|
internal: { port: 9001 }
|
||||||
health_path: /health
|
health_path: /health
|
||||||
proxy:
|
proxy: true # expose at central-context.<gateway.domain>
|
||||||
caddy:
|
|
||||||
path_prefix: /central-context
|
|
||||||
manage:
|
manage:
|
||||||
systemd: {}
|
systemd: {}
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ browser on your LAN:
|
|||||||
2. **Trust** — is the certificate the node serves one the browser accepts? *(TLS)*
|
2. **Trust** — is the certificate the node serves one the browser accepts? *(TLS)*
|
||||||
|
|
||||||
Castle answers #1 by leaning on your LAN's own DNS, and #2 with a per-node choice
|
Castle answers #1 by leaning on your LAN's own DNS, and #2 with a per-node choice
|
||||||
of three TLS modes. They're orthogonal: you pick a resolution strategy and a trust
|
of two TLS modes. They're orthogonal: you pick a resolution strategy and a trust
|
||||||
strategy, and any working combination is fine.
|
strategy, and any working combination is fine.
|
||||||
|
|
||||||
## The gateway is the single ingress
|
## The gateway is the single ingress
|
||||||
|
|
||||||
Every gateway-reachable service goes through the Caddy **gateway**. Exposure is a
|
Every gateway-reachable service goes through the Caddy **gateway**. Exposure is a
|
||||||
single **checkbox** (`proxy.caddy` on a service):
|
single **checkbox** (`proxy: true` on a service):
|
||||||
|
|
||||||
- **unchecked** — the service is reachable only at its own `host:port` (no gateway
|
- **unchecked** — the service is reachable only at its own `host:port` (no gateway
|
||||||
route, no DNS name).
|
route, no DNS name).
|
||||||
@@ -116,7 +116,7 @@ origin — moving a service onto HTTPS changes its origin.
|
|||||||
|
|
||||||
## Putting a service on trusted HTTPS — the recipe
|
## Putting a service on trusted HTTPS — the recipe
|
||||||
|
|
||||||
1. **Check the box.** Add `proxy: { caddy: {} }` to the service — it's now exposed
|
1. **Check the box.** Add `proxy: true` to the service — it's now exposed
|
||||||
at `<service-name>.<gateway.domain>` (rename the service to change the name).
|
at `<service-name>.<gateway.domain>` (rename the service to change the name).
|
||||||
2. **Make the name resolve.** Add (or rely on) the LAN wildcard for the zone
|
2. **Make the name resolve.** Add (or rely on) the LAN wildcard for the zone
|
||||||
(§DNS). Verify: `dig +short <name>.<domain>` → the node's IP.
|
(§DNS). Verify: `dig +short <name>.<domain>` → the node's IP.
|
||||||
|
|||||||
@@ -79,8 +79,7 @@ expose:
|
|||||||
http:
|
http:
|
||||||
internal: { port: 9001 }
|
internal: { port: 9001 }
|
||||||
health_path: /health
|
health_path: /health
|
||||||
proxy:
|
proxy: true # expose at my-service.<gateway.domain>
|
||||||
caddy: {} # expose at my-service.<gateway.domain>
|
|
||||||
manage:
|
manage:
|
||||||
systemd: {}
|
systemd: {}
|
||||||
```
|
```
|
||||||
@@ -272,13 +271,12 @@ expose:
|
|||||||
|
|
||||||
### `proxy` — Expose the service at a subdomain
|
### `proxy` — Expose the service at a subdomain
|
||||||
|
|
||||||
`proxy.caddy` is a **checkbox**: present (and `enable: true`, the default) means the
|
`proxy` is a **checkbox** (a bool): `true` means the gateway routes
|
||||||
gateway routes **`<service-name>.<gateway.domain>`** to this service; absent means
|
**`<service-name>.<gateway.domain>`** to this service; omitted/`false` means the
|
||||||
the service is reachable only at its own `host:port`.
|
service is reachable only at its own `host:port`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
proxy:
|
proxy: true # expose at <service-name>.<gateway.domain>
|
||||||
caddy: {} # expose at <service-name>.<gateway.domain>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The subdomain is always the service name — there's nothing to customize (rename the
|
The subdomain is always the service name — there's nothing to customize (rename the
|
||||||
@@ -334,7 +332,7 @@ version number"). `gateway.tls` has two values:
|
|||||||
| `acme` | one `*.<domain>` `:443` site | matcher inside the wildcard site | **real Let's Encrypt wildcard, no CA install** |
|
| `acme` | one `*.<domain>` `:443` site | matcher inside the wildcard site | **real Let's Encrypt wildcard, no CA install** |
|
||||||
|
|
||||||
Path-prefix and static routes always stay on the HTTP `:<port>` site — the way to
|
Path-prefix and static routes always stay on the HTTP `:<port>` site — the way to
|
||||||
put a service on HTTPS is to give it a `proxy.caddy.host`. A node with no public
|
put a service on HTTPS is to set `proxy: true` (and use acme mode). A node with no public
|
||||||
domain stays on `off` (plain HTTP; use `localhost`/direct ports for anything that
|
domain stays on `off` (plain HTTP; use `localhost`/direct ports for anything that
|
||||||
needs a secure context).
|
needs a secure context).
|
||||||
|
|
||||||
@@ -387,7 +385,7 @@ it needs **no inbound exposure and no public A records** for the services. Only
|
|||||||
**LAN DNS** resolves `*.<domain>` to the gateway's private IP. (HTTP-01 can't
|
**LAN DNS** resolves `*.<domain>` to the gateway's private IP. (HTTP-01 can't
|
||||||
validate a wildcard, so DNS-01 — and thus the provider token — is mandatory here.)
|
validate a wildcard, so DNS-01 — and thus the provider token — is mandatory here.)
|
||||||
|
|
||||||
Every subdomain is the **service name**: a service checks the `proxy.caddy` box and
|
Every subdomain is the **service name**: a service sets `proxy: true` and
|
||||||
is published at `<name>.<domain>`. Services stay domain-agnostic (switching
|
is published at `<name>.<domain>`. Services stay domain-agnostic (switching
|
||||||
`gateway.domain` needs no service edits). One `*.<domain>` site means a single cert
|
`gateway.domain` needs no service edits). One `*.<domain>` site means a single cert
|
||||||
covers every route — adding a service needs no new cert.
|
covers every route — adding a service needs no new cert.
|
||||||
@@ -560,8 +558,7 @@ services:
|
|||||||
http:
|
http:
|
||||||
internal: { port: 9001 }
|
internal: { port: 9001 }
|
||||||
health_path: /health
|
health_path: /health
|
||||||
proxy:
|
proxy: true # expose at my-service.<gateway.domain>
|
||||||
caddy: {} # expose at my-service.<gateway.domain>
|
|
||||||
manage:
|
manage:
|
||||||
systemd: {}
|
systemd: {}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -123,8 +123,7 @@ services:
|
|||||||
http:
|
http:
|
||||||
internal: { port: 9001 }
|
internal: { port: 9001 }
|
||||||
health_path: /health
|
health_path: /health
|
||||||
proxy:
|
proxy: true # expose at my-service.<gateway.domain>
|
||||||
caddy: { path_prefix: /my-service }
|
|
||||||
manage:
|
manage:
|
||||||
systemd: {}
|
systemd: {}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ import { defineConfig } from "vite"
|
|||||||
import react from "@vitejs/plugin-react"
|
import react from "@vitejs/plugin-react"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
// Castle sets VITE_BASE to the gateway serve prefix at build time (`/<name>/`,
|
// Castle sets VITE_BASE=/ at build time (every frontend serves at its
|
||||||
// or `/` for the root app). Reading it here makes the bundle's absolute asset
|
// subdomain root). Reading it here keeps the bundle.s absolute asset
|
||||||
// URLs resolve when served behind the gateway at a subpath — no hand-tuning.
|
// URLs resolving at the root — no hand-tuning.
|
||||||
base: process.env.VITE_BASE ?? "/",
|
base: process.env.VITE_BASE ?? "/",
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
resolve: {
|
resolve: {
|
||||||
@@ -157,8 +157,7 @@ services:
|
|||||||
expose:
|
expose:
|
||||||
http:
|
http:
|
||||||
internal: { port: 5173 }
|
internal: { port: 5173 }
|
||||||
proxy:
|
proxy: true # expose the dev server at my-frontend.<gateway.domain>
|
||||||
caddy: {} # expose the dev server at my-frontend.<gateway.domain>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
See @docs/registry.md for the full registry reference.
|
See @docs/registry.md for the full registry reference.
|
||||||
|
|||||||
@@ -117,11 +117,12 @@ the function holds credentials and can meter usage.
|
|||||||
|
|
||||||
## Gateway & secure context
|
## Gateway & secure context
|
||||||
|
|
||||||
A `public` app is fine on the gateway's HTTP static route at `/my-app/`. An app
|
A supabase app is a `frontend` program (its `public/` is served in place), so the
|
||||||
that uses **auth or WebCrypto** needs a **secure context**, so give it its own
|
gateway serves it at its own subdomain `<name>.<gateway.domain>`. With
|
||||||
HTTPS host route instead — `proxy.caddy.host: my-app.lan` with `gateway.tls:
|
`gateway.tls: acme` that subdomain is HTTPS — a **secure context**, which apps
|
||||||
internal` — exactly like the substrate itself (`supabase.lan`). See the "HTTPS for
|
using **auth or WebCrypto** require — with no private CA to install. (The substrate
|
||||||
host routes" section of @docs/registry.md.
|
service itself is likewise at `supabase.<gateway.domain>`.) See
|
||||||
|
@docs/dns-and-tls.md.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user