docs+install: sweep deploy/start → castle apply
Update the README, AGENTS.md, install.sh, and docs/ for the converge model: one `castle apply [name] [--plan]` replaces `deploy && start` and the per-kind enable/disable/install/start/stop verbs. CLI references now list apply + restart (the imperative bounce) + logs; recipes use `castle apply`; "turn it off" is documented as `enabled: false` + apply. install.sh's next-steps collapse to a single `castle apply`. Verified: no retired-verb command instructions remain in any markdown; install.sh syntax OK; live `castle apply --plan` reports converged; doctor all-green; core 129 / cli 31 / api 59 pass.
This commit is contained in:
@@ -204,7 +204,7 @@ A service's env is exactly its `defaults.env` — castle injects nothing
|
||||
implicitly. Values may use `${port}`/`${data_dir}`/`${name}`/`${secret:…}`
|
||||
placeholders, which deploy resolves into the registry's flat `env`.
|
||||
|
||||
**`$CASTLE_HOME/artifacts/specs/registry.yaml`** (per-node, not in the repo, generated by `castle deploy`) — Node config:
|
||||
**`$CASTLE_HOME/artifacts/specs/registry.yaml`** (per-node, not in the repo, generated by `castle apply`) — Node config:
|
||||
|
||||
```yaml
|
||||
node:
|
||||
@@ -228,7 +228,7 @@ deployed:
|
||||
```
|
||||
|
||||
The node config says what's deployed *here* and with what concrete
|
||||
values. `castle deploy` reads the spec from the repo, resolves the
|
||||
values. `castle apply` reads the spec from the repo, resolves the
|
||||
`defaults.env` placeholders and secrets, resolves binary paths,
|
||||
and writes the registry. Systemd units and Caddyfile are then generated
|
||||
from the registry — never from the spec directly.
|
||||
@@ -280,7 +280,7 @@ programs on a single node and across multiple Castle nodes.
|
||||
|
||||
**MQTT topics:**
|
||||
- `castle/{hostname}/registry` — retained JSON, full NodeRegistry.
|
||||
Published on connect and after `castle deploy`.
|
||||
Published on connect and after `castle apply`.
|
||||
- `castle/{hostname}/status` — `"online"` (retained) / `"offline"` (LWT).
|
||||
LWT ensures nodes are marked offline if they disconnect unexpectedly.
|
||||
|
||||
@@ -457,7 +457,7 @@ $CASTLE_HOME/ ← Config & artifacts (default ~/.castle)
|
||||
├── code/ ← Program source (your programs)
|
||||
│ └── <name>/
|
||||
├── artifacts/
|
||||
│ ├── specs/ ← Generated by `castle deploy`
|
||||
│ ├── specs/ ← Generated by `castle apply`
|
||||
│ │ ├── Caddyfile
|
||||
│ │ └── registry.yaml ← Node config (what's deployed here)
|
||||
│ └── content/ ← Built frontend assets
|
||||
@@ -491,7 +491,7 @@ Castle's architecture parallels Erlang/OTP, mapped onto Unix:
|
||||
| Application | Component (independent, self-contained) |
|
||||
| Application resource file | Component spec in `castle.yaml` |
|
||||
| Release config (sys.config) | Node config in `$CASTLE_HOME/artifacts/specs/registry.yaml` |
|
||||
| Release assembly | `castle deploy` (spec + node config → runtime) |
|
||||
| Release assembly | `castle apply` (spec + node config → runtime) |
|
||||
| Supervisor | systemd (restart policies, ordering) |
|
||||
| Process | Running service/worker/job |
|
||||
| Application env | Env vars |
|
||||
@@ -527,12 +527,12 @@ What exists today:
|
||||
- **CLI** — `castle` command, installed via `uv tool install --editable cli/`
|
||||
- **Three packages** — `castle-core` (models, config, generators),
|
||||
`castle-cli` (commands), `castle-api` (HTTP API)
|
||||
- **Source/runtime split** — `castle.yaml` (spec) → `castle deploy` →
|
||||
- **Source/runtime split** — `castle.yaml` (spec) → `castle apply` →
|
||||
`$CASTLE_HOME/artifacts/specs/registry.yaml` (node config). Systemd units and
|
||||
Caddyfile generated from registry with fully resolved paths. No repo references
|
||||
in runtime artifacts.
|
||||
- **Explicit env with placeholders** — a deployment's env is exactly its
|
||||
`defaults.env`; `castle deploy` resolves `${port}`/`${data_dir}`/`${name}`/
|
||||
`defaults.env`; `castle apply` resolves `${port}`/`${data_dir}`/`${name}`/
|
||||
`${secret:…}` into concrete values. No hidden convention injection.
|
||||
- **Gateway** — Caddy on port 9000, Caddyfile generated from registry
|
||||
- **API** — `castle-api` on port 9020, reads from registry (optional
|
||||
|
||||
@@ -63,7 +63,7 @@ for tools/libraries.
|
||||
at `/usr/local/bin/caddy` when `tls: acme`.
|
||||
- Systemd user units at `~/.config/systemd/user/castle-*.service` (+ `.timer`);
|
||||
the unit for program `X` is `castle-X.service`. Use drop-in `*.service.d/*.conf`
|
||||
for extra env `castle deploy` shouldn't overwrite.
|
||||
for extra env `castle apply` shouldn't overwrite.
|
||||
- The `container` launcher resolves docker via `shutil.which("docker")` (preferred
|
||||
over rootless podman on this box).
|
||||
- Service data at `$CASTLE_DATA_DIR/<name>/`; secrets at `~/.castle/secrets/`
|
||||
|
||||
@@ -122,7 +122,7 @@ origin — moving a service onto HTTPS changes its origin.
|
||||
(§DNS). Verify: `dig +short <name>.<domain>` → the node's IP.
|
||||
3. **Set `gateway.tls: acme`** (with `domain`/`acme_email`), plus the operational
|
||||
prerequisites (below).
|
||||
4. **Deploy & reload:** `castle deploy` regenerates the Caddyfile and reloads Caddy.
|
||||
4. **Deploy & reload:** `castle apply` regenerates the Caddyfile and reloads Caddy.
|
||||
5. **Update the app's origin allowlist** if it has one (§secure context).
|
||||
|
||||
## Operational prerequisites
|
||||
@@ -142,7 +142,7 @@ a DNS token.
|
||||
(`~/.castle/secrets/<TOKEN_NAME>`, scope: the DNS provider's "edit DNS records"
|
||||
permission for your zone) and map it into the gateway service env in
|
||||
`deployments/castle-gateway.yaml` (`defaults.env`), so Caddy reads it as
|
||||
`{env.<TOKEN_NAME>}`. `castle deploy` warns if the domain, env var, or secret is
|
||||
`{env.<TOKEN_NAME>}`. `castle apply` warns if the domain, env var, or secret is
|
||||
missing.
|
||||
- **`acme` — stage first.** Set `CASTLE_ACME_STAGING=1` at deploy to use Let's
|
||||
Encrypt's staging CA (generous rate limits) while verifying issuance, then unset
|
||||
|
||||
@@ -333,7 +333,7 @@ proxy: true # expose at <service-name>.<gateway.domain>
|
||||
`public: true` additionally projects a proxied service to the public internet via a
|
||||
Cloudflare tunnel, at **`<service-name>.<gateway.public_domain>`** (a separate zone,
|
||||
so internal subdomain names stay out of public DNS). Defaults to `false` — public is
|
||||
explicit — and **requires `proxy: true`**. `castle deploy` generates the cloudflared
|
||||
explicit — and **requires `proxy: true`**. `castle apply` generates the cloudflared
|
||||
ingress from the set of public services. Needs `gateway.public_domain` +
|
||||
`gateway.tunnel_id` set and the `castle-tunnel` service running; see
|
||||
@docs/tunnel-setup.md for the one-time setup.
|
||||
@@ -467,7 +467,7 @@ Setup (the parts castle can't do for you):
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${secret:CLOUDFLARE_API_TOKEN}
|
||||
```
|
||||
`castle deploy` warns if the domain, this env var, or the secret is missing.
|
||||
`castle apply` warns if the domain, this env var, or the secret is missing.
|
||||
- **LAN DNS.** Add a wildcard on your LAN's DNS server (usually the router)
|
||||
pointing `*.<domain>` at the gateway's private IP — `address=/<domain>/<gateway-ip>`
|
||||
(dnsmasq) or the equivalent A record. The public zone gets no A records, so
|
||||
@@ -493,8 +493,9 @@ manage:
|
||||
systemd: {}
|
||||
```
|
||||
|
||||
Enables `castle service enable/disable` and `castle service logs`. An empty `{}`
|
||||
uses defaults (enable=true, restart=on-failure, restart_sec=2).
|
||||
Marks the deployment systemd-managed, so `castle apply` generates and reconciles
|
||||
its unit (and `castle service logs` tails it). An empty `{}` uses defaults
|
||||
(enable=true, restart=on-failure, restart_sec=2).
|
||||
|
||||
Full options:
|
||||
```yaml
|
||||
@@ -636,17 +637,17 @@ castle program create my-service --stack python-fastapi # 1. Scaffold + regist
|
||||
cd /data/repos/my-service && uv sync # 2. Install deps
|
||||
# ... implement ...
|
||||
castle program test my-service # 3. Run tests
|
||||
castle service enable my-service # 4. Generate systemd unit, start
|
||||
castle gateway reload # 5. Update Caddy routes
|
||||
castle apply my-service # 4. Render unit + routes and reconcile (start it)
|
||||
```
|
||||
|
||||
After `service enable`, the service starts automatically on boot and restarts
|
||||
on failure. Manage with:
|
||||
`castle apply` renders the systemd unit + Caddy route and starts the service
|
||||
(which then runs on boot and restarts on failure). Manage with:
|
||||
|
||||
```bash
|
||||
castle logs my-service -f # Tail logs
|
||||
castle service run my-service # Run in foreground (for debugging)
|
||||
castle service disable my-service # Stop and remove systemd unit
|
||||
castle service restart my-service # Imperative bounce
|
||||
# To stop it durably: set `enabled: false` in its deployment, then `castle apply`
|
||||
```
|
||||
|
||||
### Tool lifecycle
|
||||
@@ -656,7 +657,7 @@ castle program create my-tool --stack python-cli # 1. Scaffold + register
|
||||
cd /data/repos/my-tool && uv sync # 2. Install deps
|
||||
# ... implement ...
|
||||
castle program test my-tool # 3. Run tests
|
||||
uv tool install --editable /data/repos/my-tool/ # 4. Install to PATH
|
||||
castle apply my-tool # 4. Install the path deployment on PATH
|
||||
```
|
||||
|
||||
### Job lifecycle
|
||||
@@ -676,7 +677,7 @@ manage:
|
||||
systemd: {}
|
||||
```
|
||||
|
||||
`castle job enable my-job` generates both a `.service` (Type=oneshot)
|
||||
`castle apply my-job` generates both a `.service` (Type=oneshot)
|
||||
and a `.timer` file.
|
||||
|
||||
## Infrastructure paths
|
||||
|
||||
@@ -171,7 +171,7 @@ See @docs/registry.md for the full registry reference.
|
||||
## Serving with Caddy
|
||||
|
||||
For production, the static build output is served by Caddy rather than a Node
|
||||
process. You do **not** write this block by hand — `castle deploy` generates it.
|
||||
process. You do **not** write this block by hand — `castle apply` generates it.
|
||||
The flow:
|
||||
|
||||
1. You build the frontend with `castle program build <name>` (deploy does **not**
|
||||
|
||||
@@ -119,7 +119,7 @@ leaves the schema intact (and says so). To destroy the data too:
|
||||
castle delete my-app --purge-data # drop schema my_app cascade
|
||||
```
|
||||
|
||||
`castle deploy` then prunes the schema from `PGRST_DB_SCHEMAS`; **restart the
|
||||
`castle apply` then prunes the schema from `PGRST_DB_SCHEMAS`; **restart the
|
||||
`supabase` service** for PostgREST to pick up the added/removed schema list.
|
||||
|
||||
## Auth, RLS & the three privacy layers
|
||||
@@ -161,7 +161,7 @@ service itself is likewise at `supabase.<gateway.domain>`.) See
|
||||
castle program create my-app --stack supabase --description "..." # scaffold + register
|
||||
castle program build my-app # apply unapplied migrations to the substrate
|
||||
castle program test my-app # deno test over functions/ (if deno present)
|
||||
castle deploy && castle gateway reload # serve the static UI at /my-app/
|
||||
castle apply # serve the static UI at /my-app/
|
||||
```
|
||||
|
||||
## Scaffolding
|
||||
|
||||
@@ -17,7 +17,7 @@ internet at `<name>.<gateway.public_domain>`, via a Cloudflare Tunnel.
|
||||
Cloudflare (no inbound holes, no public IP needed) and forwards each public
|
||||
hostname to the gateway on `:443`, rewriting the Host header and TLS SNI to the
|
||||
*internal* name so Caddy routes it and its wildcard cert validates. The public
|
||||
surface is exactly the set of `public: true` services — `castle deploy` generates
|
||||
surface is exactly the set of `public: true` services — `castle apply` generates
|
||||
the ingress from the registry.
|
||||
- **One kill switch.** Stop `castle-tunnel` → instantly nothing is public.
|
||||
|
||||
@@ -76,8 +76,8 @@ manage:
|
||||
Bring it online:
|
||||
|
||||
```bash
|
||||
castle deploy # writes cloudflared.yml from public services
|
||||
castle service enable castle-tunnel # start the tunnel
|
||||
castle apply # writes cloudflared.yml from public services
|
||||
castle apply castle-tunnel # start the tunnel
|
||||
```
|
||||
|
||||
## Using the toggle
|
||||
@@ -92,10 +92,10 @@ public: true # also expose at <name>.pub.payne.io via the tunnel
|
||||
Then just deploy:
|
||||
|
||||
```bash
|
||||
castle deploy
|
||||
castle apply
|
||||
```
|
||||
|
||||
`castle deploy` regenerates `~/.castle/artifacts/specs/cloudflared.yml` from the
|
||||
`castle apply` regenerates `~/.castle/artifacts/specs/cloudflared.yml` from the
|
||||
current set of public services and restarts `castle-tunnel`. Flip `public` back to
|
||||
`false` (or remove it) and redeploy to un-expose — the hostname drops out of the
|
||||
ingress immediately.
|
||||
@@ -116,14 +116,14 @@ printf %s "<token>" > ~/.castle/secrets/CLOUDFLARE_PUBLIC_DNS_TOKEN
|
||||
chmod 600 ~/.castle/secrets/CLOUDFLARE_PUBLIC_DNS_TOKEN
|
||||
```
|
||||
|
||||
With it present, every `castle deploy` **reconciles** the public CNAMEs against the
|
||||
With it present, every `castle apply` **reconciles** the public CNAMEs against the
|
||||
current public set — creating missing ones and deleting removed ones — and prints a
|
||||
one-line summary. It only ever touches records pointing at *this* tunnel
|
||||
(`<tunnel_id>.cfargotunnel.com`), so hand-managed records in the same zone are safe.
|
||||
This token is separate from `CLOUDFLARE_API_TOKEN` (which is the ACME token for the
|
||||
internal zone); the public zone is usually a different zone/account.
|
||||
|
||||
Without the token, `castle deploy` instead prints the exact command to run per host:
|
||||
Without the token, `castle apply` instead prints the exact command to run per host:
|
||||
|
||||
```bash
|
||||
cloudflared tunnel route dns <tunnel-id> <name>.pub.payne.io
|
||||
|
||||
Reference in New Issue
Block a user